Abstraction. A word, I’m sure, you encountered already many times, and maybe used yourself as well.
It’s one of the most important concept in software development and in computer science in general. Everybody praises its virtues and its power. Authors wrote about it, birds sing about it, its name is whispered all around the kingdoms. Abstraction is attractive, sexy, old and modern at the same time. The Alpha and The Omega.
However, do you know what the concept of abstraction really implies? Do you know what the benefits and the costs using them?
To be honest, for many, many years, I thought I knew what was an abstraction. When I had to explain to a less experienced developer what it was, I couldn’t easily give a precise and simple definition. This was a prove that I didn’t really know what it was.
You know, I couldn’t do it. I couldn’t reduce it to the freshman level. That means we really don’t understand it.
The concept of abstraction is so important, so fundamental, you will find it everywhere: procedural programming, functional programming, object oriented programming (OOP), even in real life!
Today, I would like to go with you on a deep dive in the world of abstractions. We will:
- Define what an abstraction is, illustrated with examples.
- Cover what are the different sorts of abstraction you can find in software development.
- Analyzing the (obvious) benefits of using abstractions, but as well their drawbacks.
- Understand the difference between abstraction and indirection, two concepts often linked but not interchangeable.
The abstractions are waiting for us.
The General Concept of Abstraction
What’s an Abstraction, Exactly?
Let’s begin by defining what we are talking about.
In our day to day life, as a non-technical human being speaking to other non-technical human beings, we mostly know abstract
as an adjective: abstract art, abstract ideas, or abstract concepts. We think of it as something very loosely related to reality (or not at all), and it often conveys the sense of something difficult to understand
What about the noun, an abstraction? Let’s open the Holy Dictionary to show us the way:
The quality of dealing with ideas rather than events.
We come back to this concept of reality versus idea. An abstraction is therefore not an event, something real, but something less anchored to reality, belonging in the realm of ideas.
Let’s continue our quest of knowledge, riding our keyboard to go hearing the predictions of the oracle Google™! The previous definition was a bit vague, so let’s take another one, from another dictionary:
An abstraction is a general idea rather than one relating to a particular object, person, or situation.
We are here introduced with the idea of generalization (general idea
). We are confirmed as well that an abstraction is abstract (crazy! I know!) rather than something real (object, person or situation
).
We progress! The light will come soon answering our thirst of knowledge! You know what? Let’s be crazy. Let’s look at the etymology of the word abstraction. It comes from the Latin abstractus, which looks like a good word to use for a spell in a video game. It means: “to draw away”. In other word, to hide.
Well, well, well. We have now gathered the three main properties of an abstraction:
- The concept of idea versus reality.
- The concept of generalization.
- The concept of hiding, or removing.
But wait! Are we software developers? Should we look at the definition in some fancy technical textbook, instead of using what the lower people used two centuries ago? Let’s do that:
Abstraction is the purposeful suppression, or hiding, of some details of a process or artifact, in order to bring out more clearly other aspects, details, or structure.
This is from a book from the Oregon State University (the cover of this book is fantastic) and the best definition I found after quite a lot of research.
What do we have here? Again this idea of hiding or removing details, in order to bring out more clearly other aspects, details, or structure
. Basically, if you have something with many details you don’t need, creating an abstraction might be a good idea, instead of using the real object. In that case, an abstraction is a representation of something.
In short, an abstraction will simplify a process or artifact
abstracted, by providing what you really need, and hiding the useless details you don’t care.
By the way, speaking about simplicity in software development, I wrote quite a lot about it here.
There’s still an interesting paradox we didn’t solve: we saw that the word abstract, used as an adjective, is often considered as something hard to understand, which is definitely not a consequence of simplification. When we look at abstract art for example, artists can present to us something which could feel pretty complicated, full of shapes and form we don’t really understand.
Often, abstract art is based on something existing, but so many details have been removed that we don’t recognize the subject anymore. It could be over-simplified, and now we’re wondering what the heck this artist was trying to represent.
That’s a very interesting idea. Let’s keep that in mind, we will address it later in the context of software development.
Finally, don’t think about abstraction as a black and white concept: an abstraction can be more or less abstract. When an abstraction is very close to the reality it represents, we say that an abstraction is concrete.
For example, a coffee machine with one single button is more simplified, more abstract than a washing machine which has a more complex interface.
Abstraction is not about vagueness, it is about being precise at a new semantic level.
We will come back extensively to the washing machine. How great is that?
That’s Nice and All, But Gimme Some Examples!
Wonderful Washing Machines
Alright. Let’s take some real life example of what an abstraction could be. It will help us to make some interesting parallel with the abstractions we find in software development. Analogies for the win!
Look intensely to your washing machine. Don’t be shy. I know you always wanted to.
What do you see?
A bunch of buttons and knobs in order to wash whatever you want to wash. Depending on your needs, if you want to wash white delicate clothes or robust jeans, you will set a different washing program thanks to the washing machine interface.
Now, in order to finally wash your clothes, do you need to know how the washing machine works internally? Do you need to know that drums, water valves, thermostat and springs will work beautifully in order to wash your clothes?
Thankfully no! The button “start” abstracts, draw away, hide every detail of the real mechanism:
- Starting the washing machine is just an idea. The reality is what happens inside the washing machine itself.
- It’s simpler to push a button than to set up manually when the valves should open, close, what should do the drums and so on.
- Even if some washing machines work a bit differently (to save power for example), you will always have the famous “start” button. It’s a generalization across the different type and brand of washing machines.
Marvelous! We come back to the three properties of an abstraction, as we defined it above.
The Map is Not the Territory
I have another interesting example in my example bag: a map.
Can you guess how the three properties of an abstraction can apply to a geographical map?
Here’s the answer:
- A map will simplify the reality: instead of having every rock, flower and whatnot represented, you will only have the essential information needed.
- A map is an abstract idea of a portion of land. A pure representation, which can differ from map to map.
- Maps have some general conventions, some ways to represent the same things. The topography will be lines with numbers, to represent the elevation, for example.
There is a famous sentence coined by Alfred Korzybski which is as well the title of this part:
The map is not the territory
It means that abstractions are not the objects they abstract. It means that the river you can see on a map is very different from a real river. It sounds obvious, but it’s important not to forget, since an abstraction won’t necessarily looks like it.
Your Application is an Abstraction of an Abstraction of an Abstraction
Abstraction can be layered, that is you can abstract an abstraction. After all, an abstraction could need some simplification, generalization, and idealization, depending on the context.
Let’s take a random software. What does this software abstract? Does this abstraction abstract something else? We could come up with this list of abstraction layers:
- User interface
- High-level language (PHP, Java, JavaScript)
- Low-level language (C)
- Machine language
- Architecture (registers, memory, arithmetic unit…)
- Circuit elements (logic gates)
- Transistors
- Solid-state physics
- Quantum mechanics
This is from the excellent Berkeley’s CS61a course (2010). Keep in mind that it is a simplification (an abstraction?), in reality there are many, many more layers.
The highest (concrete) level of abstraction is the first layer 1
. It’s the layer which is the closer to the business reality. The user will use it to interact with your software and do whatever you want him to do he wants.
Your high level code (using a high level language) will translate the business requirements you had. Again, it’s pretty close to the business reality, closer than lower abstraction levels.
The high level language can be considered as an interface to act on the other abstraction levels, like you would use the interface of a washing machine. This idea of interface is also called abstraction barrier
: it’s a border between two layers.
More and more you will go down, more and more you will go further away from the reality of the business domain. You will find yourself in more esoteric places like memory management and hardware specifics. Quantum mechanics is even beyond what we experience daily.
Do the user of your software care about the technologies it uses? Does he care about its architecture? Its storage system? Does he care about transistors?
Nop. Not at all. What’s important for the customer is that the UI (User Interface) give him the power to solve his problems and answer his needs.
Everything else is nicely abstracted.
Type of Abstractions in Programming
Now that we defined what an abstraction is and what are its main three properties, let see what are the abstractions available to us in our programming languages.
But first, a little quiz: what’s the essence of programming?
- A complete mess nobody grasps most of the time.
- Data and control flow.
- Exotic naming conventions and whether we should use space instead of tabs.
You’re right! The good answer is 2.
- Data is the information stored somehow on a computer.
- Behaviors process the data and possibly transform it, overtime. This is called control flow.
That’s why we can speak about two types of abstraction: data abstraction and control flow abstraction.
Data Abstraction
While doing some research to write this article, I saw a bit everywhere that data abstraction was exclusive to OOP paradigm. Well, this is simply wrong.
Data abstraction is available in many languages which are not considered OOP, such as C or even Lisp. A primitive data type (a data type made available directly by your programming language) is already an abstraction. For example, the data type string
is, to us, a collection of characters, but in memory the value of your variable will be a bunch bytes.
What’s the point of data abstraction?
First, It tells the compiler (or the interpreter, depending on the language) what to do with the data itself. If it’s a string, mathematics operations on it should be forbidden, or handled by changing implicitly the data type (as it is the case for higher programming language).
Second, when you read your code, the variable’s data type will tell you:
- What’s the type of the variable’s value.
- What’s the possible behaviors of the variable you can use.
There is a difference between the low level abstraction offered by primitive types in a language (like string), and the data types you can create in modern programming languages, often using objects.
These data types are called Abstract Data Type (ADT). The concept was defined by Barbara Liskov, who was searching a way to isolate behaviors, basically inventing encapsulation. Her paper was very influential for the whole development world.
At the end, data abstraction is meant to:
- Simplifying by hiding the complex memory management (for some language) and behavioral mechanisms.
- Creating new concepts, like the concept of “character” in a
string
, or whatever you want with ADT. - Providing general behaviors you can reuse everywhere.
Since data is so important in programming, data abstraction is very important as well. You will see them all the time!
Control Abstraction
Nowadays, we are likely to work with structured programming languages, which allow us to use different structures to create behaviors.
In most programming language, subroutines will be your control abstraction of choice. They are:
- Functions.
- Procedures (it’s like a function, except that it doesn’t return anything).
Since we don’t distinguish procedures and functions in modern programming language, let’s call these subroutines simply functions.
Here’s why a function is an abstraction:
- The name of a function simplify and hide its internal mechanism. After all, when you call a function, you don’t really care about its implementation. You only care about its name and its arguments, which should give you the details you need.
- You need to be aware that the name of a function is not the reality. It can describe what’s inside, but it can be sometimes ambiguous, difficult to understand or simply wrong.
- A function generalize a behavior: it can be reused anywhere, hopefully in a small defined scope.
If you need to look at the implementation detail of a function to use it, it’s not well named or designed. Don’t hesitate to rename it or to refactor it.
Basically, you can abstract everything and anything in your code using simple functions.
Abstractions in OOP
We covered till now the different abstractions you can use in many programming language, whether they are considered object oriented or not. What are the abstractions you can use only in OOP?
This part is not meant to explain the OOP paradigm in depth. Let me know if you want me to cover that in another article.
Classes
A class is an abstraction which gather data (called properties or attributes) and behaviors acting on its data (called methods).
Let’s take an example:
<?php
class Parser
{
private $filepath;
public function __construct(string $filepath)
{
if (file_exists($filepath)) {
$this->filepath = $filepath;
} else {
throw new Exception(printf("the file %s doesn't exist!", $filepath));
}
}
public function parse(): string
{
$content = file_get_contents($this->filepath);
return $content;
}
}
$parser = new Parser("test.txt");
echo $parser->parse();
In this small script you can see the class Parse
used, by instantiating it and trying to parse a file on the last two lines. If you only look at these last two lines (which will be often the case, since each classs should have its own file), you don’t know what happens when the object is created and what the Parser::parse
method will do. You only hope that it will parse your file, but you don’t care how.
In a nutshell:
- The class
Parse
simplifies the operations on its data, by hiding them. It’s called encapsulation in the OOP world. For example, the verification that the file exists is hidden. - What is available outside of the class (the
Parse::parse()
method) is only a representation of the behavior of the same class. The nameparse
describe what the method is doing, but it doesn’t describe that the PHP functionfile_get_contents()
is called, for example. This is a useless detail. - The class
Parse
try to generalize the concept of parsing a file. It could be used each time we need to parse a file.
Abstract Classes
In some language, like PHP, Java or C#, you can create abstract classes. It’s basically a template many other class can inherit from, in order to generalize some behaviors.
Let’s take our example to another production-ready-class-of-the-year level:
<?php
abstract class Parser {
public function verifyFileExtension(string $filepath, string $expected): bool
{
$ex = explode(".", $filepath);
$extension = end($ex);
return $extension == $expected;
}
}
class YamlParser extends Parser
{
private $filepath;
public function __construct(string $filepath)
{
if (file_exists($filepath) && $this->verifyFileExtension($filepath, "yaml")) {
$this->filepath = $filepath;
} else {
throw new Exception(printf("the file %s doesn't exist or is not valid!", $filepath));
}
}
public function parse(): string
{
$content = file_get_contents($this->filepath);
return $content;
}
}
$parser = new YamlParser("test.yaml");
echo $parser->parse();
- We have now an abstract class which generalize some validation (
Parser::verifyFileExtension
). It can be used by any class which inherit the abstract class, like theYamlParser
class. - The
YamlParser
class is simpler, the abstract class draw away the details we might not care, that is the implementation ofParser::verifyFileExtension()
. - The abstract class
Parser
represent the general idea of a parser, which could be inherited by more concrete class, theYamlParser
, or even aXmlParser
or aJsonParser
class.
At the end, generalization is the main purpose of an abstract class. If you need to simplify by hiding details, using a normal class is enough. Heck, even if you need to generalize some concept through a bunch of classes, you can use composition as well without any abstract class.
As we will see below, you need to be very careful when you begin to use abstract classes in order to generalize behaviors: it creates indirection and incidentally nodes in your brain.
Interface Construct
What do I mean by interface construct? This kind of thing:
<?php
interface Parser
{
public function parse(): string;
}
The word interface
is one of the most ambiguous word we use in software development. It can mean different things, depending on the context. That’s why, when I speak about the language keyword interface
, as you can find it in many high level languages, I will always use the expression interface construct.
Nothing official here, it’s just the way I found to differentiate the general concept of interface we saw above, and the interface
keyword.
Interface constructs can:
- Hide implementation details.
- Generalize a concept: it can be implemented by an unlimited amount of classes.
- Create an abstract idea: the idea of a
Parser
in the example above.
There are two main differences between an interface construct and an abstract class:
- The interface construct doesn’t use inheritance and allow you to follow another important concept in OOP, polymorphism.
- Methods of an abstract class can be implemented directly in the abstract class. Methods of an interface construct can’t be implemented directly in the interface construct.
You might wonder: why do we need interface constructs if we already have classes, and even abstract classes? Well, the main purpose of interfaces is not abstraction, but the possibility to swap implementation. We will come back to that below.
For now, keep in mind that interface constructs aren’t magical: it’s not because you will use them that your application will be incredibly abstracted and tremendously scalable.
Is Everything That Great in the Magical World of Abstractions?
The literature has tendency to sell us the benefit of abstractions without necessarily speaking about their pitfalls.
Now that we saw what’s an abstraction, its properties and the tools we have to create them in our code, let see why they can be useful and how they can harm your codebase.
Simplifying using Abstractions
The Benefits
We only have a limited amount of mental energy, and it’s very difficult for us, simple humans, to store and think about a lot of information and implementation details at once. Hiding details you don’t care is the abstraction’s massive benefit.
We already spoke about it: giving a descriptive name to a set of command is very convenient.
The benefits of abstraction for simplicity are essential. We would all go crazy if we would need to manage everything at the hardware level, while trying to express complex business requirements. You can try to write some assembly and make it portable across a whole range of hardware to be convinced.
The Pitfalls
You thought the abstraction world was made of rivers of honey, roads of chocolate and clouds of marshmallow? Everything has a drawback, and, even if it looks weird, simplicity as well.
The Naming Problem
You see, hiding the details you don’t need is very good, but you still need to precise roughly what’s your function, class or interface try to abstract. Naming comes into play, and as Phil Karlton was saying:
There are only two hard things in Computer Science: cache invalidation and naming things.
That’s why, sometimes, things won’t be properly named. The obvious scenario would be a function which do much more than it claims: I remember having a deleteUser
function which was deleting users… and other unrelated things. A wonderful surprise.
An application with the right semantic for its abstractions will raise its overall quality higher.
Over Simplification
Lose of Control
Let say that you design a magical parser able to parse anything and everything: from JSON to XML to markdown to this curious format made of tabs, space and emojis.
You want it to be very simple to use. Everybody will like it on Github! Companies will fight to have you, they will pay you millions, it will be finally glory and fortune.
Only a single method is available on your Parser
class, to keep things extremely simple. With no argument. Parser::parse()
.
Obviously, the user needs to put the file he wants to parse in a precise path on his hard disk he can’t choose. The output will be the screen, something he can’t choose either.
Then come the release date: you are waiting for the glory you deserve, by posting your amazing side project on Reddit, Facebook, Linkedin, Discord, ICQ, MSN Messager, and of course AIM.
Unfortunately, the only thing you get is complaints. Nobody understands how it works: “Where should I put my file? In what format the output will be?”
People want to configure it. They want to solve their specific problems. Simplicity is hurting your idea. The interface is too simple.
An abstraction should draw away details, that’s true, but it should brings out other details as well. The important, useful details.
Let’s come back to out washing machine example: who would like to have a washing machine with only one single button, and nothing else?
Well, I’m sure there would be some people buying the idea, me included, but I have friends (incredible, I know) who cares a bit more than I do about clothes. They want the ability to choose their washing program and the temperature depending on the material they wash, and other obscures things lost in the spiral of complexity.
The morale of the story? There are two types of complexity:
- Necessary complexity
- Unnecessary complexity
You can’t get rid of the first if you want your software to succeed, the second is useless and harmful. Keep that in mind when you try to abstract everything and anything.
Washing Away Too Many Details
Over simplification can be harmless when you simplify things so much you don’t know what aspect of the business domain it serves.
You’re an e-commerce and you have this Box
class? What is it for? Is it a shipment box? A product box? The checkout box, where you put all your products?
I worked with domain objects so abstracted it was difficult to know for what they were used at the first place. My advice: stay as close as possible to the business problems you solve, close to the reality of your features, to the real life. Make your abstraction concrete, at least at first, and name them accordingly.
This picture, from computersciencewiki, illustrates what I mean: the first heart has too many (disgusting) details, the last one doesn’t have enough. Who would understand that it’s a heart?
Leaky Abstractions
We saw above that an abstraction remove or hide details we shouldn’t care of. Many define abstraction as such, in the wild world of The Internet. The harsh reality which will come back, one day, in your wonderful face, is that abstractions only hide things, never remove them.
What’s the difference?
I love my washing machine example, so let’s come back to it, again. One day, you’ll go to your bathroom / cellar / kitchen and find some water on the ground. A swimming pool growing and growing. There might be already a couple of water lily and some innocent frogs, if you come back from holidays.
The washing machine leaked. Its convenient interface was hiding how it was really working, but even if these details were hidden, they came back to haunt you. The unknown mechanism, the details unseen, broke. Since you have no clue how the damn thing is working and how it could break, you have no clue what happened.
What can you do? Opening the toolbox your mother-in-law offered you for Christmas and destroying the Washing Machine From Hell© even more, trying to make sense of all the complexity formerly abstracted?
If you’re a bit more reasonable, you could as well take your phone and call somebody who knows how it works, who knows the underlying abstracted mechanism and how to fix it.
To come back to the magic world of software development, you can’t really call a repairer when an abstraction below yours creates nasty bugs. You are the builder and the repairer. Therefore, when an abstraction is leaking, you might dive in it and try to fix the problems.
Let’s look all together what are the possible abstractions which can leak, and the difficulty to solve them. The first point is the easiest to solve, the last point the hardest, and between a whole spectrum of headaches:
- You wrote the abstraction. Fixing it might not be a big problem. Your abstract class
Parser
deleted half the database! That’s fine, you wrongly named theBulkCreate()
function: it should have beenBulkDelete()
. Oopsy Doopsy! - Your colleague wrote the abstraction. You can dive into it and, if you don’t understand anything, you can directly ask him. Easy peasy, lemon squizzy! However, if your colleague left the company moons ago, you need to make sens of his gibberish. That’s still fine, you get away with painkillers and a good deployment on a Friday evening. Lucky you!
- The abstraction is from another external library. Things get a bit more eerie:
- You need to make sens of it and find the bug. Nobody to help you if nobody worked with it before!
- You need to fix the bug.
- You need to submit a pull request, or ask nicely to the library’s author to fix it.
- They might fix it in three months. Your app desperately needs the fix? You could do it yourself by forking the library, forgetting about it and wondering why you have security issues.
- The leaking abstraction is a bit lower. For example your programming language has a few bugs. You need to find it, contact the maintainers and wait. No way to fork a whole programming language.
- The leaking abstraction is even lower! On the hardware level for example. You’re screwed. You need to change the hardware itself and hope for the best. If nobody had the same problem, it’s basically playing Russian roulette.
- There is a bug in quantum physics. You might end up in esoteric articles for specialized newspapers! Congratulations!
Scary, isn’t it? This list is not only ordered by difficulty but by likelihood as well: it’s very likely you create some bugs in your own application, and it’s very unlikely that a logic gate begins to behave weirdly. There are many layers in between, which, normally, protect you.
All of that to say that you need to know what’s going on in the closest layer of abstraction you work with. At least. They’re all interconnected to each other, knowing how they work might even give you an edge many developers don’t have, to fix problems and even having a sharp understanding how to find better and more performant solutions.
It comes back to understand the first principle of your craft, as I explain here.
The details of the abstraction might not appear in your code, your user interface or your app, but never forget they are still there: they can give you poor performances, make your ideas impossible to realize, bringing you a lot of complexity, or simply break.
The pain point is often external libraries: you don’t control them, they are not as well tested as a programming language or the layers of abstraction below, and they might contain a lot of rubbish. Make sure you wrap them using interfaces you control (using a wrapper for example), and only use what you need in your app. I might come back to it in another article, if somebody wants me to write about that.
Generalizing with Abstractions
The Benefits
Like simplicity, it would be very difficult for us to code anything if abstractions weren’t there to generalize concepts and behaviors.
Every function and structures available as part of a programming language are generalizations. They define a concept you can use everywhere, possibly in every application, even if they have nothing related to each other business wise. You need to use a loop? You can use the for
construct, which generalize the concept of loops.
It sounds obvious enough, so let’s go back to the dark side.
The Pitfalls
Up Front Generalizations
Most of the time, the problem arises when generalization are made in the layer of abstraction you create. After all, programming language are well tested and more or less broadly used. Often, they are supported by a community which can spot defects and performance problems.
When you build an application, it’s a different story.
Your code is more related to the reality of the business. If you’re programming an e-commerce platform, you will have the concepts of products, orders and shipments for example.
A company needs to adapt to its market and, therefore, possibly change very quickly its tactics and strategies. You need to understand the business well in order to translate its concepts in your code and make them scalable. When you need to generalize them, your understanding have to be even greater.
Indeed, modifying generalizations can be dangerous. If your abstractions are used everywhere, you need to be sure that anything using them won’t break, because of your changes.
That’s why you should not generalize up front. When you code something, a piece of behavior which might be used somewhere else in the future, so as you (or anybody else) think, don’t abstract it right away. Doing so would be only a wild assumption, a guess, and guessing is not what you should do.
Wait and see if this same behavior is used somewhere else. Wait to have a greater understanding of the business concept it’s derived from.
If the same knowledge, the same behavior is used at multiple places, you can begin to think about abstracting it to generalize the concept. If you’re not sure that the behavior is a good generalization which will hold for everything using it, just copy-past the code for now. It’s not that bad.
“Not that bad?!” you might yell at your computer, ready to write a comment full of anger and profanities. “What about the DRY principle, you horrible and dirty monkey?!”
Glad you speak about it! I covered the DRY principle in another article. You might want to read it, it’s one of the most misunderstood principle in software development.
Many, many things are contextual in our field. You need to decide what to do depending on the context you’re in. However, I never saw a generalization used only at one place being useful. Can we even speak about generalization in that case?
If you really need to use abstraction as a mean of generalization, use simple abstraction constructs, before going into the holy OOP design patterns. Functions are very powerful, for example.
If the repeated behavior include data and get more and more complicated, create a class. First, in order to simplify and draw away the complexity, and second to make it available to the other classes which need it. Please, don’t make it globally available.
Generalization can as well makes the purpose of your code very blurry. I saw systems where everything was so generalized it was difficult to know what business knowledge it tried to codify. As Dijkstra was saying:
The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise.
Abstract Classes and Inheritance
This section begins with a magical story.
It’s the story with me, a developer, an e-commerce company and a lot of labels you can put on shipments. You know, the stuff with a name, an address and other information about the recipient.
At the beginning, we were shipping with the carrier DPD in multiple countries. Creating a label meant sending some data to DPD’s API and get back the data we needed to print the label, mostly data about formatting.
Since the process to create a label was the same for Germany, Austria and a couple of other countries, I thought it would be good to abstract everything not to repeat my code, in an abstract class.
It worked well, till Italy came into the party. If you always wanted to know what is the worst carrier in Europe, I can tell you that Bartolini is. Did you ever worked with an API exchanging monstrous files with wrong (or missing) data, using spaces as separators? Joyful.
Anyway, my abstract class I created earlier was not useful at all for this carrier. I had to modify it, creating some creepy conditionals, like if ($carrier == "bartolini"){ //do many stuff }
. This was the beginning of the end.
The company was growing very fast and more and more carriers was coming cluttering my abstraction. It was a mess, horrible to maintain, with a cyclomatic complexity going through the roof.
The moral of the story? I should have created my abstract class after having a couple of carriers implemented. I would have not guessed anymore what was common between their implementation, I would have known it.
Duplication is far cheaper than the wrong abstraction.
Moreover, creating an abstract class with many classes inheriting from it (one class per country) was hard coupling everything together.
I could have implemented some class representing the carriers, use them in some of my country label class using composition, and I would have never created bloated abstract class painful to scale.
Hopefully, you can learn from my mistakes!
Abstraction and Indirection
Since we are all here, comfy on hour favorite chair, enjoying a tea, a coffee or a bottle of a 30 years old whisky, let’s talk about a very unfortunate misconception in software development: the confusion between abstraction and indirection.
You see, I took some time to define abstraction at the beginning of this article partly because many developers out there define it wrongly.
These folks will tell you that an abstraction allows you to replace part of your implementation with something else, thanks to the interface construct or the abstract classes.
This is not the definition of abstraction. This is a property of an indirection.
What’s an indirection, you might rightfully wonder? From the Oxford dictionary:
Indirectness or lack of straightforwardness in action, speech, or progression.
In computing, it means calling something using one (or multiple) middle man. To understand that, let’s go back to our example full of attractive and cute parsers:
<?php
interface Parser
{
public function parse(): string;
public function verifyFileExtension(string $expected): bool;
public function verifyFileExists(): bool;
}
class YamlParser implements Parser
{
private $filepath;
public function __construct(string $filepath)
{
$this->filepath = $filepath;
if (!$this->verifyFileExists() || !$this->verifyFileExtension("yaml")) {
throw new Exception(printf("the file %s doesn't exist or is not valid!", $this->filepath));
}
}
public function parse(): string
{
$content = file_get_contents($this->filepath);
return $content;
}
public function verifyFileExtension(string $expected): bool
{
$ex = explode(".", $this->filepath);
$extension = end($ex);
return $extension == $expected;
}
public function verifyFileExists(): bool {
return file_exists($this->filepath);
}
}
class Exporter {
private $parser;
public function __construct(Parser $parser)
{
$this->parser = $parser;
}
public function export(): string {
return $this->parser->parse();
}
}
$parser = new YamlParser("test.yaml");
$exporter = new Exporter($parser);
echo $exporter->export();
If you look at the constructor Exporter::__construct
, you can see that it needs a parser Parser
as argument. Parser
is an interface construct, so the method Exporter::export
call parse on an interface construct. At runtime, YamlParser
will be used, but indirectly.
The interface is the middle man here.
Is this interface an abstraction as well? Well, it hides the implementation details of the concrete class (YamlParser
), so it looks like it’s an abstraction.
However, each class implementing Parser
must implement every single of its method, which is basically the totality of the behavior of the class YamlParser
. There is a 1:1 relationship between the interface construct and the class which implements it.
It’s a bit like you would have buttons for each operation a washing machine have to do: you would need to push one to open (or close) the valves, another one to move the drum, another one to pour the washing liquid on your clothes.
Can we still call that a simplification? Not really, if we need to implement everything. What about generalization? Well, it will be difficult as well, again because there are many methods to implement. Every class will need to answer too many constraints.
At the end, this interface construct is more about indirection than abstraction. Indirection can bring flexibility: we can always swap the YamlParser
used in the Exporter
by something else, like JsonParser
.
The problem: it brings complexity as well, as I pointed out in another article. Levels of indirection can be difficult to comprehend at once when you need to do some change in your codebase. Our brain is not good to reason on many layers of indirection.
Here’s the key points of the relationship between abstraction and indirection:
- Some abstractions create indirections, like an abstract class or an interface construct.
- Creating an indirection doesn’t mean you create a (useful) abstraction.
- Indirections can make your software more flexible to changes.
- Be sure that you need this flexibility! If you intend to swap some implementation, be sure that you have more than one implementation in your codebase available right now. Not in the future.
Seeing Through The Abstraction Layers
The world is a very complex place. We abstract away many things, all the time: what we see and feel is abstracted by our senses and our brain. We don’t consciously think of every detail about everything, or we would drive totally crazy, trying to make sens of too much information.
Our mental power is limited.
However, abstractions can fool us, believing that we know things, based on our experience, even if the details of these experiences are washed away for our own good. It’s the same with any kind of abstraction, real or from the computing world: knowing how an abstraction works doesn’t mean that we know how it works behind the abstraction.
The map is not the territory.
To summarize, here’s what we learnt in this article:
- An abstraction is meant to be a representation of something more complex, in order to simplify it and / or to generalize it.
- There are two categories of abstraction in computing (not only in OOP): data abstraction and control abstraction.
- Specifically in OOP, there are constructs you can use depending on your programming language: classes, abstract classes and interface constructs are very commons.
- Abstractions have tremendous benefits but pitfalls as well: it’s important to find good names for them, and hiding details doesn’t that you need to hide everything.
- An abstraction can leak: as developers, we must have some knowledge about what’s behind the abstraction, the underlying complexity the abstraction try to draw away.
- Generalizing “for the future” only bring complexity. You should generalizing using abstractions when you presently need it.
- The word interface is one of the most ambiguous term in development. When somebody speaks about interfaces, it’s not necessarily about the
interface
keyword. - Interface constructs and abstract classes are powerful and ambiguous: they can be used as abstractions but often are mainly indirections.
My experience showed me that a deep knowledge about the different layers of abstraction in computing is the best tool you can have to develop robust solutions and answer the most complex problems. Don’t try to learn everything behind every abstraction at once, but stay curious how things work internally.