As I was browsing through Twitter the other day, I saw someone complaining about abstractions among the lines of “we add abstraction layers on top of more abstraction layers and expect the system to be easy to maintain.” I understand this feeling because I’ve also felt that way before. However, I think there seems to be a lot of hate around “abstractions.” They are somehow seen as “layers” or “more boilerplate”, but abstractions are actually about simplifications.
It’s not rare to hear that Object-Oriented Programming (OOP) is a way to write systems that model the “real world.” But there is a catch in here. The real world is way too complex to be modeled in a computer program. Instead, we should focus on building simplifications of the real world, or simulations, if you prefer. We shouldn’t handle every single edge-case that might exist in the real world. Instead, we should avoid adding more if statements or logic paths to our programs. Also, the real world is full of hard rules like gravity and the speed of light which our software simulations are not bound to.
Alan Kay — the person who coined the term “OOP” — has talked about it several times, but I like this article from 1977 published by Scientific American:
Although the hardware of the computer is subject to natural laws (electrons can move through the circuits only in certain physically defined ways), the range of simulations the computer can perform is bounded only by the limits of human imagination. In a computer, spacecraft can be made to travel faster than the speed of light, time to travel in reverse.
Abstractions are simplifications. You take a complex problem and wrap it in an abstraction that turns all that work into a few lines of code. Usually, these abstractions come in the shape of libraries/packages, but not necessarily external ones.
For instance, recently in a project, we were transcribing audio files. Instead of writing the code that talks to the AWS SDK where we need the transcription to happen, we introduced a thin abstraction layer that handles the transcription process for us. We have even written it in a way that we can swap the transcription service to use something like GCP Speech to Text or some other service (in fact, we are going to implement a GCP driver soon and will have both implementations running at the same time using the same abstraction). No external or private packages needed, just one interface and its own namespace inside the app.
Good abstractions simplify the userland code. It doesn’t mean the complexity is gone. It means that the complexity was pushed off the main stage into a thin and simple layer (a package or an interface). It’s still there but should be contained and can be managed almost independently of the userland code. The code is not more complicated because of this, because complexity has to live somewhere.
Sometimes, though, in order to improve the abstraction, you have to break the contract that was made with your userland code. That’s okay. When you are writing a package, that’s what SemVer is for. Also, your userland code should be relatively easy to change (since the complexity was pushed away from it) so that adapting to the new contract should also be relatively easy.
We should have a few, good abstractions in place. These should simplify the code. If it’s not simplifying, don’t blame “abstractions” in general. And when building abstractions, don’t let architecture astronauts scare you. I think this somehow matches DHH’s blog post and keynote on “Conceptual Compression”, but they take it a bit further there.