Liskov substitution principle
17th Aug 2020 by Aneesh Mistry

Key Takeaways
• Understand the importance of the Liskov substitution principle to inheritance and shared class behaviour.
• Review the rules that ensure the inherited behaviours of subclasses from their parents are as expected.
• Design tests to ensure the behaviour of a parent class is not modified by its subclasses.

What is the Liskov Substitution Principle?

Introduced by Barbara Liskov, the Liskov substitution principle (LSP) is used to decide when it is appropriate to extend a class as opposed to other strategies of abstraction to achieve the goal of shared properties between classes. As we have seen in the previous blog, the open-closed principle can be used to abstract the behaviours of a method; the Liskov substitution principle is used to extend upon the design by contract approach where the behaviour of the class is the primary concern.

The Liskov substitution principle is used to make assertions upon the substitution of behaviours that are implied by a subclass from its parent. When a class is extended, we are in effect implying that the subclass "IS A" extension of the parent class. While the open-closed principle is a focussed upon the structure of a class, LSP promotes strong behaviour sub-typing of classes to the parent class to ensure the expected behaviour of the methods are applicable to the extension.

The below quote is often used to describe the importance of behaviours between inherited classes:
"If it looks like a duck, quacks like a duck, but needs batteries - you probably have the wrong abstraction"


Liskov Substitution by design

When we use inheritance to share class methods and properties, we are implying that the subclass can be used to replace instances of the parent class.
The LSP asserts three rules when applying inheritance with subclasses:
• The subclass cannot enforce stricter rules than the parent class.
• The return value of a subclass method can only differ as a subclass of the return type from the parent.
• The subclass of a parent must be completely capable of substitution for its parent across each behaviour.


Implementing LSP by design

If we reflect upon the rules above, the first two can be validated through the Java compiler and are therefore inherent with Java. A problem can arise with ensuring the behaviour of each method is appropriate for the subclass of the parent.


Demonstration: Is a Penguin a bird?

The below code example demonstrates how easily behaviour can be modified from the intuition that a subclass IS AN extension of a parent class. When we design an application of Birds, Penguins, and Seagull Objects, we may reflect upon the use of class inheritance by saying, out loud, "a Penguin is a Bird, and a Seagull is also a Bird". In our source code, we may therefore create the Seagull and Penguin as subclasses of Bird.

Image of the classes

class Bird{
    private boolean hasWings = true;

    public void fly(){

    }
    public boolean getWings(){
        return this.hasWings;
    }    
}

class Penguin extends Bird{

}
class Seagull extends Bird{

}

In our example above, we have confused the properties of the Bird class with the behaviours of the Bird class when creating the Penguin class. The Penguin is a bird because it has the properties of a bird: it has wings, lays hard-shelled eggs etc. However the Penguin does not behave like a bird in the sense that it cannot fly.

The LSP has been breached as the Penguin class is now capable of calling the .fly() method; a behaviour that should not be possible.


Complying with LSP

To resolve the problem, the behaviour of the Bird class can be further split into two: BirdCanFly and BirdCannotFly:

class BirdCanFly{
   private boolean hasWings = true;

    public void fly(){

    }
    public boolean getWings(){
        return this.hasWings;
    } 
}
class BirdCannotFly{

   private boolean hasWings = true;

    public boolean getWings(){
        return this.hasWings;
    } 
}

The Penguin and Seagull can now extend the appropriate class to obtain the Bird properties with and without the flying behaviours:

class Penguin extends BirdCannotFly{

}

class Seagull extends BirdCanFly{

}

The example above illustrates the potential conflict that can arise with the implied substitution from inheritance. The behaviours of flying can be further imposed by the parent class with methods such as "getTopFlightSpeed" and "land" that would imply further irrelevance for the Penguin class without the redefined BirdCannotFly class. The


Conclusion

The LSP is applied to application design when we consider the behaviours of classes that are inherited from other parent classes. Inheritance is one of the most valuable OOP concepts and therefore LSP is an important consideration to make upon child classes whenever a class is extended.

While it may seem natural to extend classes that fit into the "IS A" Object construct, we must consider how the behaviours of a class are inherited, and not just the properties. LSP can be implemented by reviewing the behaviours each class will inherit before adding behaviours to a parent class or extending them, or they can be implemented through unit testing of a parent class with each of their child classes.

Series link: Go to the next SOLID principle: Interface segregation Principle


Share this post