Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: March 18, 2024
At the beginning of our professional career as software developers, most of us program to implementations.
Later on, either intuitively or because of need, we slowly tend to change this mindset. We write more and more code to abstractions and interfaces.
In this tutorial, we’ll see what these terms mean and what pros and cons do they have.
To show the different techniques, we’ll walk through a few simple tasks. Remember the game series Big Car Stealing? We’re lucky enough to be the developers of the newest version.
The first task to solve is to make the character drive the cars. Our employer gave us absolute trust to handle the situation the best way we see.
For the first proof of concept, we’re focusing only on speeding up and slowing down.
Our first intuition is to create two classes: Player and Car:
The only thing we need to do is to invoke the drive() method of the Player class and pass a Car instance.
We implement everything, and it works like a charm. However, we get our next task. On top of driving a car, we must support driving a truck, as well.
No problem, we introduce the Truck class and the Player.drive(Truck) method:
So far, so good. But after that, we face the next task: driving boats. (Wait, what? Boats? We thought the game is about cars. Weird.)
We could repeat the same process we did with the truck. However, we start wondering: how many other things should we able to drive? We look at the backlog and see 31 different driveable things coming up (including bicycles, planes, submarines, jetpacks, and even space stations).
Therefore, we need a different approach. How could we do better?
Abstractions to the rescue! We decide that we’ll create an abstract Vehicle class, which will be the superclass of the Car, Truck, Boat, and all future classes.
Also, this way, we’ll need a single drive(Vehicle) method in the Player class:
The next task we get is to handle accidents in the game. During these accidents, the vehicles suffer damage. To support this, we introduce new methods:
However, since the method names break() and brake() are so similar, we confused them, which caused a frustrating debugging session. When we finally find the problem’s source, we propose a question. Can we somehow hide those methods we don’t need?
We get the idea to create multiple base classes for different scenarios: Driveable and Breakable. We took a class about the BDecreased programming language in the university, which allowed multiple inheritances. However, now we use the HotBrownStuff language, which doesn’t support that (for good reasons). How can we proceed then?
HotBrownStuff has a new concept over BDecreased: interfaces. A class can implement multiple interfaces, which makes it possible to solve the problem with the following class diagram:
We’re close to burnout because of the amount of refactoring we have to do every time a new feature comes. But the development must go on, so we get our next assignment: when we hit a building with a vehicle, it should suffer damage, too. We take a deep breath and get ready to rewrite half of the codebase again.
However, when we think about solving it, we find a straightforward implementation.
We make the building implement the Breakable interface:
To our surprise, everything is working fine without much effort. The birds are chirping again. The sun is shining. We even smile again. We can thank all of this for a better architectural design.
What was the difference between the techniques we used?
First, we directly used the implementing classes from other classes. Usually, we call this method “programming to classes” or “programming to implementations”.
It makes the code tightly coupled because there’ll be many dependencies between different classes. This makes the code fragile because when we modify one part of the code, it tends to break things in many unexpected places.
Next, we introduced an abstract class, which decoupled our classes’ clients from the concrete implementations. We call this technique “programming to abstraction”.
But we were still mixing different aspects of the functionality in the same abstraction.
Last, we introduced multiple abstractions: interfaces. We call this method “programming to interfaces”. Note that an interface is also an abstraction. Therefore, this method is the subset of programming to abstractions.
With interfaces, on top of decoupling implementations, we were able to decouple multiple concepts.
In a nutshell, when we’re programming to interfaces, the different business logic parts are not connected through implementations. They’re connected through interfaces.
Let’s start with the cons. We have to create much more types: interfaces, classes, sometimes abstract classes. It may be overwhelming at first, but we can manage this if we use a good folder/package structure.
Also, we’ll need an external component to instantiate the implementations. Preferably, it’s in the infrastructure and not in the business logic. We’ll take back to this topic in the section about design patterns.
But it’s a small price comparing to the pros. We’ll explain those benefits in detail in the following sections.
Think about the example with the car, truck, and boat. We used the same method names for all of those, but we could easily name the methods differently without a common ancestor. For example, accelerate(), speedUp(), and goFaster() are all valid candidates to name the same functionality. We could mix those in the different classes. For example, the car could accelerate, the truck speed up, and the boat goes faster.
With abstractions, we declare a contract between classes. The contract states what kind of operations the implementation will provide to the client. It doesn’t say a thing about how those operations are working, though – which is a good thing. This way, we can focus on what we want to do instead of how we can do that.
Only the interface is visible to other parts of the business logic. We should strive to keep these interfaces small, simple, and straight to the point to increase cohesion. In other words, with abstractions, we introduce boundaries between different parts of the application.
If we do this, we won’t accidentally leak implementation details, which tend to introduce tight coupling between different components. This would make refactoring and modification hard. Also, it’ll make the code harder to understand.
On top of that, since we don’t see (at first sight) the implementing classes, only the abstraction (therefore, the contract). Therefore, we can more easily comprehend the logic of the code. Again, we can focus on what the class does instead of how it does that. Also, we don’t have to keep in mind the names and responsibilities of dozens of classes. The abstraction hides all those details.
For example, the JDBC API defines many interfaces and a single class. The JDBC drivers implement those interfaces. However, we don’t use those classes from our application code. We only use the core JDBC types.
Louse coupling and fewer responsibilities make the code more testable.
Since we depend on interfaces, we can easily pass test doubles instead of concrete implementations. Also, since these interfaces are smaller and have well-defined responsibilities, providing mocks for those is straightforward.
We also saw that we could introduce new implementations without modifying the client code. It’s a powerful concept because we can extend the details in any way we please. If we have new requirements, we can get rid of the old implementation and replace it with a new one. For example, if we abstract the data access layer, we can switch from an SQL database to a graph database without changing the business logic.
JDBC also relies on this concept. If we decide to use a different database engine, the only thing we need to do is to replace the JDBC driver. The application code will stay the same because it’s independent of the implementing classes.
Programming to interfaces makes it easier to follow multiple SOLID principles.
Note that with only depending on interfaces, this last step is already inferred. We cannot instantiate a class without depending on it.
We already mentioned that we need some infrastructure to instantiate the implementations. Usually, we solve this problem using the Factory, Factory Method, Static Factory Method, Abstract Factory, and Builder patterns. Using Dependency Injection (preferably through a framework, like Spring, CDI, or Guice) makes this straightforward.
Of course, depending on the requirements, we may mix some of these patterns. It depends on the exact problem at hand.
The previous patterns make it easier to program interfaces. On the other side of the coin, some patterns rely on abstractions. Therefore, programming to interfaces will make it easier to use them. A few examples are Adapter, Composite, Decorator, Proxy, Mediator, Observer, State, Strategy, and Visitor.
Programming to interfaces looks tedious to beginners because of the higher number of interfaces and classes. Also, introducing many dependency hierarchies may be strange at first, too.
However, we’re following good OO practices and principles. Programming to interfaces will make our application loosely coupled, more extensible, more testable, more flexible, and easier to understand. It takes time and practice to master it, but it’s worth the effort.