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 shouldn‘t 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.