„Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.”
— Barbara Liskov

Co oznacza LSP?

Zasada podstawienia Liskov mówi, że jeśli klasa B dziedziczy po klasie A, to powinna zachowywać się jak A. A dokładniej: można użyć obiektu B wszędzie tam, gdzie oczekujemy A, i nic nie powinno się zepsuć.

W praktyce chodzi o to, żeby nie tworzyć klas potomnych, które łamią logikę klasy bazowej, czyli:

❌ rzucają wyjątki w sytuacjach, które dla klasy nadrzędnej są normalne,
❌ zmieniają znaczenie metod,
❌ ignorują oczekiwane zachowanie interfejsu.

Przykład łamania LSP – kwadrat jako prostokąt?

To klasyczny przykład. Załóżmy, że mamy klasę Prostokat, a ktoś wpada na pomysł, że kwadrat to też prostokąt, więc zrobimy dziedziczenie:

public class Prostokat {
    protected int szerokosc;
    protected int wysokosc;

    public void ustawSzerokosc(int szerokosc) {
        this.szerokosc = szerokosc;
    }

    public void ustawWysokosc(int wysokosc) {
        this.wysokosc = wysokosc;
    }

    public int obliczPole() {
        return szerokosc * wysokosc;
    }
}
public class Kwadrat extends Prostokat {
    @Override
    public void ustawSzerokosc(int szerokosc) {
        this.szerokosc = szerokosc;
        this.wysokosc = szerokosc;
    }

    @Override
    public void ustawWysokosc(int wysokosc) {
        this.szerokosc = wysokosc;
        this.wysokosc = wysokosc;
    }
}

Wygląda logicznie? Teoretycznie tak. Ale jeśli użyjemy Kwadrat zamiast Prostokat:

Prostokat p = new Kwadrat();
p.ustawSzerokosc(5);
p.ustawWysokosc(10);
System.out.println(p.obliczPole());
// Spodziewamy się 50, ale dostajemy 100!

Błąd! Klasa Kwadrat łamie kontrakt klasy Prostokat. Programista oczekuje niezależnej zmiany szerokości i wysokości – a kwadrat to uniemożliwia.

Przestrzeganie LSP – inne podejście

Zamiast na siłę dziedziczyć, lepiej użyć wspólnego interfejsu lub klasy abstrakcyjnej:

public interface Figura {
    int obliczPole();
}
public class Prostokat implements Figura {
    private int szerokosc;
    private int wysokosc;

    public Prostokat(int szerokosc, int wysokosc) {
        this.szerokosc = szerokosc;
        this.wysokosc = wysokosc;
    }

    public int obliczPole() {
        return szerokosc * wysokosc;
    }
}
public class Kwadrat implements Figura {
    private int bok;

    public Kwadrat(int bok) {
        this.bok = bok;
    }

    public int obliczPole() {
        return bok * bok;
    }
}

Teraz obie figury działają zgodnie ze swoją naturą, i nie zaskakują:

Figura f1 = new Prostokat(5, 10);
Figura f2 = new Kwadrat(5);
System.out.println(f1.obliczPole()); // 50
System.out.println(f2.obliczPole()); // 25

Korzyści z LSP

  • Bezpieczeństwo zamiany – podmieniasz klasę i nic się nie sypie,
  • Czytelność kontraktu – obiekty robią dokładnie to, czego oczekujesz,
  • Mniej ifów i wyjątków – nie musisz sprawdzać, z czym masz do czynienia,
  • Lepsze projektowanie dziedziczenia – unikasz pułapek typu „dziedziczenie na siłę”.

Podsumowanie

Zasada Liskov może brzmieć akademicko, ale w praktyce to zdrowy rozsądek: jeśli ktoś podaje Ci zamiennik, oczekujesz, że zadziała tak samo, a nie wybuchnie. Tak samo w kodzie – dziedziczenie to obietnica zachowania się jak rodzic. Nie łam tej obietnicy.


Dodaj komentarz

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