The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. In simpler terms, if a class B is a subclass of class A, then we should be able to use B wherever we use A without any issues.

Example of LSP Violation

Consider the following classes:

class Bird:
    def fly(self):
        return "Flying"
 
class Sparrow(Bird):
    pass
 
class Ostrich(Bird):  # Ostrich cannot fly
    def fly(self):
        raise Exception("Ostriches can't fly!")

In this example, Sparrow is a true subtype of Bird because it can fly. However, Ostrich violates the LSP because it cannot fly, even though it is a subclass of Bird. If we replace a Bird object with an Ostrich object in a program expecting all birds to fly, it will lead to an error.

Correcting the Violation

To adhere to LSP, we can refactor the classes:

class Bird:
    def make_sound(self):
        return "Some sound"
 
class FlyingBird(Bird):
    def fly(self):
        return "Flying"
 
class Sparrow(FlyingBird):
    def make_sound(self):
        return "Chirp"
 
class Ostrich(Bird):
    def make_sound(self):
        return "Boom"

Now, Sparrow and Ostrich are both valid subtypes of Bird, and we can use them interchangeably without violating the LSP.

2. Avoiding “Inheritance for Code Reuse Only”

Inheritance should represent a logical “is-a” relationship rather than being used solely for code reuse. If a subclass does not logically represent a type of the superclass, it can lead to confusion and maintenance issues.

Example of Misuse of Inheritance

class Vehicle:
    def start_engine(self):
        return "Engine started"
 
class Car(Vehicle):
    def start_engine(self):
        return "Car engine started"
 
class Bicycle:  # Not a Vehicle
    def start_engine(self):
        return "Bicycle doesn't have an engine"

In this example, Bicycle is not a type of Vehicle, yet it is trying to implement a method that belongs to Vehicle. This creates confusion because a bicycle does not logically fit into the vehicle hierarchy.

Correcting the Misuse

Instead of using inheritance, we can use composition or interfaces to represent the relationship more accurately:

class Vehicle:
    def start_engine(self):
        return "Engine started"
 
class Car(Vehicle):
    def start_engine(self):
        return "Car engine started"
 
class Bicycle:
    def ride(self):
        return "Riding the bicycle"

In this corrected version, Bicycle is no longer a subclass of Vehicle, which clarifies the relationship and avoids confusion. Each class has its own responsibilities and behaviors without misrepresenting their relationships.

Conclusion

In summary, when designing classes with single inheritance:

  • Ensure that subclasses adhere to the Liskov Substitution Principle, meaning they can be used interchangeably with their parent class without causing issues.
  • Avoid using inheritance merely for code reuse; instead, focus on logical “is-a” relationships to maintain clarity and correctness in your class hierarchy.