In my opinion, LSP is the simplest and easiest principle to be used. It expresses only one thing: A superclass object should be replaceable by its subclass object without affecting the correctness of the programme. Whereas polymorphism refers to the same behaviour having multiple different presentations. It seems that polymorphism and LSP are in natural conflict.
But in fact, it’s not. LSP does not conflict with polymorphism; rather, LSP guides us in implementing polymorphism safely. It ensures that while using polymorphism, the interchangeability of superclass and subclass objects does not lead to unexpected behaviors.
Polymorphism is a programming paradigm. It tells you what you can do.
LSP is a design principle. It tells you what you should do.
Introduction to LSP
What is the Liskov Substitution Principle?
LSP states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program. In simpler terms, if class B is a subtype of class A, we should be able to replace instances of A with instances of B without disrupting the behavior of our program.
Why is LSP Important?
Inheritance, while one of the pillars of object-oriented programming, is invasive. This is because subclasses are forced to inherit all properties and methods of the superclass, even if they don’t need them. This means that subclasses will be forced to have properties and methods they don’t need, and methods in the superclass will be modified by the subclass.
The main function of LSP is to restrict the invasiveness of inheritance.
The term “invasive” in the context of inheritance in OOP refers to a situation where a superclass imposes its structure and behaviour on its subclasses. Invasiveness is the main drawback of inheritance. Here are a few reasons why inheritance is considered invasive:
- Limited Flexibility: Subclasses are forced to inherit all properties and methods of the superclass, even if they don’t need them. This can lead to a lack of flexibility and can create unnecessary dependencies.
- Exposing Internal Details: The superclass often exposes its internal details to subclasses, which goes against the principle of encapsulation. This can make the system more complex and harder to understand.
- Tight Coupling: Inheritance creates a very tight coupling between the superclass and its subclasses. Changes in the superclass can have ripple effects on all subclasses, which can lead to fragile code that is hard to maintain.
- Difficulty in Refactoring: Refactoring a hierarchy of classes that use inheritance can be difficult and risky. Since subclasses are so tightly coupled to their superclasses, changes in one class can unintentionally affect others.
- Inappropriate Abstractions: Sometimes, inheritance is used to share code rather than to create a proper
is-a
relationship. This can lead to inappropriate abstractions and design that does not properly reflect the intended relationships between objects.As a result, many modern programming practices and design patterns recommend using composition and interface-based design to replace inheritance, to reduce tight coupling and to improve code maintainability and flexibility.
More about: Why do We Need to Use More Composition and Less Inheritance?
How to Apply LSP in Practice?
Example of Violating LSP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Bird {
public void fly() { /* ... */ }
public void layEggs() { /* ... */ }
}
public class Pigeon extends Bird {
// Pigeon can fly and layEggs
}
public class Ostrich extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Ostriches can't fly"); // Ostrich can't fly
}
}
Here, Ostrich
overrides the non-abstract methods fly()
of Bird
. Substituting Bird
with Ostrich
changes the behaviour of fly()
, violating LSP.
Besides this, due to the drawbacks and invasiveness of inheritance, Ostrich
is forced to have behaviour it should not have: fly()
.
Correcting LSP Violation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface Flying {
void fly();
}
public class Bird {
public void layEggs() { /* ... */ }
}
public class Pigeon extends Bird implements Flying {
@Override
public void fly() {
// FlyingBird specific fly implementation
}
}
public class Ostrich extends Bird {
// Ostrich can't fly
}
In this corrected design, fly()
is placed as an abstract method in an interface
for flying birds to implement. All the non-abstract methods of Bird
have not been overridden. So, the behaviour of Bird
will not be changed by its child class. This design adheres to LSP.
Avoid overriding non-abstract methods
- In my opinion, applying LSP in practice is very simple and only one thing needs to be observed:
- Avoid overriding non-abstract methods
This is because as long as the non-abstract methods are not overridden, the subclass will not change the behaviour of the superclass.
- In fact, even without considering LSPs, overriding non-abstract methods is still strange behaviour and should not be recommended. Because:
- A non-abstract method in a superclass can be seen as a convention, which represents itself and its child classes should have such behaviour. Overriding a non-abstract method is a violation of the convention.
- For example,
fly()
is implemented in Bird to represent that all birds are flyable. But Ostrich can’t fly, so it must override this method. - This is usually confusing: since it is stated in
Bird
that a bird must be able to fly, are ostriches birds or not? This inheritance does not seem strong.
- For example,
- If a method of a class may be overridden, it means that the method is not a common feature. Overriding a non-abstract method indicates that the method shouldn’t be implemented in the superclass.
- For example,
Ostrich
is a subclass ofBird
suggesting that fly is not a common feature of birds. - In this case, why implement
fly()
inBird
? That’s not a reasonable convention.
- For example,
- A non-abstract method in a superclass can be seen as a convention, which represents itself and its child classes should have such behaviour. Overriding a non-abstract method is a violation of the convention.
Subclasses should extend, rather than modify, the functionality of the superclasses.