Teoria
Definicja zasady podstawienia Liskov mówi: Funkcje, które używają wskaźników lub referencji do klas bazowych, muszą być w stanie używać również obiektów klas dziedziczących po klasach bazowych, bez dokładnej znajomości tych obiektów. Tłumacząc to prostszy język, możemy powiedzieć, że w miejsce klasy bazowej powinniśmy mieć możliwość podstawienia dowolnej klasy dziedziczącej bez utraty poprawnego działania programu.
Z zasady tej wywodzą się dwie kolejne – warunki wstępne nie powinny być wzmacniane w typach pochodnych, a warunki końcowe nie powinny być w nich mniej restrykcyjne( należy zachować odpowiedni kierunek ich stopniowania).
Przykład
Rozważmy omawianą zasadę na podstawie popularnego przykładu z kaczkami. Poniższy kodu reprezentuje klasę kaczki, a w niej trzy metody odpowiedzialne za latanie/pływanie i chodzenie. Mamy również przedstawicieli tego gatunku w postaci kaczki krzyżówki oraz kaczki mandarynki, które po wspomnianej klasie dziedziczą.
public class Duck {
public void fly(){
System.out.println("I'm flying");
}
public void walk(){
System.out.println("I'm walking");
}
public void swim(){
System.out.println("I'm swimming");
}
}
public class MallardDuck extends Duck{
@Override
public void fly() {
System.out.println("I'm flying mallard duck");
}
@Override
public void walk() {
System.out.println("I'm walking mallard duck");;
}
@Override
public void swim() {
System.out.println("I'm swimming mallard duck");;
}
}
public class MandarinDuck extends Duck{
@Override
public void fly() {
System.out.println("I'm flying mandarin duck");
}
@Override
public void walk() {
System.out.println("I'm walking mandarin duck");;
}
@Override
public void swim() {
System.out.println("I'm swimming mandarin duck");;
}
}
Taka konstrukcja kodu spełnia reguły podstawienia Liskov . Możemy stworzyć instancję (obiekt) klasy MandarinDuck lub MallardDuck i wywoływać na nich metody bez podawania konkretnego podtypu, nie ma możliwości, aby program został przez nas uszkodzony. Zastanówmy się jednak, co w momencie, jeśli chcielibyśmy dodać do grona klas dziedziczących po Duck kaczkę gumową? To nadal jest kaczka, jednak nie umie ona latać ani chodzić.
public class RubberDuck extends Duck{
@Override
public void fly() {
//???
}
@Override
public void walk() {
//???
}
@Override
public void swim() {
System.out.println("I'm swimming rubber duck");
}
}Po wprowadzeniu odpowiednich zmian umożliwiających implementację gumowej kaczki, kod przestaje spełniać omawianą w artykule zasadę (dodatkowo łamie SRP i OCP). Rozwiązaniem tego problemu byłoby wprowadzenie dodatkowych klas jak SwimmingDuck oraz FlyingDuck i odpowiednie zrestrukturyzowanie dziedziczenia. Innym rozwiązaniem jest usunięcie bazowej klasy Duck i dodanie do programu interfejsów odpowiedzialnych za poszczególne czynności, a następnie „składanie” kaczek z odpowiednich metod ( ten sposób przedstawiony poniżej).
public interface Flying {
void fly();
}
public interface Walking {
void walk();
}
public interface Swimming {
void swim();
}
public class MallardDuck implements Flying, Walking, Swimming{
@Override
public void fly() {
System.out.println("I'm flying mallard duck");
}
@Override
public void walk() {
System.out.println("I'm walking mallard duck");;
}
@Override
public void swim() {
System.out.println("I'm swimming mallard duck");;
}
}
public class MandarinDuck implements Flying, Walking, Swimming{
@Override
public void fly() {
System.out.println("I'm flying mandarin duck");
}
@Override
public void walk() {
System.out.println("I'm walking mandarin duck");;
}
@Override
public void swim() {
System.out.println("I'm swimming mandarin duck");;
}
}
public class RubberDuck implements Swimming{
@Override
public void swim() {
System.out.println("I'm swimming rubber duck");
}
}Warto też mieć na uwadze, że słabym rozwiązaniem jest również pozostawienie problematycznej metody pustej, czy wyrzucenie błędu, bo to również narusza zasadę podstawienia Liskov.