
„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