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.

Liskov Substitution 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.

  1. 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.
  2. 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