What is Abstraction?
Abstraction is one of the core concepts in computer science and, as the name suggests, not an easy thing to handle. Thanks to proper abstraction layers that have already been implemented into our daily life, we don't have to think about how our email is delivered to the recipient. We only write the email, click 'send', and we don't care about anything else because it happens on other levels of abstraction.
In terms of programming, we can say that abstraction is a technique of arranging the complexity of code by defining separate levels of system functionality. Abstractions are specific parts of code responsible for different things. Ideally, abstractions make the code easier to extend, read, understand and maintain.
How Abstraction Works
Here's a very simple example. Let's say we've got a Car class. In order to make Car work, we at least need its body, e.g. Sedan or Cabriolet. So we have a Car base class here and two subclasses: Sedan and Cabriolet. Car is a proper abstraction class here, as there's no point of instantiating it directly (it wouldn't even work), and both Sedan and Cabriolet are perfect Car object examples. Do you want to add a Pickup body? No problem, it will still work fine. Problems may occur when you add a Bus subclass to Car, on the grounds that buses are quite similar to cars. In such a situation, you should consider creating a Vehicle abstract class and Truck and Car as separate subclasses of Vehicle.
How to Use Abstraction Correctly
Abstractions can make or break productivity. There is no silver bullet for defining abstractions that will fit your app perfectly for years, but if you stick to the two ground rules, you'll at least minimise the likelihood of implementing a wrong abstraction in your code base.
You have to bear in mind that no abstraction is actually better than the wrong abstraction. Using proper abstractions in your app helps to maintain it over time, but if you're doing it wrong, you're adding unnecessary complexity to the project, which can be hard to understand in the future. Of course, a slightly missed abstraction is still better than 100000 LOC file full of spaghetti code, but consider the following scenario based on the car example above.
We have a Car class, which is a perfect abstraction for its Cabriolet and Sedan subclasses. Everything works fine, and we’re happy because our code is clean.
A few months later our application requirements change, and we have to handle Trucks in our app. As we’re already dealing with some really big cars using our Car parent class, we decide that a Truck could also be considered as a Car, because this class already implements a lot of logic that we’ll be using with Trucks as well. The current abstraction is almost perfect, and everything works fine.
Another few months pass and a new developer joins the project with a task to implement Buses in our app. S/he sees that we have an abstraction in the Car parent class, which seems to have worked fine with Trucks too. So s/he decides that a Bus could also be considered a Car, just like Trucks. S/he alters the code by adding some extra parameters and conditionals here and there. It still works.
Time passes, requirements change, another developer continues to use the Car abstraction, adding extra parameters and logic that does things depending on the value of the parameters. Our abstraction now behaves differently for various cases and is not universal anymore. The trouble started in step 2.
After a few loops of step 4, our code becomes totally incomprehensible.
As you can imagine, extracting and sticking to the wrong abstraction makes your code unnecessarily complicated sooner or later. It would have been much better to extract an abstract class Vehicle in step 2, and then Car, Truck, Bus, Motorbike, etc. as its subclasses. Or not to extract the Car abstract class at all.
The other, more practical advice, is that abstract classes should never be directly instantiated. Instantiating abstract classes means that something is wrong with your code and the abstraction is poorly implemented. Imagine how an instance of a Vehicle class mentioned above could behave? What could it do for you in your system? It should never be instantiated as an abstract class. Vehicle only provides some shared logic for its children.
Thanks to these two simple rules, your code base will become more logical and give structure to a consistent system.