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

How it’s broken

Imagine you were tasked with managing a small fleet of Spaceships. You know that, among other things, they will be expected to fly to a destination, sometimes even to engage camouflage, to pass by enemy forces unnoticed:

public interface Spaceship {

    String flyToDestination();
    String camouflage();
}

With these specifications, you put out a request for two spacecraft – a StealthShip, for the more dangerous excursions, and a CargoShip for your regular supply runs. The request is quickly granted and you receive a brand new Stealth Ship, capable of stealthily flying to the planet Titan:

public class StealthShip implements Spaceship {

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

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

…As well as as a more modest Cargo Ship:

public class CargoShip implements Spaceship {

    @Override
    public String flyToDestination() {
        return "Flying to Omicron Persei 8";
    }

    @Override
    public String camouflage() {
        throw new NotImplementedException();
    }
}

The camouflage() method of the CargoShip should immediately spark your interest. Since the Spaceship interface demands that all its children should implement the method, there was no other choice but to do so. Unfortunately, a CargoShip does not (and, in our case, should not) possess the ability to camouflage. To circumvent this problem, someone decided to make the method throw a NotImplementedException.

Let’s assume, for the sake of example, that you didn’t notice this design flaw and asked one of your subordinates to order the ships to depart:

public class LiskovSubstitionPrincipleExample1 {

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

Unfortunately, this doesn’t go well:

Exception in thread “main” sun.reflect.generics.reflectiveObjects.NotImplementedException
at pl.danielfrak.code.lsp.example1.broken.CargoShip.camouflage(CargoShip.java:14)
at pl.danielfrak.code.lsp.example1.broken.LiskovSubstitionPrincipleExample1.main(LiskovSubstitionPrincipleExample1.java:12)

The person responsible for commanding your fleet to depart was not aware of the implementation details of the different Spaceships (and they shouldn’t be). They assumed that, since the Spaceship interface declares the ability to camouflage(), then every such ship should be able to do so. The assumption was correct; the error is in how the interface was designed.

Rephrasing our original definition, the Liskov Substitution Principle tells us that, if StealthShip extends Spaceship, then every instance of StealthShip should be replaceable by any instance of Spaceship so that the application continues to work. CargoShip is an instance of Spaceship (by virtue of inheritance), so, within the abstraction of Spaceship they should be interchangeable. Clearly, they are not.

How to fix it

Because not every Spaceship should be able to camouflage() (as demonstrated by the existence of a CargoShip) then we should not define that method in the Spaceship interface. Instead, we should define two different interfaces, fixing what was a violation of the Interface Segregation Principle:

public interface Spaceship {

    String flyToDestination();
}

public interface CamouflageableSpaceship extends Spaceship {

    String camouflage();
}

We can now redesign our ships:

public class StealthShip implements CamouflageableSpaceship {

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

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

public class CargoShip implements Spaceship {

    @Override
    public String flyToDestination() {
        return "Flying to Omicron Persei 8";
    }
}

The fact that a StealthShip can use camouflage and a CargoShip cannot is now made explicit, there is no way to accidentally order the CargoShip to engage its nonexistent stealth mode. Thus, we have ensured that the orders will always be correct.

public static void main(String[] args) {
        List<Spaceship> nonStealthShips = Arrays.asList(new CargoShip(), new CargoShip());
        for(Spaceship spaceship : nonStealthShips) {
            System.out.println(spaceship.flyToDestination());
        }

        List<CamouflageableSpaceship> stealthShips = Arrays.asList(new StealthShip(), new StealthShip());
        for(CamouflageableSpaceship spaceship : stealthShips) {
            System.out.println(spaceship.flyToDestination());
            System.out.println(spaceship.camouflage());
        }
    }
// Console output
Flying to Omicron Persei 8
Flying to Omicron Persei 8
Flying to Titan
Engaging camouflage!
Flying to Titan
Engaging camouflage!

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. 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.

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.

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 *