You know, when I heard the name of the Liskov Substitution Principle for the first time, I thought it would be the most difficult one in SOLID principles. The principle’s name sounded very strange to me. I judged the book by its cover, and I convinced myself that I wouldn’t grasp it. Eventually, it turned out that it was one of the easiest and straight forward principles in SOLID principles.
So, let’s start our journey by putting a simple definition for the Liskov Substitution Principle:
It’s the ability to replace any object of a parent class with any object of one of its child classes without affecting the correctness of the program.
I know it sounds strange to you but let’s break it into pieces. Suppose we have a program that has a parent class. The parent class has some child classes who inherit from. If we decided to create some objects from the parent class in our program, we’ve to be able to replace any one of them with any object of any child class, and the program should work as expected without any errors.
In other words, we have to be able to substitute objects of a parent class with objects of child classes without causing the program to break. That’s why the principle has the keyword ‘substitution’ in its name. As for Liskov, it’s the name of the scientist Barbara Liskov who developed a scientific definition for that principle. You can read this article Liskov substitution principle on Wikipedia for more information about that definition.
Now, let’s try to link the definition we’ve just discussed to a famous example to understand the principle.
Bird
is a class which has the two methods eat()
and fly()
. It represents a base class that any type of bird can extend.
public class Bird {
public void eat() {
System.out.println("I can eat.");
}
public void fly() {
System.out.println("I can fly.");
}
}
Swan
is a type of bird that can eat and fly. Hence, it has to extend the Bird
class.
public class Swan extends Bird {
@Override
public void eat() {
System.out.println("OMG! I can eat pizza!");
}
@Override
public void fly() {
System.out.println("I believe I can fly!");
}
}
Main
is the main class of our program which contains its logic. It has two methods, letBirdsFly(List<Bird> birds)
and main(String[] args)
. The first method takes a list of birds as a parameter and invokes their fly methods. The second one creates the list and passes it to the first.
public class Main {
public static void letBirdsFly(List<Bird> birds) {
for(Bird bird: birds) {
bird.fly();
}
}
public static void main(String[] args) {
List<Bird> birds = new ArrayList<Bird>();
birds.add(new Bird());
letBirdsFly(birds);
}
}
The program simply creates a list of birds and lets them fly. If you try to run this program, it will output the following statement:
I can fly.
Now, let’s try to apply the definition of this principle to our main method and see what happens. We are going to replace the Bird
object with the Swan
object.
public static void main(String[] args) {
List<Bird> birds = new ArrayList<Bird>();
birds.add(new Swan());
letBirdsFly(birds);
}
If we try to run the program after applying the changes, it will output the following statement:
I believe I can fly!
We can see that the principle applies to our code perfectly. The program works as expected without any errors or problems. But, what if we tried to extend the Bird
class by a new type of bird that cannot fly?
public class Penguin extends Bird {
@Override
public void eat() {
System.out.println("Can I eat taco?");
}
@Override
public void fly() {
throw new UnsupportedOperationException("Help! I cannot fly!");
}
}
We can check whether the principle still applied to our code or not by adding a Penguin
object to the list of birds and run the code.
public static void main(String[] args) {
List<Bird> birds = new ArrayList<Bird>();
birds.add(new Swan());
birds.add(new Penguin());
letBirdsFly(birds);
}
I believe I can fly!
Exception in thread "main"
java.lang.UnsupportedOperationException: Help! I cannot fly!
Ops! it didn’t work as expected!
We can see that with the Swan
object, the code worked perfectly. But with the Penguin
object, the code threw UnsupportedOperationException
. This violates the Liskov Substitution Principle as the Bird
class has a child that didn’t use inheritance correctly, hence caused a problem. The Penguin
tries to extend the flying logic, but it can’t fly!
We can fix this problem using the following if check:
public static void letBirdsFly(List<Bird> birds) {
for(Bird bird: birds) {
if(!(bird instanceof Penguin)) {
bird.fly();
}
}
}
But this solution is considered a bad practice, and it violates the Open-Closed Principle. Imagine if we add another three types of birds that cannot fly. The code is going to become a mess. Notice also that one of the definitions for the Liskov Substitution Principle, which is developed by Robert C. Martin is:
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
This is not the case with our solution, as we’re trying to know the type of the Bird
object to avoid the misbehavior of the non-flying birds.
One of the clean solutions to solve this issue and refollow the principle is to separate the flying logic in another class.
public class Bird {
public void eat() {
System.out.println("I can eat.");
}
}
public class FlyingBird extends Bird {
public void fly() {
System.out.println("I can fly.");
}
}
public class Swan extends FlyingBird {
@Override
public void eat() {
System.out.println("OMG! I can eat pizza!");
}
@Override
public void fly() {
System.out.println("I believe I can fly!");
}
}
public class Penguin extends Bird {
@Override
void eat() {
System.out.println("Can I eat taco?");
}
}
Now we can edit the letBirdsFly
method to support flying birds only.
public class Main {
public static void letBirdsFly(List<FlyingBird> flyingBirds) {
for(FlyingBird flyingBird: flyingBirds) {
flyingBird.fly();
}
}
public static void main(String[] args) {
List<FlyingBird> flyingBirds = new ArrayList<FlyingBird>();
flyingBirds.add(new Swan());
letBirdsFly(flyingBirds);
}
}
The reason we forced the letBirdsFly
method to accept flying birds only is to guarantee that any substitution for the FlyingBird
will be able to fly. Now the program works as expected and outputs the following statements:
I believe I can fly!
You can see that the Liskov Substitution Principle is about using the inheritance relationship in the correct manner. You’ve to create subtypes of some parent if and only if they’re going to implement its logic properly without causing any problems.
We’ve reached the end of this journey, but we still have another two principles to cover. So take your time reading about this principle and make sure that you understand it before moving on. Stay tuned!
Top comments (9)
is this different from polymorphism?
i see an opportunity for using interfaces rather than inheritance. Unless LSP is strictly about class inheritance (but that seems contrived since interfaces in Java are effectively just classes with extra constraints to regulate their single inheritance model).
if they are the same then an interface Flyable implemented by flying birds (base class has eat) would work. the Main letBirdsFly method can be letFly and accept a list of Flyable (extending ability to let any flying object be invoked, even beyond birds).
Polymorphism is an object-oriented programming concept. This principle is a design principle which uses the OOP concepts such as Polymorphism to achieve a cleaner software design (Object-Oriented Design).
You can use normal classes, abstract ones, or interfaces. The principle isn't stricted to any type of them. Actually I used inheritance for simplicity, as there are many languages that don't support interfaces. You can use interfaces if u won't create objects from the parents, but if it's not the case use normal classes and inheritance.
You have to know that it doesn't change the way the principle is applied.
thanks for the thorough response. i hadn’t heard of the LSP before but understood its application in polymorphism.
and thank you for the series it was well written.
Excellent explaination!
Thanks Jason. I'm glad you liked it!
great article !
Thanks Maxi!
good job. )
Thanks Anton.