SOLID: Liskov Substitution Principle

Prabhanu Gunaweera
6 min readDec 16, 2023

--

Let's talk about the Liskov substitution principle.

The Liskov substitution principle (LSP) is a tricky-to-understand principle of SOLID. Having a good understanding of this is crucial to make you a better programmer. Especially to handle inheritance properly. The LSP is a little confusing if you don’t have the correct understanding and after getting the correct idea of this you will discover the importance of aligning your code with this principle.

Let’s dive into this;

The LSP was originally introduced by American Computer Scientist, Barbara Liskov in 1987. The original representation of the principle was a mathematical one and it is as follows;

“Let φ(x) be a property provable about objects x of type T. Then φ(y) should also be true for objects y of type S where S is a subtype of T

Robert Martin(AKA Uncle Bob) rephrased this as follows;

“Derived classes must be usable through the base class interface, without need for the user to know the difference”

Okay now you have the basic idea of the LSP, Oh isn’t it? Then let me write this in straightforward terms, and it would be like this,

“Subtypes must be substitutable for their base type”

I guess now it is more clear to you. Let’s take an example to get some further understanding,

Most of the materials I read online when I was trying to learn this principle used the rectangle and square example and it's somewhat confusing since we can’t get rid of the idea that ‘the square is a rectangle’, what we learnt in our maths classes. In programming ideally, the square is not a rectangle. Let’s keep that example aside for the moment. I will take a simple example to explain where this goes wrong,

Assume we are writing a program for the birds section in a zoo.

We can create a base class called Bird and add the fly and walk method to the Bird class. Duck and Ostrich classes inheriting from the Bird class, the class diagram is as follows,

Java implementation of this is as follows, Since Ostrich cannot fly I have to return an error when executing the fly method of the Ostrich.

public class Bird{
public void fly() {
System.out.println("Bird is flying");
}
public void walk(){
System.out.println("Bird is walking");
}
}

public class Duck extends Bird{
@Override
public void fly() {
System.out.println("Duck is flying");
}

@Override
public void walk() {
System.out.println("Duck is walking");
}
}

public class Ostrich extends Bird{
@Override
public void fly() {
throw new UnsupportedOperationException("Ostrich cannot fly");
}

@Override
public void walk() {
System.out.println("Ostrich is walking");
}
}

Let's assume that every day we have to give exercises for the Birds and Birds should have to walk and fly while exercising

public class Zoo {
public static void main(String[] args) {
Bird[] birds = new Bird[2];
birds[0] = new Bird();
birds[1] = new Bird();

for (Bird bird : birds) {
bird.walk();
bird.fly();
}
}
}

Now let's substitute Bird with its sub-classes, which are Duck and Ostrich and the main class looks as follows;

public class Zoo {
public static void main(String[] args) {
Bird[] birds = new Bird[2];
birds[0] = new Duck();
birds[1] = new Ostrich();

for (Bird bird : birds) {
bird.walk();
bird.fly();
}
}
}

When executing Ostrich's fly method, this program will throw an error since Ostrich cannot fly. Now we should go back and think about our implementation.

What went wrong, the reason for the above error is we included the flying method inside the bird base class and it is also not a common method for all the birds. Therefore, it is impossible to substitute the Bird base class with its sub-classes without breaking the code. This is where we are violating LSP.

So what can we do to correct this? We can add another class called FlyingBird and inherit the Duck class from that class.

New implementation as follows,

public class Bird{
public void walk(){}
}

public class FlyingBird extends Bird{
public void fly(){}
}
public class Duck extends FlyingBird{}

public class Ostrich extends Bird{}

Now the main method would be like following, this

public class Zoo {
public static void main(String[] args) {
Bird[] birds = new Bird[2];
birds[0] = new Bird();
birds[1] = new Bird();

for (Bird bird : birds) {
bird.walk();
//This check must be implemented because there is no
//fly method exists in the Bird class
if (bird instanceof FlyingBirds) {
((FlyingBirds) bird).fly();
}
}
}
}

Now let's try to run the same program by substituting bird with its subclasses;

public class Zoo {
public static void main(String[] args) {
Bird[] birds = new Bird[2];
birds[0] = new Duck();
birds[1] = new Ostrich();

for (Bird bird : birds) {
bird.walk();
if (bird instanceof FlyingBirds) {
((FlyingBirds) bird).fly();
}
}
}
}

It won't throw any error because we were able to successfully substitute sub-classes for their base class. This example follows the LSP and we have done proper inheritance inside the code.

I think now you have a good understanding of how to write code obeying the LSP.

(I got the idea of this example from the Stackoverflow answer and extended that to include it in this article, Here is the link for that answer.)

How to violate LSP

As you have seen in the above example LSP will be violated through bad inheritance and polymorphism, What are the common scenarios that violate LSP

  1. Return something unexpected — In typed languages, this is not possible
  2. Throwing errors — We did it in the above example
  3. Changing the core — Modifying the behaviour of the superclass in the subclass, adding extra preconditions or postconditions, throwing new exceptions or errors, or returning different types or values

How to correct LSP violations

This is somewhat tricky, as in our example we can change the class hierarchy to correct LSP violations. In our case we introduce a new flying bird class, Likewise, we can introduce new classes and interfaces. Also, we can refactor polymorphism as well to prevent LSP violations. The most important thing is having a good understanding of the business case and building up business logic and class hierarchy based on that. And careful about is-a relationships and while modifying the code, ensure changes won't break inheritance.

This is a little tricky, in our example we modified the class hierarchy to fix LSP violations. In our case we introduce a new flying bird class, similarly, we can introduce new classes and interfaces. Also, we can reconstruct polymorphism to prevent LSP violations. The most important thing is to have a good understanding of the business case and build the business logic and class hierarchy based on that. Take care of relationships and while changing code, ensure that changes don’t break inheritance.

Here we come to the end of an article on one of the SOLID principles, hope to talk about other principles in upcoming articles. If you have any queries on this please ping me on LinkedIn. Follow me to read more articles on programming and software development. Let's meet with a new article soon.

Thanks for reading..! Happy debugging..!

--

--

Prabhanu Gunaweera
Prabhanu Gunaweera

Written by Prabhanu Gunaweera

Engineer | Tech blogger | Flutter consultant CHECKOUT - www.prabhanu.com

No responses yet