Writing good object-oriented software is not an easy task. Fortunately for us, we’re not the first to do it, and a bunch of programmers before us built a few principles and techniques we can rely on to create robust, easy-to-read, object-oriented code.
Among those principles are the SOLID principles.
In this short tutorial, we’re going to focus on one of those principles: the Liskov Substitution Principle.
2. Definition of Liskov Principle
So, what is the Liskov Substitution principle? As introduced, it’s part of the SOLID principles, a set of rules to follow in order to produce good object-oriented software. First, we’ll talk a bit about those and then move on to the Liskov Substitution principle.
2.1. The SOLID Principles
These are some principles explained and promoted by Robert C. Martin, or Uncle Bob, a famous actor of the software development world:
- Single Responsibility principle
- Open-Closed principle
- Liskov Substitution principle
- Interface Segregation principle
- Dependency Inversion principle
Following these principles should lead us toward producing software that is more readable and easier to maintain and evolve.
2.2. The Liskov Substitution Principle
Among them, of course, is the Liskov Substitution principle. It states that “subclasses should be substitutable for their base classes“, meaning that code expecting a certain class to be used should work if passed any of this class’ subclasses.
Let’s imagine a Vehicle class, extended by some Car and Truck classes:
Then, let’s say we have a Garage class that repairs all kind of vehicles:
The act of reparation is implemented by the Garage#repair method, which takes a Vehicle argument. That means, whether we give a Car or a Truck to this method, it must work in order to respect the Liskov Substitution principle.
Now, we can perfectly imagine designing a class accepting Vehicle, but throwing some kind of error when a noncompliant Vehicle is passed. For example, let’s say we have a CarDriver class with a drive() method, and this method takes a Vehicle argument though car drivers don’t drive trucks:
Then, as clients of such a class, we expect to be able to give any kind of Vehicle to the drive method, but passing on a Truck will result in a failure. That is an example of a class that doesn’t respect the Liskov Substitution principle.
This has consequences, which we’re going to see in the next section. But first, let’s see how we could’ve enforced the principle here.
Obviously, a car driver shouldn’t be driving a truck. So, instead of putting the relationship between CarDriver and Vehicle, we should keep it on the specific class level, between CarDriver and Car:
3. Consequences of Breaking the Principle
What would the consequences of breaking the Liskov Substitution principle be?
3.1. Misleading Code
First of all, this would bring us to misleading code. Effectively, we should expect some behavior to work, but it doesn’t. If we’re lucky, this is documented, and we read the documentation early enough to adapt our code. If not, then we gain that knowledge when the code runs… and fails! That leads us to the next two consequences.
3.2. Less Readable Code
Let’s say we were lucky enough to learn that some feature we want to use doesn’t work with all subclasses. So, we’ve to deal with it. In the best scenario, we can just avoid using the non-fitting subclasses. In the worst case, however, we must find a way to handle those classes.
Let’s take our example and say we’re not the owners of the classes and can’t change them, then we must add some conditional logic to separate Car and Truck code paths. Where we had a fluid set of instructions, we now have multiple possible branches and are forced to manage subclasses. Isn’t one of the goals of a class hierarchy to avoid doing that?
3.3. Error-Prone Code
Now, let’s face it, we’re not always that lucky. Sometimes, we just naively use some code, thinking it will work no matter the subclasses we pass on to it, and then our code fails where it should’ve worked! We might see that in the testing phases. As well as we might not see that until production use, and that’s a bigger issue.
In this article, we’ve learned about the Liskov Substitution principle, one of the SOLID principles. We saw what its purpose was and how breaking that principle could impact our code.