DEV Community

Cover image for Liskov Substitution Principle
Amr Saeed
Amr Saeed

Posted on • Originally published at amrsaeed.com

Liskov Substitution Principle

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.");
    }
}
Enter fullscreen mode Exit fullscreen mode

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!");
    }
}
Enter fullscreen mode Exit fullscreen mode

bird-swan-2

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

If we try to run the program after applying the changes, it will output the following statement:

I believe I can fly!
Enter fullscreen mode Exit fullscreen mode

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!");
    }
}
Enter fullscreen mode Exit fullscreen mode

bird-swan-penguin-1

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);
}
Enter fullscreen mode Exit fullscreen mode
I believe I can fly!
Exception in thread "main" 
java.lang.UnsupportedOperationException: Help! I cannot fly!
Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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.");
    }
}
Enter fullscreen mode Exit fullscreen mode
public class FlyingBird extends Bird {

    public void fly() {
        System.out.println("I can fly.");
    }
}
Enter fullscreen mode Exit fullscreen mode
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!");
    }
}
Enter fullscreen mode Exit fullscreen mode
public class Penguin extends Bird {

    @Override
    void eat() {
        System.out.println("Can I eat taco?");
    }
}
Enter fullscreen mode Exit fullscreen mode

bird-swan-penguin-flyingbird-3

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
vampiire profile image
Vamp

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).

Collapse
 
amrsaeedhosny profile image
Amr Saeed

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.

Collapse
 
vampiire profile image
Vamp

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.

Collapse
 
codemouse92 profile image
Jason C. McDonald

Excellent explaination!

Collapse
 
amrsaeedhosny profile image
Amr Saeed

Thanks Jason. I'm glad you liked it!

Collapse
 
mcsee profile image
Maxi Contieri

great article !

Collapse
 
amrsaeedhosny profile image
Amr Saeed

Thanks Maxi!

Collapse
 
nismooooooo profile image
Anton Dreka

good job. )

Collapse
 
amrsaeedhosny profile image
Amr Saeed

Thanks Anton.