Liskov substitution principle

Subclasses should satisfy the expectations of clients accessing subclass objects through references of supercalss type, not just as regards syntactic safety (such as absence of “method-not-found” errors) but also as regards behavioral correctness.

Barbara Liskov

When inheritance is used, it should be possible to replace an object of the base class with an object of a subclass without breaking the program (either technically or from a business perspective). In other words, if a class Child inherits from a class Parent, then an object of the Child class should be usable in any method that works with a Parent reference or pointer.

This means the subclass must implement all the methods of the base class. The subclass should add new features or extend the base class, but it should not change or remove any important behavior.

Let’s look at an example. We have a base class IRobotRoomba, and two classes (specific models) that inherit from it:

public class IRobotRoomba {
    public void vacuum () {
        System.out.println("IRobot Roomba is vacuuming");
    }

    public void mop () throws Exception {
        System.out.println("IRobot Roomba is mopping");
    }
}
public class Plus405Combo extends IRobotRoomba{
    final int height = 106;

    @Override
    public void vacuum() {
        System.out.println("IRobot Roomba Plus 405 Combo is vacuuming");
    }

    @Override
    public void mop() {
        System.out.println("IRobot Roomba Plus 405 Combo is mopping");
    }
}
public class I5 extends IRobotRoomba {
    final int height = 92;

    @Override
    public void vacuum() {
        System.out.println("IRobot Roomba I5 is vacuuming");
    }

    @Override
    public void mop() throws Exception {
        // THIS MODEL CAN'T MOP
        throw new Exception();
    }
}

Now, we have a class SmartHomeSystem that uses the robot to vacuum and mop:

public class SmartHomeSystem {
    public void vacuumHome (IRobotRoomba iRobot) {
        iRobot.vacuum();
    }
    public void mopHome (IRobotRoomba iRobot) {
        try {
            iRobot.mop();
        } catch (Exception e) {
            System.err.println("An error occured!");
        }
    }
}

Let’s create a list of robots:

  • iRobotRoomba,
  • iRobotRoomba Plus 405 Combo,
  • iRobotRoomba i5

and use them to vacuum and mop the house:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Runner {
    public static void main (String[] args) {
        SmartHomeSystem system = new SmartHomeSystem();

        List<IRobotRoomba> robots = new ArrayList<>(Arrays.asList(new IRobotRoomba(), new Plus405Combo(), new I5()));

        for (IRobotRoomba robot : robots) {
            system.vacuumHome(robot);
            system.mopHome(robot);
        };
    }
}

Program output:

IRobot Roomba is vacuuming  
IRobot Roomba is mopping  
IRobot Roomba Plus 405 Combo is vacuuming  
IRobot Roomba Plus 405 Combo is mopping  
IRobot Roomba I5 is vacuuming
An error occured!

The I5 model cannot mop. Replacing the base class with I5 breaks the program – it breaks the Liskov substitution principle.

Improved implementation

Let’s move the mopping function into a separate interface:

public class IRobotRoomba {
    public void vacuum () {
        System.out.println("IRobot Roomba is vacuuming");
    }
}
public interface moppingDevice {
    void mop ();
}

and implement it in iRobot Roomba Plus 405 Combo:

public class Plus405Combo extends IRobotRoomba implements MoppingDevice {
    final int height = 106;

    @Override
    public void vacuum() {
        System.out.println("IRobot Roomba Plus 405 Combo is vacuuming");
    }

    @Override
    public void mop() {
        System.out.println("IRobot Roomba Plus 405 Combo is mopping");
    }
}

Class I5 only needs to implement 1 method now:

public class I5 extends IRobotRoomba {
    final int height = 92;

    @Override
    public void vacuum() {
        System.out.println("IRobot Roomba I5 is vacuuming");
    }
}

We also update the SmartHomeSystem class to use objects implementing MoppingDevice interface for mopping:

public class SmartHomeSystem {
    public void vacuumHome(IRobotRoomba iRobot) {
        iRobot.vacuum();
    }

    public void mopHome(MoppingDevice iRobot) {
        iRobot.mop();

    }
}

This time we need to create 2 lists:

  • robots contain all IRobotRoomba devices (vacuum cleaners),
  • but moppingDevices contains only devices implementing moppingDevice interface (only iRobot Roomba Plus 405 Combo at the moment)

and again use them to vacuum and mop the house:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Runner {
    public static void main (String[] args) {
        SmartHomeSystem system = new SmartHomeSystem();

        List<IRobotRoomba> robots = new ArrayList<>(Arrays.asList(new IRobotRoomba(), new Plus405Combo(), new I5()));
        List<MoppingDevice> moppingDevices = new ArrayList<>(Arrays.asList(new Plus405Combo()));

        for (IRobotRoomba robot : robots) {
            system.vacuumHome(robot);
        };

        for (MoppingDevice moppingDevice : moppingDevices) {
            system.mopHome(moppingDevice);
        };
    }
}

Program output:

IRobot Roomba is vacuuming  
IRobot Roomba Plus 405 Combo is vacuuming  
IRobot Roomba I5 is vacuuming  
IRobot Roomba Plus 405 Combo is mopping

We moved the problematic method into a separate interface. Only robots that can mop implement it. This way, we can safely replace base class objects with subclass objects.

Additional rules from the Liskov substitution principle

There are 2more rules that come from the Liskov substitution principle:

  • Subclasses shouldn’t strengthen preconditions.
    Preconditions can be weakaned – it won’t break a program. If the preconditions in the base class are harder to satisfy, the program will still have to satisfy them..
  • Subclasses shouldnt weaken postconditions.
    Postconditions can be strengthen – it won’t break a program. Można wzmacniać warunki końcowe ponieważ to nie zepsuje programu.If the postconditions in the base class are easier to satisfy, the program will be prepared to handle them.