You join a new team and after a brief onboarding you are given your first task. It’s a simple one - you need to display an additional input field on a page.
After some digging you find out that most pages use the same generic component to display forms.
“That’s easy” - you say - “I can just add another parameter to it”
Then you open the file and you see that the four engineers before you had done the exact same thing. Multiple times. At this point you take a deep breath and realise that the next week is going to be interesting.
Abstraction is a technique which is used to hide the implementation details of something behind a single idea. It allows us to use complex functionality through a simple interface.
When we find that we are writing similar code, we usually look for a way to create an abstraction. To move the duplicated logic in a parent class, module or a function.
So when is an abstraction wrong?
Some abstractions can be misleading. Their interfaces may not communicate well enough what they do. But I would consider this a naming or a documentation problem.
A truly wrong abstraction is created when we fail to solve a code duplication problem. Code that looks the same may not always change the same way. So seemingly identical logic may actually serve different purposes in the future.
Putting ideas that evolve differently under the same abstraction is what makes it wrong.
We as engineers have learned how to use abstraction to solve duplication problems. However this doesn’t mean that every such problem requires an abstraction.
We design one based on the current state of the code but that may be misleading. Instead of focusing on the now, we should be thinking about the future state of the duplicated code.
We may remove the duplication for the time being. But what will happen if the code that uses the abstraction needs to change differently in the future? We would have only replaced one problem with another. Instead of handling repetition we now need to take care of a faulty abstraction.
We can’t predict the future but we can observe how logic evolves with time. If we rush to extract the duplications we risk putting together code that changes differently. When we try to unify different ideas our abstractions become too… abstract.
An abstraction that hides too many details in the end hides nothing. It becomes too generic. It will depend on many parameters to guide the logical flow and its details will bubble up to the surface.
The implementation will be a mess of boolean flags, conditional statements and logic that branches off in different directions. Extending and modifying such code becomes taxing. The easiest way to get something done is to add yet another condition and contribute to the mess.
At some point the benefits of the abstraction are diminished from all the extra work and complexity that comes with it.
How can we remedy this? We must avoid going into extremes. We can’t wait for duplication to become unmaintainable. But at the same time we don’t want to abstract everything the instant two code blocks start looking the same.
I find the Rule of 3 to be a sensible middle ground that I can agree with. It says that once you need to write the same code for the third time you should try to extract it.
This gives us some breathing room and allows us to see if an actual pattern emerges. Seeing the same code in multiple places helps us to form a better API. Knowing the subtle differences between the different usages helps us to parametrise the abstraction properly.
Of course, every rule has its exceptions. When you are faced with this decision you might decide to wait more to see how the API can evolve. In other cases you may create an abstraction without waiting for the code to be duplicated.
It all depends on the specifics of the problem, the technologies and the business domain.
Imagine that you’re in the shoes of the engineer from the example in the beginning. The easiest way to approach the problem would be to add an extra parameter to the common abstraction.
But if every component introduces its specifics then the abstraction would stop bringing value. It would make the knowledge required to work with it higher. It would reduce the amount of details that it hides.
If we find ourselves in a situation in which we want to add details to an API for a corner case then we need to reconsider.
We can’t predict the future so when we find that some code is changing differently from the rest we shouldn’t add its details to the common abstraction. It’s better to break out of it. Take our the necessary code and duplicate it with the additional specific logic. Maintaining that would be less risky than building on top of the existing abstraction.
That way the components that work the same still rely on a solid abstraction. And the specific case is left with an easy to maintain solution.
- A wrong abstraction is one which tries to unify ideas that change differently.
- We use abstractions to solve the problem of repetition. When we overdo it it becomes another burden that we need to take care of
- Wrongly designed abstractions don’t scale in the long term. They become too generic to the point of not hiding enough details.
- A solution for that is the Rule of 3 and not being afraid of breaking out of abstractions when necessary.