Towards robust software, with SOLID principle 3/5 explained clearly!
This post is part 3 of a series on the SOLID principles.
You can find the second post here.
Before we delve into the reasoning of this principle and how it fits in with the last two principles, let’s look at the definition.
The Liskov substitution principle is formally defined as:
Let ϕ(x) be a property provable about objects x of type T. Then ϕ(y) should be true for objects y of type S where S is a subtype of T. Source: Barbara Liskov
Let’s face it, this makes no sense to the modern software engineer.
Here’s a simpler definition:
It should be possible to replace an instance of a superclass with an instance of a subclass without causing breaking changes.
Say you have a subclass which derives from a base class/superclass (inheritance). Let’s say you make some changes to the subclass such as change of parameter value types and perhaps also the return type. In this case you would have violated the LSP principle because replacing an instance of the superclass with an instance of the subclass would lead to breaking changes. The code that depends on the base class would expect a return type of
str for example but when you replace the instance with the subclass, it returns an
int . This would could cause the code to crash or raise other errors.
In short, the behavior of the base class must conform to the behavior of the superclass. In general, the function signature (function) and return type must be unchanged in the subclass.
The main purpose of this principle is to ensure that old codebases do not break when new code is introduced. Furthermore, it ensures flexible code.
Let’s look at a simple example. We change the scenario slightly from previous posts by considering a car rather than a car dealership:
Car class defines a car. We initialize it with a name, the number of gears, the speed and the current gear position. The
changeGear function allows changing of gears while the
accelerate Function increases the speed of the car by 1. If the gear is in the neutral position
N we don’t change the speed but inform the user instead.
Let’s say we want to model a
SportsCar also. We make it inherit from the base class
Car. At initialization we also define a
turbos variable which is a list showing what turbo levels the car supports. Because of the behavior change we need to define a new acceleration function that takes
turbo as a parameter. The
speed is increased by the turbo amount instead of
1 so the sports car can accelerate faster.
__main__ function we define a normal car.
Here’s the problem:
If we try to replace the
Car instance this with an instance of a
SportsCar it causes the code to break since
SportsCar expects a
By LSP, replacing the above should not give an error and the code should function as normal.
There are two ways to go about this.
A simple solution would be to replace the
turbos variable with a
turbo variable having a fixed value. And removing the parameter
turbo from the
accelerate function like so:
As you can see, the function signatures are now directly identical to the base class function signatures. This means we can replace the
Car code in the
__main__ function with
SportsCar without code breakage. Yay!
However, if you notice we have a fixed turbo value
2 in this case.
What if we want the user to define the turbo value like it was possible before?
This requires using an abstract class. On we go!
The issue is the way we have modeled the cars. To make the code above conform to the LSP and have the functionality of turbo choice we must create an abstract base class
Car. Then we create two separate subclasses that inherit from it namely:
RegularCar. The diagram illustrates this:
It’ll make more sense with the code:
Car to an abstract class using the
ABC module. The details of this are not important. The
init function is annoated with
@abstractmethod to denote an abstract function. This ensures that a
Car cannot be instantiated. Only classes that inherit from this can be instantiated.
Next, we create two classes that inherit from
RegularCardefining a normal car without turbo
SportsCara sports car with turbo
RegularCar has no additional changes.
SportsCar class we can make the modifications we desire. We define the
turbos array and the
turboAccelerate function containing the extra
In this case, changing the function signature in
SportsCar is not violating the LSP. Why? Because the base class
Car is abstract so it cannot be instantiated.
If it cannot be instantiated, it cannot be replaced.
In this post we looked at the Liskov substitution principle and what it means.
We saw an example violating the principle and solve it using two methods.
LSP helps make code flexible and prevents issues when old code is extended with new code. LSP ensures that behavior of code remains the same across both new and old codebases, resulting in code that is less prone to breakage.
Overall, code robustness increases as a result.
This post was a bit more involved than previous posts especially since we approached the problem in a roundabout way. I wanted to explore an interesting scenario that increased complexity slightly. I hope you enjoyed the content!