SOLID Principles #3 – The Liskov Substitution Principle

The Liskov Substitution Principle, introduced by Barbara Liskov, represents the “L” in SOLID. It states that every implementation of a class should be replaceable with an implementation of any class that extends it. In other words, every instance of a parent class should be replaceable by any instance of its child classes so that the application continues to work.

At its most basic, it means that a client should not need to know which specific sub-type of a parent entity they are calling if they are only interested in behavior declared in the parent.

A popular meme represents this principle as an inverse of the Duck Test:

If it looks like a duck, quacks like a duck, but needs batteries – you probably have the wrong abstraction.

Example 1 – a glaring violation

How it’s broken

Imagine you were tasked with managing a small fleet of Spaceships. You know that they will be expected to fly to a destination:

public interface Spaceship {

    String flyToDestination(String destination);
}

The fleet consists of a CargoShip, a PassengerShip and two StealthShips. The StealthShips should engage their camouflage before departing, to pass by enemy forces unnoticed:

public class CargoShip implements Spaceship {

    @Override
    public String flyToDestination(String destination) {
        return "Flying to " + destination;
    }
}
public class PassengerShip implements Spaceship {

    @Override
    public String flyToDestination(String destination) {
        return "Flying comfortably to " + destination;
    }
}
public class StealthShip implements Spaceship {

    @Override
    public String flyToDestination(String destination) {
        return "Flying to " + destination;
    }

    public String camouflage() {
        return "Engaging camouflage!";
    }
}

Eager to fulfill your mission, you instruct your Spaceships to depart. You decide to personally check every ship to see if it is a StealthShip, so that you can instruct it to engage its camouflage:

public class LiskovSubstitutionPrincipleExample1 {

    public static void main(String[] args) {
        List<Spaceship> fleet = Arrays.asList(
                new StealthShip(), new StealthShip(), new CargoShip(), new PassengerShip());
        for(Spaceship spaceship : fleet) {
            depart(spaceship);
        }
    }

    private static void depart(Spaceship spaceship) {
        if(spaceship instanceof StealthShip) {
            System.out.println(((StealthShip) spaceship).camouflage());
        }
        System.out.println(spaceship.flyToDestination("Titan"));
    }
}

You end the day exhausted from inspecting your entire fleet.

While the error in this case is not in the definition of the various Spaceships, the depart() method is clearly badly designed. You should not have to personally inspect every Spaceship to check what its subtype is.

Besides the use of instanceof, a good indicator of a violation of LSP is the usage of downcasting – in this case we are downcasting Spaceship to StealthShip to be able to engage its camouflage. Both of these are code smells and are considered to be bad practices.

The biggest problem, however, is that the depart() method in its current state must know about every possible subtype of Spaceship. If you ever add another type of ship with the ability to camouflage, an additional check will need to be added. Code like this is hard to maintain and prone to bugs.

How to fix it

The easiest solution to our dilemma is to separate the Spaceships into two fleets – one which should camouflage before departing, and one which should not:

public class LiskovSubstitutionPrincipleExample1 {

    public static void main(String[] args) {
        List<StealthShip> fleet1 = Arrays.asList(new StealthShip(), new StealthShip());
        for(StealthShip spaceship : fleet1) {
            departStealthily(spaceship);
        }

        List<Spaceship> fleet2 = Arrays.asList(new CargoShip(), new PassengerShip());
        for(Spaceship spaceship : fleet2) {
            depart(spaceship);
        }
    }

    private static void departStealthily(StealthShip spaceship) {
        System.out.println(spaceship.camouflage());
        depart(spaceship);
    }

    private static void depart(Spaceship spaceship) {
        System.out.println(spaceship.flyToDestination("Titan"));
    }
}

Now there is no need to check the types of ships – we have already segregated them into suitable categories.

If we are certain that more types of stealthy spacecraft are expected soon, we can further refactor the code and create a SpaceshipWithCamouflage interface, implement it in StealthShip and redefine our stealthy fleet as a fleet of SpaceshipsWithCamouflage:

public interface SpaceshipWithCamouflage extends Spaceship {

    String camouflage();
}
public class StealthShip implements SpaceshipWithCamouflage {

    @Override
    public String flyToDestination(String destination) {
        return "Flying to " + destination;
    }

    @Override
    public String camouflage() {
        return "Engaging camouflage!";
    }
}
public class LiskovSubstitutionPrincipleExample1 {

    public static void main(String[] args) {
        List<SpaceshipWithCamouflage> fleet1 = Arrays.asList(new StealthShip(), new StealthShip());
        for (SpaceshipWithCamouflage spaceship : fleet1) {
            departStealthily(spaceship);
        }

        List<Spaceship> fleet2 = Arrays.asList(new CargoShip(), new PassengerShip());
        for (Spaceship spaceship : fleet2) {
            depart(spaceship);
        }
    }

    private static void departStealthily(SpaceshipWithCamouflage spaceship) {
        System.out.println(spaceship.camouflage());
        depart(spaceship);
    }

    private static void depart(Spaceship spaceship) {
        System.out.println(spaceship.flyToDestination("Titan"));
    }
}

With this change, we are able to easily handle any present and future spacecraft, regardless of their concealment abilities.

One could argue that Liskov’s principle still cannot be satisfied, as we still can’t replace a StealthShip with a CargoShip if we want to enable camouflage. However, an often missed detail of LSP is that the instances should be replaceable within the abstraction of their parent. That is, we should be able to successfully call any method declared on the Spaceship interface on any of its children. The principle does not extend to behaviors which are not declared in the parent.

Example 2 – a subtle violation

How it’s broken

There are more subtle ways to violate LSP. Imagine that a battle is taking place between two captains – Capt. Adama and Capt. Reynolds. The latter, surprised at the sudden unprovoked attack by the former, has scrambled his spacecraft and is ready to engage. Both of the men have SpaceShips:

public interface Spaceship {

    void shoot(String enemy);
}

The difference is, Capt. Adama has a fleet of FighterShips:

public class FighterShip implements Spaceship {

    @Override
    public void shoot(String enemy) {
        System.out.println("Actually shooting at " + enemy);
    }
}

While Capt. Reynolds, confused by the sudden invasion, is trying to use his ToyShips:

public class ToyShip implements Spaceship {

    @Override
    public void shoot(String enemy) {
        System.out.println("Making \"pew pew\" sounds in the direction of " + enemy + "!");
    }
}

Both men try to shoot each other:

public class LiskovSubstitutionPrincipleExample2 {

    public static void main(String[] args) {
        List<Spaceship> captReynoldsFleet = Arrays.asList(new ToyShip(), new ToyShip());
        List<Spaceship> captAdamaFleet = Arrays.asList(new FighterShip(), new FighterShip());

        for (Spaceship spaceship: captReynoldsFleet) {
            spaceship.shoot("Capt. Adama");
        }
        for (Spaceship spaceship: captAdamaFleet) {
            spaceship.shoot("Capt. Reynolds");
        }
    }
}

Unfortunately, it does not end well for Capt. Reynolds:

Making "pew pew" sounds in the direction of Capt. Adama!
Making "pew pew" sounds in the direction of Capt. Adama!
Actually shooting at Capt. Reynolds
Actually shooting at Capt. Reynolds

What we have witnessed here is a problem with abstractions. The ToyShip may look similar to a SpaceShip, it may even have behaviors which are similar at first glance. However, the similarity is only an illusion. A client using a Spaceship derivative expects it to shoot at a given target. In the case of the ToyShip, this is clearly not the case, as it only makes vaguely threatening noises at its victim.

How to fix it

The solution here is to not pretend that a ToyShip is a subtype of Spaceship:

public class ToyShip { // no longer implements Spaceship

    public void shoot(String enemy) {
            System.out.println("Making \"pew pew\" sounds in the direction of " + enemy + "!");
    }
}

With this change, it becomes impossible to confuse the toy with the actual machine and so the two men can wage war without further embarrassment:

public class LiskovSubstitutionPrincipleExample2 {

    public static void main(String[] args) {
        List<Spaceship> captReynoldsFleet = Arrays.asList(new FighterShip(), new FighterShip());
        List<Spaceship> captAdamaFleet = Arrays.asList(new FighterShip(), new FighterShip());

        for (Spaceship spaceship: captAdamaFleet) {
            spaceship.shoot("Capt. Reynolds");
        }
        for (Spaceship spaceship: captReynoldsFleet) {
            spaceship.shoot("Capt. Adama");
        }
    }
}
Actually shooting at Capt. Reynolds
Actually shooting at Capt. Reynolds
Actually shooting at Capt. Adama
Actually shooting at Capt. Adama

Conclusion

When designing derived classes, it is important to make sure that they are replaceable by their siblings. That is, if two classes share a common interface (or parent class), substituting one of them for the other should not break the promises of that interface.

You should aim to create derived classes which compliment their base behavior, instead of replacing it. Conversely, that base behavior must be devised in such a way that substitution of children is possible. More often than not, it means that an interface should only declare the behaviors that it absolutely needs.

Make sure that each derived class is not only a proper child of its parent on a surface level but also matches the expected behavior of the parent class and will not lead to unexpected behavior when used by clients.

Photo by Roksolana Zasiadko

Daniel Frąk Written by:

Be First to Comment

    Leave a Reply

    Your email address will not be published. Required fields are marked *