SOLID Principles: Liskov Substitution Principle

Amr Saeed
5 min readOct 25, 2020

--

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:

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!

Originally published at https://amrsaeed.com on October 25, 2020.

--

--

Amr Saeed

A software engineer with 5+ years working in back-end, front-end, and DevOps. Love building cool stuff that scales and making life easier for fellow developers.