41
Back to Basics: SOLID Principles (Liskov Substitution)
The five object-oriented programming principles a.k.a. SOLID principles establish practices that lend to developing software with considerations for maintaining and extending as the project grows.
In this post, we'll be going over the third SOLID principle.
The formal definition of the principle says:
Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
Frankly speaking, this just went over my head. So let's try to simplify this.
A sub-class must be substitutable for its superclass. This principle aims to ascertain that a sub-class can assume the place of its superclass without breaking the program.
If LSP is not followed, a new subclass might necessitate changes to any client of the base class or interface. If LSP is followed, clients can remain unaware of changes to the class hierarchy. The client should behave the same regardless of the subtype instance that it is given.
The LSP, therefore, helps to enforce both the open-closed principle and the single responsibility principle.
Let's take an example to see this in action:
class Vehicle:
def __init__(self, name):
self.name = name
def engine(self):
pass
def start_engine(self):
pass
class Car(Vehicle):
def engine(self):
return True
def start_engine(self):
self.engine()
class Bicycle(Vehicle):
def engine(self):
raise Exception("not implemented")
def start_engine(self):
raise Exception("not implemented")
def start_pedaling(self):
return True
vehicles = [
Car('queen'),
Bicycle('booster')
]
def drive_vehicle(vehicles):
for vehicle in vehicles:
if isinstance(vehicle, Car):
vehicle.start_engine()
elif isinstance(vehicle, Bicycle):
vehicle.start_pedaling()
In the above example, the Car and Bicycle implement the Vehicle class but they violate the Liskov Substitution Principle in two places.
- The Bicycle class doesn't support start_engine and engine methods. It will throw an exception if we try to call those methods. Due to this, we have our second violation.
- In the drive_vehicle method we are checking the type of the class instance and accordingly calling the relevant methods to drive the vehicle.
These are direct violations of LSP, as we should be able to change the type of class that implements the same superclass without the client being aware of this change.
Solution?
For the first violation, we will divide our vehicle class into two classes. VehicleWithEngine
will implement the Engine class with its relevant methods and the Car and other vehicles with an engine will implement this class.
class Engine:
def engine(self):
pass
def start_engine(self):
pass
class VehicleWithEngine(Engine):
def __init__(self, name):
self.name = name
def engine(self):
pass
def start_engine(self):
pass
class Car(VehicleWithEngine):
def engine(self):
""" define the engine """
return True
def start_engine(self):
""" implement the start up process for the engine"""
self.engine()
class Bike(VehicleWithEngine):
def engine(self):
""" define the engine """
return True
def start_engine(self):
""" implement rest of the steps to start an engine"""
self.engine()
For the second violation, we will implement another class VehicleWithoutEngine
which will not implement the Engine class and will have its methods required for driving without an engine.
class VehicleWithoutEngine:
def __init__(self, name):
self.name = name
def start_pedaling(self):
pass
class Bicycle(VehicleWithoutEngine):
def start_pedaling(self):
"""implement the pedaling process"""
return True
Now our drive_vehicle function will be changed to:
def drive_vehicle():
car = Car('Queen')
car.start_engine()
bicycle = Bicycle('Booster')
bicycle.start_pedaling()
Car and Bicycle both might be vehicles but both have different ways of working.
This post is Part 3 of the series, Back to Basics, each covering one SOLID principle.
References:
41