Obiekty klasy pochodnej powinny dawać się używać w miejscach, gdzie oczekiwane są obiekty klasy bazowej, bez wpływu na poprawność działania programu.
Barbara Liskov
Jeżeli występuje dziedziczenie, powinno być możliwe użycie obiektu klasy potomnej zamiast klasy bazowej bez wpływu na poprawność działania programu (zarówno od strony językowej jak i biznesowej). Czyli jeśli klasa Potomna dziedziczy z klasy Bazowej, to obiekt klasy Bazowej powinno dać się zastąpić obiektem klasy Potomnej w każdej funkcji używającej wskaźnika lub referencji do klasy Bazowej.
Oznacza to, że klasa potomna musi implementować wszystkie metody klasy rodzica. Klasa potomna powinna rozszerzać możliwości klasy bazowej.
Rozważmy przykładową sytuację. Mamy klasę bazową IRobotRoomba i 2 dziedziczące z niej klasy reprezentujące poszczególne modele:
public class IRobotRoomba {
public void vacuum () {
System.out.println("IRobot Roomba odkurza");
}
public void mop () throws Exception {
System.out.println("IRobot Roomba mopuje");
}
}
public class Plus405Combo extends IRobotRoomba{
final int height = 106;
@Override
public void vacuum() {
System.out.println("IRobot Roomba Plus 405 Combo odkurza");
}
@Override
public void mop() {
System.out.println("IRobot Roomba Plus 405 Combo mopuje");
}
}
public class I5 extends IRobotRoomba {
final int height = 92;
@Override
public void vacuum() {
System.out.println("IRobot Roomba I5 odkurza");
}
@Override
public void mop() throws Exception {
// TEN MODEL NIE MA FUNKCJI MOPOWANIA throw new Exception();
}
}
Klasa SmartHomeSystem odpowiada za sprzątanie domu: odkurzanie i mopowanie:
public class SmartHomeSystem {
public void vacuumHome (IRobotRoomba iRobot) {
iRobot.vacuum();
}
public void mopHome (IRobotRoomba iRobot) {
try {
iRobot.mop();
} catch (Exception e) {
System.err.println("Wystąpił błąd!");
}
}
}
Utwórzmy jeszcze listę naszych robotów i dodajmy do niej 3 urządzenia:
iRobotRoomba,
iRobotRoomba Plus 405 Combo,
iRobotRoomba i5
i każdego użyjmy do odkurzania i mopowania domu:
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);
};
}
}
Po uruchomieniu przykładu dostaniemy:
IRobot Roomba odkurza
IRobot Roomba mopuje
IRobot Roomba Plus 405 Combo odkurza
IRobot Roomba Plus 405 Combo mopuje
IRobot Roomba I5 odkurza
Wystąpił błąd!
Model i5 nie ma funkcji mopowania. Użycie obiektu klasy I5 zamiast IRobotRoomba psuje działanie programu.
Poprawiona implementacja
Wydzielmy mopowanie z klasy bazową IRobotRoomba do oddzielnego interfejsu:
public class IRobotRoomba {
public void vacuum () {
System.out.println("IRobot Roomba odkurza");
}
}
public interface moppingDevice {
void mop ();
}
i zaimplementujmy go w 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 odkurza");
}
@Override
public void mop() {
System.out.println("IRobot Roomba Plus 405 Combo mopuje");
}
}
klasa I5 musi teraz zaimplementować tylko 1 metodę:
public class I5 extends IRobotRoomba {
final int height = 92;
@Override
public void vacuum() {
System.out.println("IRobot Roomba I5 odkurza");
}
}
Musimy jeszcze zmodyfikować typ parametru w klasie SmartHomeSystem. Mopowanie musi przyjmować obiekty implementujące interfejs MoppingDevice :
public class SmartHomeSystem {
public void vacuumHome(IRobotRoomba iRobot) {
iRobot.vacuum();
}
public void mopHome(MoppingDevice iRobot) {
iRobot.mop();
}
}
Tym razem musimy utworzyć 2 listy:
roboty zawierają wszystkie urządzenia typu IRobotRoomba,
ale urządzenia mopujące zawierają tylko modele implementujące moppingDevice czyli tylko iRobot Roomba Plus 405 Combo
i ponownie użyjmy ich do odkurzania i mopowania domu:
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);
};
}
}
Po uruchomieniu programu tym razem dostaniemy:
IRobot Roomba odkurza
IRobot Roomba Plus 405 Combo odkurza
IRobot Roomba I5 odkurza
IRobot Roomba Plus 405 Combo mopuje
Zmniejszyliśmy naszą klasę bazową i przenieśliśmy problematyczną metodę do interfejsu. Dzięki temu tylko model z funkcją mopowania musi ją zaimplementować. Możemy bezpiecznie zastępować obiekty klasy bazowej obiektami klas potomnych.
Dodatkowe zasady
Z zasady podstawienia Liskov wynikają dodatkowe 2 zasady:
Jeśli klasa bazowa zawiera jakieś warunki wstępne, to w klasach potomnych warunki wstępne nie mogą być wzmacniane. Można osłabiać warunki wstępne ponieważ to nie zepsuje programu. Jeśli w klasie bazowej warunki wstępne są trudniejsze do spełnienia, to program i tak będzie je musiał spełnić.
Jeśli klasa bazowa zawiera jakieś warunki końcowe, to w klasach potomnych warunki końcowe nie mogą być osłabiane. Można wzmacniać warunki końcowe ponieważ to nie zepsuje programu. Jeśli w klasie bazowej warunki końcowe są łatwiejsze do spełnienia, to program będzie na nie przygotowany.