×

Search anything:

Liskov Substitution Principle

Binary Tree book by OpenGenus

Open-Source Internship opportunity by OpenGenus for programmers. Apply now.

Introduction

Software design is not an easy task. Just drawing nice looking class diagrams with variety of classes, interfaces, showing associations etc is not enough. You will need to translate the class diagram into easily maintainable, readable, and extensible source code. These qualities of a source code can only be achieved from a good design. This brings us to the notion of SOLID principles. They were established in order to make designs easier to understand, maintain and extend.

Liskov Substitution Principle

We are going look at one of these principles, the "Liskov Substitution Principle", with some examples. This principle was first introduced by Barbara Liskov, who stated it as follows;

What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all
programs P defined in terms of T, the behavior of P is unchanged when o1 is
substituted for o2 then S is a subtype of T.

Then later paraphrased as;

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it
Robert C. Martin

What is meant here is that an object should be replaceable with their subtypes without affecting the correctness of the program.

Let's take a very common example to explain this.
We have a Bird and an Ostrich is a bird, a bird can fly but an ostrich cannot, consider the following class diagram:

bird-ostrich-incorrect
We have the corresponding source code;


public class Bird {
    public void fly(){
        //Using my wings to fly!
    }
}

public class Ostrich extends Bird {
    @override
    public void fly(){
        //unimplemented
        throw new RuntimeException();
       }
   }
}

The behaviour of the Ostrich when flying is different from the bird when flying, this doesn't conform to the liskov substitution principle. Passing the Ostrich to a function which takes a reference to a bird as parameter will throw a runtime exception, affecting the correctness of the program.
We modify the design as follows;

bird-ostrich-correct
and obtain this source code;


public class Bird{
    //I'm a Bird
}
public class FlyingBirds extends Bird{
    //I can fly
    
    public void fly(){
    //Using my wings to fly
}

public class Ostrich extends Bird{
    //I can't fly
} 

Another example is the car and racing, a racing car, we have:

car-racing-incorrect


class Car {
    
    public double getCabinWidth(){
        //return cabin width
    }
}

class racingCar extends Car {
    @override
    public double getCabinWidth(){
        //UNIMPLEMENTED
    }
    
    public double getcockpitWidth(){
        //return cockpith width
    }
    
}

We have the CarUtils car where we are going to study the behaviour of the RacingCar relative to Car.


public class CarUtils {
    public static void main(String[] args) {
        car first = new Car();
        car second = new Car();
        car third = new RacingCar();
        
        List<Car> myCars = new ArrayList<>();
        myCars.add(first);
        myCars.add(second);
        myCars.add(third);
        
        for(Car car: myCars) {
            System.out.println(car.getCabinWidth());
        }
    }
}

In the main() we created three variables of Car reference. The first two Car reference are instances of Car while the third Car is an instance of RacingCar.
All the cars are added in the list and the for loop is iterated.
The first and second car return the width of their cabin which is then printed. In the third iteration the code will not work correctly since getCabinWidth() unimplemented here, hence differences in behaviour from its base type and failure to respect the liskov principle.

You can solve this by altering class diagram, ie;

bird-ostrich-correct-1

Then we have;


class vehicle {
    //I'm a vehicle
    
}

class Car extends Vehicle {
    // I'm a car with cabinWidth
    
}

class RacingCar extends Vehicle {

    //I'm a Vehicle with cockpitWidth
    
}

This implementation doesn't violates the liskov substitution principle.

Lets try to not to change the inheritance and modify Car and RacingCar using instanceof, ie;


Class Car {
    
   public double getCabinWidth(Car r) {
        if (r instanceof Car) {
            return r.cabinWidth;
        else if (r instanceof RacingCar) {
            return r.cockPitWidth;
        }
    }
    
}

class RacingCar extends Car {
    
    public double getCockpitWidth() {
        return this.cockPitWidth;
    }
    
}

This solution seems to work well, the if clause will be verified for both instances of Car and RacingCar, But what if we add another derived class of Car, the above code will have to be modified, hence violation of the Open-Closed Principle..
Trying to violate the liskov principle as I've done above leads to tightly coupled classes. The overall result will just be a high number of interdependent classes which are difficult to maintain.

Conclusion

We should always apply the Liskov Substitution principle it enables us to design valid inheritance associations as well as writing extensible , easily maintainable source code with loosely coupled software modules.

Liskov Substitution Principle
Share this