Liskov Substitution Principle

Liskov Substitution Principle – LSP

Ta zasada mówi, że obiekty w programie powinny być zastępowalne instancjami ich podklas bez wpływu na prawidłowość działania programu.

Innymi słowy, jeśli klasa B jest podklasą klasy A, to powinniśmy być w stanie zastąpić obiekt klasy A obiektem klasy B, a program powinien działać poprawnie.

Korzyści z zastosowania zasady LSP:

  1. Stabilność kodu: Podklasy mogą zastępować klasy bazowe bez ryzyka błędów, co czyni kod bardziej niezawodnym.
  2. Łatwiejsze testowanie: Można testować podklasy niezależnie, co upraszcza proces testowania.
  3. Elastyczność i rozszerzalność: Kod jest łatwiejszy do rozszerzania bez modyfikacji istniejących klas.
  4. Czytelność i zrozumiałość: Klasy mają jasno zdefiniowane role, co ułatwia zrozumienie kodu.
  5. Unikanie błędów: Zapobiega niespójnościom w zachowaniu podklas, które mogłyby prowadzić do trudnych do wykrycia błędów.

Przykład z życia codziennego:

Klasa bazowa: Urządzenie do komunikacji

  • Każde urządzenie do komunikacji pozwala na prowadzenie rozmów telefonicznych.

Podklasy: Telefon stacjonarny i Smartfon

  • Telefon stacjonarny: Umożliwia rozmowy telefoniczne, ale tylko w jednym miejscu.
  • Smartfon: Umożliwia rozmowy telefoniczne, ale także inne funkcje, takie jak SMS-y, aplikacje, internet.

Zasada Liskov w działaniu

Zgodnie z zasadą Liskov, Telefon stacjonarny i Smartfon mogą być używane zamiennie w miejscach, gdzie oczekuje się urządzenia do komunikacji, ponieważ oba pozwalają na wykonywanie rozmów telefonicznych.

Przykład łamania zasady Liskov

Załóżmy, że ktoś tworzy nową podklasę Walkie-Talkie, która dziedziczy po klasie Urządzenie do komunikacji. Walkie-Talkie umożliwia komunikację, ale działa na zupełnie innych zasadach (ma ograniczony zasięg, nie działa przez sieć telefoniczną).

Jeśli spróbujemy użyć Walkie-Talkie w kontekście, gdzie oczekujemy standardowego urządzenia telefonicznego, które może połączyć się z dowolnym numerem, system nie będzie działać prawidłowo. Na przykład, funkcja wybierania numeru czy historia połączeń nie ma sensu w przypadku Walkie-Talkie.

To łamie zasadę Liskov, ponieważ Walkie-Talkie nie może być używane zamiennie z innymi urządzeniami do komunikacji telefonicznej, co prowadzi do niespójności i problemów w systemie.

Przykład programu przed użyciem zasady LSP:

Załóżmy, że mamy klasy Kaczka i GumowaKaczka, gdzie GumowaKaczka dziedziczy po Kaczka. Klasa Kaczka ma metodę latanie(), ale GumowaKaczka nie jest w stanie latać, choć dziedziczy tę metodę.

class Kaczka {
    public void latanie() {
        System.out.println("Kaczka lata.");
    }
    
    public void kwakanie() {
        System.out.println("Kaczka kwacze.");
    }
}

class GumowaKaczka extends Kaczka {
    @Override
    public void latanie() {
        // Gumowa kaczka nie potrafi latać
        throw new UnsupportedOperationException("Gumowa kaczka nie lata.");
    }
    
    @Override
    public void kwakanie() {
        System.out.println("Gumowa kaczka nie kwacze, tylko piszczy.");
    }
}

public class Main {
    public static void main(String[] args) {
        Kaczka kaczka = new Kaczka();
        kaczka.latanie(); // Outputs: Kaczka lata.
        kaczka.kwakanie(); // Outputs: Kaczka kwacze.

        Kaczka gumowaKaczka = new GumowaKaczka();
        gumowaKaczka.latanie(); // Throws UnsupportedOperationException
        gumowaKaczka.kwakanie(); // Outputs: Gumowa kaczka nie kwacze, tylko piszczy.
    }
}

Co się tutaj dzieje?

W tym przykładzie GumowaKaczka dziedziczy po Kaczka, ale implementuje metodę latanie() w sposób, który łamie zasady Liskov, ponieważ gumowa kaczka nie jest w stanie latać. Wywołanie metody latanie() na obiekcie GumowaKaczka prowadzi do wyjątku, co jest niezgodne z oczekiwaniami wobec obiektu klasy Kaczka.

Poprawiony kod zgodny z zasadą LSP

interface Kwakanie {
    void kwakanie();
}

interface Latanie {
    void latanie();
}

class Kaczka implements Kwakanie, Latanie {
    @Override
    public void latanie() {
        System.out.println("Kaczka lata.");
    }
    
    @Override
    public void kwakanie() {
        System.out.println("Kaczka kwacze.");
    }
}

class GumowaKaczka implements Kwakanie {
    @Override
    public void kwakanie() {
        System.out.println("Gumowa kaczka piszczy.");
    }
}

public class Main {
    public static void main(String[] args) {
        Latanie kaczka = new Kaczka();
        kaczka.latanie(); // Outputs: Kaczka lata.

        Kwakanie gumowaKaczka = new GumowaKaczka();
        gumowaKaczka.kwakanie(); // Outputs: Gumowa kaczka piszczy.
    }
}

Co się zmieniło?

W tym poprawionym przykładzie:

  • Klasa Kaczka implementuje oba interfejsy Kwakanie i Latanie, co oznacza, że może kwakać i latać.
  • Klasa GumowaKaczka implementuje tylko interfejs Kwakanie, ponieważ gumowa kaczka nie lata, ale może kwakać (czy właściwie, piszczeć).

Zalety podejścia

  • Obiekty Kaczka i GumowaKaczka są używane zgodnie z ich specyfikacjami. Kaczka wspiera latanie i kwakanie, natomiast GumowaKaczka wspiera tylko kwakanie.
  • Interfejsy pomagają oddzielić odpowiedzialności i uniknąć problemów związanych z dziedziczeniem metod, które nie są adekwatne dla danej klasy.
Scroll to Top