In Praise of Top Down Programming
i-programmer.info
i-programmer.info
When there are no particularly hard/murky parts left, it's top-down from there, and that let's you choose the approach for various portions of the solution (such as, this part will be pure functions, this part will be based around an arena allocation, etc.).
Even though I mentioned both bottom-up and top-down, the 10,000 foot summary is bottom-up, because it starts there and the most time is spent in that mode.
Whenever I get away from that and go top down too early, I'm likely to run into some area that either gets reworked, or that I compromise on and _wish_ I could justify the time to rework it.
(And also with the caveat that some things are simple enough you could do them any way you want, and those are good candidates for top-down, which also seems more comfortable for more junior devs).
UPDATE: However, I completely agree with another commentor -- start with the UI first! I don't think that contradicts bottom-up ... it's about fleshing out both the requirements and the _potential_ requirements.
However, I'm also in a lucky position where I'm not responsible for the UI or any customer facing stuff. My users are internal services that interface with my APIs, which rarely need to be changed.
This has several benefits:
- it forces to confront reality of the problem and field
- you have to talk to people, and understand their needs
- you almost get your backend public API out of it for free
After that, you don't really need to do full top down, you can split in subsystems, and works on small parts of those subsystems.
It doesn't have to be a good looking or sophisticated UI. It can be a ugly, as long as it's practical and validated by the end user.
But this works only well the if the problem can be well defined and you know how to solve it.
If you need to figure it out, top down doesn't work as well.
A tick/tock of UI/functionality keeps those people from getting depressed during demo meetings.
- reassuring the people that pay the bill
- get PR feedback
- give a concrete objective with a deadline to the team
But like a lot of things, it's not useful for what most people think it's useful.
I suspect you are coming from a design background because people who have never built a UI before are always very suspicious of this suggestion.
Coding just happen to be a mean to solve their problems, but they don't understand code, and I don't understand their problem.
After a lot of frustrating client/dev interactions, I came to read "don't make me think" from Steve Krug. I realized the UI was the only language I shared with my customers to start a meaningful discussion.
This turned out to work well, and got me paid, so I sticked to it.
But... not trying to say "build the UI first" is a bad choice if it works for you. I do mostly backend work on "high trust" transactions, so I tend to look at the API first and see if it matches the requirements i have at the time.
We did work from a designer’s mockup, though.
The UI is the answer to that.
"A form with three text inputs labeled street, state and zip" is solution while "the system should allow the user to specify an address" is a problem statement. (And ideally it should be more detailed with respect to its context: "The system should allow a user to specify an address associated with a delivery record, associated with the user's id.")
So the UI is the best road to build one in collaboration with the client.
Approaches to neatly building a well-understood thing are radically different from approaches to building / tweaking / rebuilding a new, so far untried thing.
Software engineering is most valuable around the latter kind of thing; the former kind of thing is usually already built and open-sourced.
The dilemma there is that most of us only get to build things for the second time if we are working on a copy-cat product from a competitor, and those are not the sorts of projects we glorify. Everyone wants to work on new ideas, not copy-cats.
It's not exactly the same thing, but the amount of well-known stuff in it is pretty large. Here's where frameworks like Rails, or Django, or Next.js shine: they have much of the expected, n-th time code factored out, you need to glue predefined parts in predefined ways, and get an expected result.
Somewhere in the middle, however, we all need to go down the layers to either store something in a database or figure out how to work on a collection of items. This turns the process bottom-up to some extend.
Then the very last phase is going back and forth until both solutions meet in the middle.
One of these, talking about his own mentor, asked me once, rhetorically, "is he successful because of his theories, or because he uses the same people on every project?" Wish that some of my previous bosses were that aware. They didn't always see the detailed work their senior people were doing to keep the wheels on. You always assume they see that about you when it comes to who they chose to report, but to hear them enumerate the virtues of the team, and omit such things, that stings.
There are certain fictions we engage in different strategies for solving problems as well, and there's always a little Socratic method in the middle where we pretend like we haven't seen the solution at the end, and go through a farce of arriving at it step by step. That may reduce the importance of sudden epiphanies, but it does not remove it.
Even without TDD, that's kind-of how I go: I'll have the top-down general structure in mind, but not in the code, and write the actual code bottom-up. That way I encounter problems with the implementation details sooner, and can more readily adjust the overall structure without feeling bogged down by what's already written.
In the broader structure though it's more like a through-line: One nice input/output, written in that bottom-up-with-top-down-in-mind way, all the way through a use case, then expand sideways with the other inputs/outputs built on the same lower-level functionality.
Structured Design, which is a quasi top down method, works better in real life. Its main issue is that it didn't survive the OOP hype.
I've seen plenty of code in "OO" languages that is just the sort of top-down imperative function type thing being described in the article. That is, people using OO solely to encapsulate state and variable scope, using methods more like functions, versus trying to model the real world as objects. I suppose though, that's not "OOD" then?
Bertrand Meyer's book Object-Oriented Software Construction is just about as good as it gets for defining and justifying OOP. Still worth a read today. That said, functional programming and hexagonal architecture are probably more useful.
Often the UI addresses only a subset of this space -- it is only the visible part of the iceberg as it were! There is a lot going on under the UI surface. Not to mention you may want more than one (kind of) UI.
For an example see https://en.wikipedia.org/wiki/Vienna_Development_Method#Bank...
The idea is to model the key aspects as mathematically rigorously as possible while ignoring lower level details. But even if you are not as rigorous, the process of creating such a model can help clarify things. As an example, if you are developing a file system you can abstractly define operations on it (read, write, create file, dir, etc.) and one can come up with a huge number of implementations satisfying this but as you add other requirements or constraints, quite a few choices are no longer meaningful. For example you want to implement a FS that takes advantage of NVMe SSDs, they put certain constraints. So then you need a model for an SSD (erase blocks are multiples of data blocks, write must be preceded by erase, etc. etc.). The idea is to model all this using a few abstract data types and state. You need not strive to programmatically derive an implementation from such a spec for it to be useful.
It's something Functional Programming either lacks or doesn't do as smoothly. The intermediate results and state are highly useful for debugging, and Functional doesn't "like" intermediate state. Yes, it can be emulated, but the emulation is rarely good as the real thing. For one, the emulated state lacks useful variable and token names, because it's machine-generated.
s/foldr/scanl
Only sort of kidding. Can you give aconcrete example of some task you'd like intermediate state around?
Also, do you know about the validate monad?
x = foo01();
y = foo02(x);
z = foo03(x, y);
Contrast with: foo03(foo01(), foo02(foo01()) );
x, y, and z were named by humans, giving them domain meaning, whereas a debugger would give the second one no name or machine-made names for intermediate state. (Here, z, y, and z have no meaning because it's a foo-bar example, but in practice they'd have better names.)The first is also often easier to read than the second, at least to my eyes. I know devs who can read the second quickly, but I don't think it's the norm. The LINQ "dot style" can be a little easier to read, but has similar problems.
As would foo0x.
I don't think many functional programmers would object to writing code as you did in your first example if you think it improves readability and if you don't change what the names refer to.
But your overall point is fair. A function, once composed, curried or having captured data, is very much like an object. Indeed its representation in memory is likely very similar to an object (mostly functional languages) or actually identical (Java). Your debugger won’t show the data in such a function but it’s happy to show the data in an object. That’s a big advantage of objects over functions. Hopefully debuggers will get better at this eventually.
x = foo01();
y = foo02(x);
z = foo03(x, y);
could easily be written in Haskell as: let x = foo01
y = foo02 x
z = foo03 x y
in z
My thinking is that functional languages don't preclude you from having intermediate state, but I will agree that composition is used often.Typically when working in a repl I don't really miss the intermediate state because my debugging consists of:
The lambdas must flow. λ> foo01 "expected result" λ> foo02 "UNexpected result"
Then I'll debug foo02 in isolation.
> foo03(foo01(), foo02(foo01()) );
I think I'd write this as the let example above with variables. Otherwise I guess I'd need to use something like:
foo03 foo01 . foo02 $ foo01
Here's a Haskell playground link with these examples if you're curious or had something else in mind and want to modify the example: https://play.haskell.org/saved/MeRGyCjr x = foo01();
foo03(x, foo02(x));
In my head I tend to see code as a directed graph, and this one most closely matches that graph without creating too big a jumble on one line. The first one that also has y and z has additional nodes and edges, making the graph more complicated than it needs to be.Your second one would have the simplest graph, except on one line like that it obscures that foo01() is called twice. If that second one was actually foo04(), then I'd prefer this as the simplest form (which I'd actually originally written before noticing foo01() was in there twice), because it has the simplest directed graph of all, and the code matches it very closely:
foo03(
foo01(),
foo02(foo04())
);
If the variables and function names were long enough, then I'd use that last format even with my first example, to avoid the whole "jumble of letters and numbers" while still keeping the directed graph simple: the_big_x = the_big_foo01()
zimzamfoo03(
the_big_x,
bang_bar02(the_big_x)
);Interactive systems like CL and Forth (as it's commonly implemented) give you that framework for free, it's already written. So why not take advantage of it?
EDIT: oh I see, it's basically the same concept as effect and feature sketches, but at the design stage rather than at the "dealing with existing code" stage. The intent is to figure upstream and downstream dependencies in your design in a structured manner. Cool.