When it comes to adding complexity to the code, I generally see two schools of programming:
- Top-down: start with abstractions, then write implementations for it.
- Bottom-up: write down the whole implementation, then make abstractions out of it.
This post advicates the second school.
Sequential code is your friend
Let’s start with apologies to an old friend, nowadays somehow despited, from our early days of programming – sequential code.
- well-known third parties
- calls to well-known and well-encapsulated parts of your app
If there’s any uncertainty about behavior of that third party, don’t go for it.
If there’s any uncertainty about other part of the app, don’t call it.
This “technique” was used by everyone when learning how to program computers. And it has some advantages!
Sequential code trumps abstractions (on a small scale)
Compare these codes. These are two equivalent, arguably typical Rails approaches of a very simple view.
# cars.yml en: cars: index: make_label: "Make" model_label: "Model" id_label: "VIN"
# site.yml en: site: title: "Bartek's Cars"
# cars/index.haml %head %title= t(‘site.title') %body %table %tbody %tr %td= t(‘cars.index.id_label') %td= t(‘cars.index.make_label') %td= t(‘cars.index.model_label') - @cars.each do |car| %tr %td= car.id %td= car.make %td= car.model
# cars/index.haml %head %title Bartek's Cars %body %table %tbody %tr %td= Make %td= Model %td= VIN - @cars.each do |car| %tr %td= car.id %td= car.make %td= car.model
The first solution consists of three files. Other than the view itself, also locale.
The second solution is just one file. Even then, it contains less code than in its counterpart (!).
Because of no functions, classes, modules, and other patterns, sequential code produces the least code possible.
Among the two codes presented above:
- Which one would you prefer? (if you never had to grow the view)
- Which one do you think a new person in the project would prefer?
- Which one do you think a person who scans the file to quickly comprehend what’s going one would prefer?
For me, the second one wins hands down in each competition.
On this scale jumping from one file to another is nuts.
Everything in one place feels right. This might be caused by the fact time passes as the lines go. Reading top to bottom and left to right is what humans are used to. We expect “the next thing” to appear in this order.
On the other hand, when the program consists of multiple classes, functions etc., the flow jumps from one place to another. To comprehend what’s really going on, one has to comprehend each piece separately and memorize the whole call stack. That’s an overhead, maybe not the biggest one, but it accumulates.
Less chances for speculative generality
Once you wrote all the code that you need, you plainly see which parts need to be abstracted away, and which not.
When you introduce abstractions without knowing all the details, you might easily overlook and overestimate the complexity of the problem, and prepare for scenarios that can’t possibly happen.
In other words, starting with abstractions exposes you to Speculative Generality code smell. In my opinion, it’s one of the most dangerous
Less chances for wrong abstractions
Starting with abstractions forces you to guess how the implementation will look like. This may cause wrong abstractions in your code at the very beginning.
code doesn’t scale Sequential
We shouldn’t stick to sequential code forever because in the long run, maintaining a sequential-only code is a nightmare. What was a bliss (no call stack) on a small scale, on a large scale becomes a curse (you can’t easily abstract some portions of the code
That’s when paradigms (OOP, FP), clean code, and other rules come into play.
Haw can we levarage the benefits of sequential coding and at the same time not fall into its pitfals? By managing complexity bottom-up!
Step 1: Start at the bottom
Just start with showing what’s going on with the code, from head to toe. That means very flat code, possibly no abstractions, with room for abstractions later.
Remember, don’t speculate!
- use only those third parties that you’re certain about
- don’t rely on other code in the codebase – it could be wrongly abstracted
Also, remember that I only ask for starting this way.
Step 2: Move complexity up
Only where you are done with the sequential solution, clean up the code (if you find it cluttered).
Cleaning up means usually introducing some pattern, class, function, whatever feels right in your case. But every cleaning comes down to one generality: hoist the complexity to higher layers.
Here is a big deal: you probably won’t abstract as much code as you would do, if you started with abstractions. Some code will remain at the bottom, and this is a huge win of
Which code should be moved up and when? – you may ask.
Very personal topic. I believe the answer is – when maintaining this sequential mess is not worth it.
For me, there are two symptoms of when to get rid of sequential code:
- repeated code
- long procedure
I don’t really see any other reasons why to get rid of sequential (remember: better from abstractions in many ways!) code.
How does it relate to other principles?
Pragmatic Programmer made a great point. Agile replaced the need for speculative generality.
Programmers had to be ready for unexpected. The more, the better.
But this is no longer true in agile reality. Agile means short feedback loops with our clients. Nowadays, we build software meeting specific requirements and validate the ideas quickly. We as programmers should aim for tailor-made (sequential) solutions because they can likely be rolled back in next iteration.
If you are in position to deliver quickly less general code (you are, if you work in agile environment), you definitely shouldn’t start with abstractions.
Every TDD cycle (Red -> Green -> Refactor) is in line with managing complexity bottom-up.
- Red. You start with writing a failing test, a new functionality. (complexity extends)
- Green. Then you make the test pass in the simplest possible way. Copy & paste from Stack Overflow is more than welcome. We don’t engineer it yet. (simplest, sequential solution)
- Refactor. You are free to refactor the code now, add the craziest patterns and remove the tiniest duplications, because you have the safety net in form of tests. (move complexity higher)
I would define TDD as “managing complexity bottom-up step by step”.
This is my favorite rule of software development. I hate “just in case code”.
Abstractions and generalizations go together. The less abstractions, the less generalization, hopefully less unnecessary code.
If you start with abstractions, you automatically generalize your code. This might be expensive, if you don’t know all the details.
Don’t start with abstractions. Start simple, possibly sequential, but don’t stay simple when it becomes a pain.
I believe nobody’s ever shared with me this thought in such a concise form. When I realized this simple rule, I had to write this post.
What do you think of it? Don’t hesitate to share your opinion!