The Palos Publishing Company

Follow Us On The X Platform @PalosPublishing
Categories We Write About

Understanding Liskov Substitution with Real Examples

The Liskov Substitution Principle (Liskov Principle) is one of the five SOLID principles of object-oriented design. It states that objects of a superclass should be replaceable with objects of a subclass without affecting the functionality of the program. In simpler terms, if a class B is a subclass of class A, you should be able to use an instance of class B wherever you expect an instance of class A, without introducing errors or unexpected behavior.

Key Idea

  • A subclass should be able to stand in for its superclass without causing any unexpected behavior.

  • The derived class (subclass) must be compatible with the behavior of the base class (superclass).

Real-World Example: Birds and Flying Birds

Imagine you have a base class called Bird. This class has a method fly(), which defines how a bird flies.

python
class Bird: def fly(self): print("Flying") class Sparrow(Bird): def fly(self): print("Flying like a sparrow") class Ostrich(Bird): def fly(self): print("Ostriches can't fly!")

Here, the Sparrow and Ostrich are subclasses of Bird, but Ostrich violates the Liskov Substitution Principle because it cannot fly. The purpose of Bird was to have the ability to fly(), but the Ostrich breaks this by introducing behavior (i.e., an inability to fly).

Why does this violate Liskov Substitution?

In a program that works with Bird objects, you expect any object that inherits Bird to be able to fly. But if you substitute a Bird with an Ostrich, the behavior changes unexpectedly, violating the principle.

Refactoring the Example for Liskov Substitution

To respect Liskov Substitution, we could redesign the class hierarchy. Instead of having a single fly() method, we can separate the concept of flying birds from non-flying birds:

python
class Bird: def eat(self): print("Eating food") class FlyingBird(Bird): def fly(self): print("Flying") class Sparrow(FlyingBird): def fly(self): print("Flying like a sparrow") class Ostrich(Bird): def run(self): print("Running like an ostrich")

Now, FlyingBird inherits from Bird and adds the fly() method. The Ostrich is no longer forced to implement fly() and instead can implement behavior specific to non-flying birds (e.g., run()).

Liskov Compliant Use Case

Now, we can replace instances of FlyingBird with Sparrow in our code, and replace Bird with Ostrich without violating expectations.

python
def animal_activity(bird: Bird): bird.eat() if isinstance(bird, FlyingBird): bird.fly() # Using instances sparrow = Sparrow() ostrich = Ostrich() animal_activity(sparrow) # Expected: Eats and flies animal_activity(ostrich) # Expected: Eats and runs

In this case:

  • The Sparrow is a FlyingBird, and it can fly.

  • The Ostrich is simply a Bird and cannot fly, but it can run.

Both classes can be substituted for Bird and behave correctly within their specific functionalities.

A More Abstract Example: Shape and Areas

Consider a scenario where we have a general class called Shape, which has a method area():

python
class Shape: def area(self): pass class Rectangle(Shape): def __init__(self, width, height): self.width = width self.height = height def area(self): return self.width * self.height class Circle(Shape): def __init__(self, radius): self.radius = radius def area(self): return 3.14159 * (self.radius ** 2)

Both Rectangle and Circle are subclasses of Shape, and both implement the area() method. Here, if we use either of them in a program expecting a Shape, everything works as expected:

python
def print_area(shape: Shape): print("Area:", shape.area()) rectangle = Rectangle(10, 5) circle = Circle(7) print_area(rectangle) # Expected: Area: 50 print_area(circle) # Expected: Area: 153.93804

Why this works with Liskov Substitution:

  • The Rectangle and Circle both honor the principle by providing their own specific implementation of the area() method.

  • They can be substituted anywhere a Shape is expected without causing unexpected behavior or breaking the program logic.

Summary of the Liskov Substitution Principle

  • Correct Hierarchy: Ensure that subclasses don’t violate expectations set by the base class.

  • Behavior Compatibility: A subclass should behave in such a way that it can be substituted for its superclass without causing errors or unexpected results.

  • Encapsulation of Varying Behavior: If different behaviors are required (like flying vs. running), it is better to split the behaviors into separate classes (e.g., FlyingBird, RunningBird), rather than forcing all subclasses to follow the same pattern.

By adhering to the Liskov Substitution Principle, the system becomes more maintainable, flexible, and easier to extend without introducing bugs or complexity.

Share this Page your favorite way: Click any app below to share.

Enter your email below to join The Palos Publishing Company Email List

We respect your email privacy

Categories We Write About