The single thing that made me a better software engineer is understanding the concept of abstraction. To illustrate it, imagine this conversation in the museum of abstract art.
A: “Why don’t you like the painting?”
B: “There are no details in it. Nothing specific.”
A: “It’s abstract.”
B: “But how can I know what it depicts?”
A: “That’s the beauty of it. You don’t have to know.”
When we make something more abstract we hide and remove details from it to make it simpler. It’s impossible to think of everything in granular detail. To consider every tiny element. That’s why we group complex operations or components under an umbrella term.
There are countless examples of abstractions in the real world. A car is a complex piece of machinery. It’s composed of many parts which themselves are made of smaller complex elements. When we think about a car we don’t consider the inner workings of the engine or the break system. We look at it as a whole.
Abstractions are a way to make complicated ideas easier to understand. To hide the details behind a single idea.
Abstraction is similarly used in both art and computer science. It is about the lack of detail. In programming we abstract away the specifics and complexity of something by putting it in a function or a class. In art we remove details until we are left only with a simple shape.
Abstractions in software engineering are meant to pass an idea through a simple interface. The API describes what a class does without exposing any details about it. An abstract painting is meant to pass a feeling without depicting a specific scene or item to the viewer.
We don’t know how exactly a function works but we know its arguments and the type of the response. It’s impossible to say what a piece of abstract art illustrates but it still evokes certain emotions in us.
There are two reasons to make something more abstract - to hide complexity or to make it more reusable.
When we have to write a series of complex operations it’s better to move them to a separate function. We give it a descriptive name and a useful doc block. If a piece of code has a well defined meaning it should be abstracted even if it’s short. That way 50 lines of code in the middle of the data flow become a single call to a
createOrder function, for example.
When we have to create multiple such functions that are related to a single part of the domain we can put them under a common abstraction. A common
OrderService is a good place to keep all the operations related to them.
But extracting functions and grouping them is more about structure. Abstraction is about hiding details and exposing ideas.
It’s not just about keeping related logic together. By putting them under the abstraction of a common service we have control over what it exposes. It can have public methods that resemble the high level operations to manage orders - create, update, cancel, refund.
But it will also host functions to validate data, authorize operations, send events and update storage. Those operations are not publicly available for the consumers of the class or module. They are used by the underlying implementation of the exposed functionality without its user knowing about them.
Creating abstraction is a powerful technique when we find similar logic in multiple places of our codebase.
If we write the same operation multiple times we can identify the common logic that is used and extract it to its own function. This function is then parametrized and exposed. When a set of classes are related and they serve similar purposes then the common logic between them can be abstracted in a parent class.
This allows us to limit the spread of complexity in the codebase.
The logic related to database operations is complicated and we shouldn’t repeat it in every class. It should be implemented in an abstract class which can then be extended. That’s how ORMs work. A model is created for each database entity and it only specifies the data and operations related to it.
Abstraction is a great tool for the purposes specified above but it is no silver bullet. Especially in terms of reusability it’s easy to overdo it.
Should similar code always have a common abstraction? Principles like DRY (Don’t Repeat Yourself) say so. The reasoning is that when a change needs to be made it can be done in a single place. But the fact that code looks identical at the moment doesn’t mean that it will necessarily remain the same.
When we are building abstractions we should be thinking whether these pieces of code will act and change in the same ways in the future. If they do then the abstraction can evolve with them. If they don’t then it’s better to keep them separated and manage the repetition.
It’s said that duplication is far cheaper than the wrong abstraction. Why is that? When code that relies on a common abstraction needs to change independently it becomes hard to manage. The abstraction will need to handle the specifics of each of its consumers. Each change will require another parameter or a conditional to be added.
With time the abstraction that was supposed to make things simpler will become a ball of yarn that’s impossible to modify or extend without deep knowledge of its workings.
If you are not sure about how the code will change in the future don’t do the abstraction. Duplication can always be managed. Untangling a common abstraction is a much harder process.
When we write complex data flows, conditional logic and error handling we can abstract those in functions. The functions that are related to specific parts of the domain can then be put into modules or classes.
Those classes can form entire applications and services. Those services can be composed together to form distributed systems.
The more you zoom out the higher the level of abstraction you’re working on. A function hides small specific implementation details. A microservice hides all the operations related to a part of the business domain.
When you’re composing a system you don’t take into account the class hierarchies but the whole area that a service covers.
Much like the car that we mentioned in the beginning, a software product is itself an abstraction. It represents an idea, a solution to a problem. A complex system that can be managed from a simple interface.
- Fully understanding the concept of abstraction is important for every engineer’s growth.
- An abstraction is a way to make complicated things easier to understand by hiding their details.
- There are many examples in the real world that illustrate this concept like cars and art.
- In software engineering abstractions are used to hide complexity and achieve reusability.
- We must be careful not to overdo this technique. Creating a wrong abstraction can be much more expensive than managing some code duplication.
- This concept can be explored on different levels - functions, modules, services, systems.