Liskov Substitution Principle

The Liskov Substitution Principle (LSP) is a principle in object-oriented programming that states that objects of a superclass should be able to be replaced with objects of a subclass without affecting the correctness of the program. In other words, if a class B is a subclass of class A, then an object of class A should be able to be used in any context where an object of class B is expected, and the program should continue to function correctly. This principle helps to ensure that the class hierarchy is structured in a way that promotes flexibility and reusability of code.

LSP is a useful principle for ensuring that a program’s class hierarchy is structured in a way that promotes flexibility and reusability of code. He states that the LSP is closely related to the concept of polymorphism and that it helps to ensure that objects of different classes can be treated as objects of a common superclass, which allows for the creation of more generic and flexible code.

Let’s check the below example:

class Bird {
    public void fly() {
        System.out.println("I can fly");
    }

    public void eats(){
        System.out.println("I eat a lot!!");
    }
}

class Penguin extends Bird {
    // no fly method 
}

class Eagle extends Bird {
    @Override
    public void fly() {
        System.out.println("I can fly very high");
    }
}

Let’s say we have a base class called Bird and two subclasses called Penguin and Eagle. The Bird class has a method called fly() which is overridden by the Eagle class and not overridden by the Penguin class. Now, let’s say we have a method called takeOff(Bird bird) that takes a Bird object as a parameter and calls the fly() method on it.

public void takeOff(Bird bird) {
    bird.fly();
}

As per LSP, we should be able to substitute an object of a subclass for an object of a superclass without affecting the correctness of the program. In this example, we should be able to pass an object of the Eagle or Penguin class to the takeOff() method and the program should continue to function correctly.

If we call the takeOff(Bird bird) method with an Eagle object, it will print “I can fly very high”, if we call it with a Penguin object, it will call the base class method and print “I can fly”. Surprise!!! Our Penguin can fly! Interesting thing about the above example is how sometimes something that sounds very logical in natural language doesn’t quite work well in code. In essence we are breaking LSP with the above code. How can we make the code follow LSP then?

Flying is an abstraction that demands an interface of its own. Birds can fly so can planes or helicopters. We can segregate the this Flying property into an interface and then make only flying bird have that behavior. Something like:

public interface Flyable {
    void fly();
}

class Bird {
    public void eats() {
        System.out.println("I eat a lot!!");
    }
}

class Eagle extends Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("I fly very high!");
    }
}

class Penguin extends Bird {
   
}

class Airplane implements Flyable{

    @Override
    public void fly() {
        System.out.println("I fly. You better pray that I fly without crashing!");
    }
}

Note: We can similarly separate “eat” abstraction into a different interface but that’s for another day.

In the new refined example, we can unabashedly shamelessly substitute child class like Eagle and Penguin in place Bird like LSP advocates.

This is a simple example, in real world scenario, we may have more complex class hierarchy, and LSP helps to ensure that the class hierarchy is structured in a way that promotes flexibility and reusability of code while maintaining the correctness of the program.

Based on the things we discussed, we can summarize that LSP helps achieve:

  • Reusability: Adhering to the LSP makes it easier to verify that code created for a particular class should also function for its subclasses, facilitating easier code reuse.
  • Flexibility and Maintainability: Because code created for one class should also function for its subclasses, the LSP promotes the development of more adaptable and maintainable software.
    This demonstrates that modifications can be done without impairing current functionality.
  • Readability and Understandability: Following the LSP can help to improve the readability and understandability of code, as it helps to ensure that classes and objects can be used interchangeably.

Posted

in