Liskov Substitution Principle

(Zasada podstawienia Liskov)

Jeżeli klasa S (podtyp) jest dzieckiem klasy T (typ), wówczas obiekty typu T można zastąpić obiektami typu S bez zakłócania działania programu.

Barbara Liskov

Czyli po podstawieniu klasy dziecka w miejscu klasy rodzica nasz program powinien zadziałać bez zakłóceń. Klasa dziedzicząca powinna tylko rozszerzać możliwości klasy bazowej, ale nie wolno jej nadpisywać metod klasy bazowej.

</> Lepszy przykład niż wykład

✘ Błędny kod

W klasie Bird utworzymy metodę takeCareOffsping() odpowiedzialną za opiekę nad potomstwem. Każde potomstwo musi najpierw przyjść na świat, więc tym zajmuje się nasza klasa bazowa.

public class Bird {
    public void takeCareOffspring() {
        System.out.println("Wydaję na świat potomostwo.");
    }
}

Jeśli w klasach Faclon i Penguin dziedziczących po Bird nadpiszemy metodę takeCareOffsping()  o elementy opieki charakterystyczne dla tych gatunków ptaków, zostanie naruszona zasada LSP.

public class Falcon extends Bird {
    public void takeCareOffspring() {
        System.out.println("Uczę moje potomstwo latać.");
    }
}

public class Penguin extends Bird {
    public void takeCareOffspring() {
        System.out.println("Uczę moje potomstwo pływać.");
    }
}

Gdy wywołamy metodę takeCareOffsping() na obiekach klas dziedziczących

public static void main(String[] args) {
    Bird bird = new Bird();
    Penguin penguin = new Penguin();
    Falcon falcon = new Falcon();

    System.out.println("BIRD");
    bird.takeCareOffspring();
    System.out.println("PENGUIN");
    penguin.takeCareOffspring();
    System.out.println("FALCON");
    falcon.takeCareOffspring();
}

okaże się że mamy uczyć pływania i latania potomstwo, które nie przyszło jeszcze na świat.

BIRD
Wydaję na świat potomstwo.
PENGUIN
Uczę moje potomstwo pływać.
FALCON
Uczę moje potomstwo latać.
✔ Prawidłowy kod

W klasach dziedziczących Faclon i Penguin wywołamy metodę klasy bazowej Bird i ją rozszerzymy. Dzięki temu unikniemy naruszenia zasady LSP i program zachowa się prawidłowo.

public class Falcon extends Bird {
    public void takeCareOffspring() {
        super.takeCareOffspring();
        System.out.println("Uczę moje potomstwo latać.");
    }
}

public class Penguin extends Bird {
    public void takeCareOffspring() {
        super.takeCareOffspring();
        System.out.println("Uczę moje potomstwo pływać.");
    }
}

Każdy gatunek wyda potomstwo a następnie zaopiekuje się nim stosownie do swojej specyfiki.

BIRD
Wydaję na świat potomstwo.
PENGUIN
Wydaję na świat potomstwo.
Uczę moje potomstwo pływać.
FALCON
Wydaję na świat potomstwo.
Uczę moje potomstwo latać.

Bardziej subtelne naruszenia zasady Liskov są związane z argumentami na wejściu i wyjściu.

Warunki wstępne

warunek wstępny – warunek, który jest sprawdzany na początku metody. Mówi jakie warunki muszą nastąpić, aby metoda zadziałała prawidłowo.

</> Lepszy przykład niż wykład

✘ Błędny kod

W klasie Mammal funkcja takeCareOffspring(String speicesName, int Age, boolean isHealthy) przyjmuje jako argumenty nazwę gatunku ssaka, jego wiek i stan zdrowia. Następnie oblicza pozostały czas opieki nad potostwem (careTime ) odejmując aktualny wiek dziecka od wieku dorosłego (przyjmujemy,  że ssaki osiągają dorosłość po 12 miesiącach życia).
Nasz warunek wstępny zabezpiecza nas przed otrzymaniem pustej nazwy gatunku.

public class Mammal {
    public void takeCareOffspring(String speciesName, int Age, boolean isHealthy) {
        int Adult = 12;
        int careTime = Adult - Age;
        
        if (speciesName.isEmpty()) {
            throw new RuntimeException("Nazwa gatunku nie może być pusta!");
        }
                System.out.println("Opiekuję się potomostwem gatunku " + speciesName + " i zajmie mi to jeszcze " + careTime + " miesięcy.");
    }
}

Teraz w klasie dziedziczącej Lion wzmocnimy warunek wstępny wymaganiem aby nazwa gatunku zaczynała się z wielkiej litery, jak przystało na króla zwierząt.

public class Lion extends Mammal {
    @Override
    public void takeCareOffspring(String speciesName, int Age, boolean isHealthy) {
        int Adult = 12;
        int careTime = Adult - Age;
        
        if (speciesName.isEmpty() || Character.isLowerCase(speciesName.charAt(0))) {
            throw new RuntimeException("Nazwa gatunku nie może być pusta ani z małej litery!");
        }
        System.out.println("Opiekuję się potomostwem gatunku " + speciesName + " i zajmie mi to jeszcze " + careTime + " miesięcy.");
    }
}

Zgodnie z zasadą podstawienia Liskov powinniśmy móc zastąpić klasę rodzica Mammal klasą dziecka Lion.

public class Main {
    public static void main(String[] args) {        
        Mammal mammal = new Mammal();
        Lion lion = new Lion();

        mammal.takeCareOffspring("mammal", 11, true);
        lion.takeCareOffspring("lion", 3, true);
    }
}

Jednak po uruchomieniu program rzuca wyjątek, ponieważ wzmocniliśmy warunek wstępny o sprawdzenie wielkości pierwszej litery (a nazwę gatunku „lion” napisaliśmy z małej litery).

Opiekuję się potomostwem gatunku mammal i zajmie mi to jeszcze 1 miesięcy.
Exception in thread "main" java.lang.RuntimeException: Nazwa gatunku nie może być pusta ani z małej litery!
	at III_liskov_substitution.conditions.Lion.takeCareOffspring(Lion.java:12)
	at Main.main(Main.java:109)

Dlatego stosujemy regułę:
warunki wstępne nie mogą być wzmocnione!

Ale możemy warunki wstępne osłabiać:
warunek wstępny w klasie rodzica Mammal może być bardziej restrykcyjny niż w klasie dziecka Lion – czyli warunek wstępny zostanie osłabiony.

✔ Prawidłowy kod

public class Mammal {
    public void takeCareOffspring(String speciesName, int Age, boolean isHealthy) {
        int Adult = 12;
        int careTime = Adult - Age;        
                if (speciesName.isEmpty() || Character.isLowerCase(speciesName.charAt(0))) {
            throw new RuntimeException("Nazwa gatunku nie może być pusta ani z małej litery!");
        }        
        System.out.println("Opiekuję się potomstwem gatunku " + speciesName + " i zajmie mi to jeszcze " + careTime + " miesięcy.");
    }
}

public class Lion extends Mammal {
    @Override
    public void takeCareOffspring(String speciesName, int Age, boolean isHealthy) {
        int Adult = 12;
        int careTime = Adult - Age;
                if (speciesName.isEmpty()) {
            throw new RuntimeException("Nazwa gatunku nie może być pusta!");
        }
        System.out.println("Opiekuję się potomstwem gatunku " + speciesName + " i zajmie mi to jeszcze " + careTime + " miesięcy.");
    }
}

Warunki końcowe

warunek końcowy – warunek na końcu metody sprawdzający czy wywołanie metody powoduje błędy logiczne

</> Lepszy przykład niż wykład

✘ Błędny kod

W klasie Mammal dodamy warunek pilnujący, aby pozostały czas opieki nad potomstwem nie mógł przyjąć wartości ujemnej (to zabezpieczy nas przed opieką nad dorosłymi).

public class Mammal {
    public void takeCareOffspring(String speciesName, int Age, boolean isHealthy) {
        int Adult = 12;
        int careTime = Adult - Age;
             if (speciesName.isEmpty() || Character.isLowerCase(speciesName.charAt(0))) {
            throw new RuntimeException("Nazwa gatunku nie może być pusta ani z małej litery!");
        }
          if (careTime < 0) {
            throw new RuntimeException("Czas opieki nad potomstwem nie może być ujemny!");
        }

        System.out.println("Opiekuję się potomostwem gatunku " + speciesName + " i zajmie mi to jeszcze " + careTime + " miesięcy.");
    }
}

W klasie Lion dodamy warunek końcowy wydłużający o 6 miesięcy konieczność opieki nad dzieckiem, jeśli nie jest ono zdrowe.

public class Lion extends Mammal {
    @Override
    public void takeCareOffspring(String speciesName, int Age, boolean isHealthy) {
        int Adult = 12;
        int careTime = Adult - Age;
                if (speciesName.isEmpty()) {
            throw new RuntimeException("Nazwa gatunku nie może być pusta!");
        }
        
        if (!isHealthy) {
            careTime = Adult - (Age + 6);
        }

        System.out.println("Opiekuję się potomostwem gatunku " + speciesName + " i zajmie mi to jeszcze " + careTime + " miesięcy.");
    }
}

Po uruchomieniu programu możemy otrzymać ujemny pozostały czas  opieki nad potomstwem lwa, ponieważw klasie dziedziczącej Lion osłabiliśmy nieopatrznie warunek końcowy z klasy rodzica Mammal.

public class Main {
    public static void main(String[] args) {
        Mammal mammal = new Mammal();
        Lion lion = new Lion();

        mammal.takeCareOffspring("Mammal", 7, true);
        lion.takeCareOffspring("Lion", 7, false);
    }
}
Opiekuję się potomstwem gatunku Mammal i zajmie mi to jeszcze 5 miesięcy.
Opiekuję się potomstwem gatunku Lion i zajmie mi to jeszcze -1 miesięcy.

Dlatego stosujemy regułę:
warunki końcowe nie mogą być osłabione!

Ale możemy warunki końcowe wzmacniać, czyli w naszym przypadku dodatkowe pół roku na opiekę można ustawić w klasie rodzica Mammal .

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *