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.
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:
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.
In this case:
-
The
Sparrowis aFlyingBird, and it can fly. -
The
Ostrichis simply aBirdand 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():
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:
Why this works with Liskov Substitution:
-
The
RectangleandCircleboth honor the principle by providing their own specific implementation of thearea()method. -
They can be substituted anywhere a
Shapeis 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.