DEV Community

Abhinav Pandey
Abhinav Pandey

Posted on • Edited on

Why is he so different from his parents? - Liskov's Substitution Principle

Liskov's substitution principle (LSP) can be described as

Instances of a derived class must be usable through the interface of its base class without the clients of the base class being able to tell the difference.

In other words, subtypes can work as a substitute for their base types.

Subtyping is a concept widely used in programming languages. A subtype is a specialization of a base type. In principle, it should have all the attributes of a base type and should be able to perform all tasks that its base type can perform. It can also have some new attributes and perform new tasks of its own.

What happens when LSP gets violated?

Instead of seeing some code as usual, lets take a different approach and illustrate this with a real life example.

John, a businessman, is visiting a city for a meeting. He decides that while he's there, he would like to do some sight seeing around the city.

He understands that he will need a vehicle to do so. He calls his friend Jane who lives in the city and asks her if he can find a vehicle when he lands at the airport. Jane informs him that he can find one just across the road from the airport exit.

John is satisfied and is looking forward to a perfect trip. However, when John lands in the city and exits the airport, he finds a train station across the road. His excitement turns into a frown.

So what went wrong? John assumed that any vehicle will get the job done for him. He didn't anticipate that some vehicles do not suit his purpose.

You may call John a bit careless but now let's see how this translates into code.

John is a client or a caller class.
Jane is a reference to a list of available vehicles.
John's plan is the task that needs to be performed.
Vehicle is a class that promised to perform the task for John.

class JohnsJourney {

    private Location currentLocation;
    private VehicleProvider vehicleProvider;
    ...

    private void travel(Location start, Location end) {
        Vehicle availableVehicle = vehicleProvider.getAvailableVehicle();
        currentLocation = availableVehicle.move(start, end);
    }
}

class Vehicle {

    String name;
    double maxSpeed;

    /**
     * Move between two locations 
     * @param start 
     * @param end
     * return final location
     */
    public Location move(Location start, Location end) {
        Location currentLocation = start;
        while(currentLocation != end) {
            keepMoving(...);
        }
        return currentLocation;
    }
    /**
     * Move on a pre-defined route 
     * @param route 
     * return final location
     */
    public abstract Location moveOnARoute(Route route);
    ...
}
Enter fullscreen mode Exit fullscreen mode

John doesn't care about what the Vehicle would be until he needs it. All he knows is that it would solve his problem. You can consider this dynamic binding. Unfortunately, one of the vehicles, Train is an imposter. It does not follow the behavior of its predecessor. It only moves on specified routes and doesn't do anything if asked to move between any two locations.

class RentalCar extends Vehicle {

    //doesn't override move

    @Override
    public Location moveOnARoute(Route route) {
        move(route.getStart(), route.getEnd());
    }
    ...
}

class Train extends Vehicle {

    @Override
    public Location move(Location start, Location end) {
        //do nothing
        return start;
    }

    @Override
    public Location moveOnARoute(Route route) {
        Location currentLocation = route.getStart();
        while(currentLocation != route.getEnd()) {
            keepMoving(...);
        }
        return currentLocation;
    }
        ...
}
Enter fullscreen mode Exit fullscreen mode

If the Vehicle was a RentalCar, it would be all good.
But its a Train and it breaks the contract which the Vehicle class presented to John.

LSP aims to avoid such unexpected situations. This brings us to a more low level explanation for LSP - when we try to call a method implemented by the child class using a reference of the base class, the result should not break the contract presented by the base class. If not, the child class is in violation of LSP and the base class is no longer suitable for dynamic binding.

Following the principle doesn't mean that all child classes will return the same output or that you shouldn't override methods. It is important to understand that contract is the basis of this principle. An Animal will move but a Dog will move differently than a Kangaroo. However, it is guaranteed that they will move and that makes them good subtypes.

How can we improve?

Add another level of abstraction - A supertype should only expose contracts that all its subtypes will be able to fulfil. If some of these subtypes work in a certain way but some of them work differently, it is likely that the contract is being exposed at the long level. Lets see how we correct this.

In our example, all vehicles cannot facilitate the move() method. Therefore, it is wrong to have that method at this level. We can rather move it to a more specialized class called PersonalVehicle.

class Vehicle {

    String name;
    double maxSpeed;
    /**
     * Move on a pre-defined route 
     * @param route 
     * return final location
     */
    public abstract Location moveOnARoute(Route route); 
    ...
}
class PersonalVehicle extends Vehicle {
    /**
     * Move between two locations 
     * @param start 
     * @param end
     * return final location
     */
    public Location move(Location start, Location end) {
        Location currentLocation = start;
        while(currentLocation != end) {
            keepMoving(...);
        }
        return currentLocation;
    }

    @Override
    public Location moveOnARoute(Route route) {
        move(route.getStart(), route.getEnd());
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)