Mostly that depends on what you want to do and how you define the task. It's best to spend time making that definition as logical, clear and simple as possible. It should be the first thing that you debug, shrink and debug again. I write the steps as comments and then fill in the code which can even still have me change those comments to suit what actually worked.
When I write something linear like an example to show how unsigned math works, I fit it all in setup() and leave loop() empty. Top-down does fit some tasks.
For most all real-time programs with the Arduino I think that setup() and loop() are the way to go with a constantly repeating fast loop() at the heart of it. Whether you use Objects or not it is good to divide the program into separate small parts and control which of those run during any single execution of loop(). That strategy gives you advantages, check it out:
Separate small parts don't have to be weaved inside control structures. Each one runs only on a certain condition which may be the value of a global or static variable holding the state of the overall process, a kind of "what am I doing now" value that survives from one execution of loop() to the next.
One part may read a serial char if available, do something with it and change the state variable so that next time through loop it does the next task, but only if a separator char was read. The first part may be gathering digits and evaluating them to a number that is complete when a space is read so the next part that uses that number is run next time around loop() by changing the state.
The first part only runs if serial is available, the second only if the state is right. Nothing holds up execution and loop() may run many many times between serial reads.
One part may be watching the time to blink a led on/off and run every time through loop(). It should probably be above the serial available check as it is more time-critical.
Other parts may be watching sensors or other time-checks and/or for other state values.
But ALL those parts are separate blocks of code. Each could be a function but for the sake of speed, readability and stack use, keep the condition checks inside of loop() and have the function called only when the condition is met.
It will be much easier to debug separate parts than to debug the same functionality inside of control structures made to handle what state and time values do. Re-arranging the order of parts, adding or removing parts is greatly simplified through using separate parts rather than highly structured code.
This works well for me. What I write for one thing often has the skeleton of a future sketch in it that I just knock the parts I can't use out, reform the parts that are close and keep the ones that don't need change and I have then new thing much faster than if I wrote it ground-up.
You can look up Finite State Machine for more details and of course BlinkWithoutDelay to see more about time checking. You can mix those together inside loop(), even have multiple state machines together if you need or want. Of course if you write tight code then you only want what you need, right?
Class objects are collections of data with related functions. They're a way to package related code and data. I'm all for OOP, have been since 1983 when I learned OOP with Forth-79.
One code practice I object to on Arduino is dynamic memory allocation. Another is wasting memory. Use of String Objects does both so I really don't like using Strings on the Arduino. I prefer to plan out memory use ahead of time and shift data in and out of buffers (maybe to/from SD card or external RAM) if necessary. I code as if I'm short of RAM because starting with 2k for heap and stack, I am!