Czym jest Zasada Podstawienia Liskov?
Zasada podstawienia Liskov, sformułowana przez Barbarę Liskov, mówi, że:
Obiekty klasy pochodnej powinny móc być używane zamiast obiektów klasy bazowej bez wpływu na poprawność programu.
Mówiąc prościej, jeśli masz klasę S, która jest podtypem klasy T, powinieneś móc bez problemu zastąpić każdy obiekt typu T obiektem typu S, a program nadal będzie działał poprawnie i zgodnie z oczekiwaniami. Klasa pochodna musi w pełni szanować “kontrakt” i zachowanie swojej klasy bazowej.
Formalna definicja brzmi:
Jeśli S jest podtypem T, to obiekty typu T mogą być zastąpione obiektami typu S bez naruszania poprawności programu.
Kluczowe Aspekty LSP
Aby w pełni zrozumieć tę zasadę, warto rozbić ją na kilka kluczowych założeń:
- Zgodność kontraktu: Klasa pochodna musi przestrzegać kontraktu zdefiniowanego przez klasę bazową. Obejmuje to sygnatury metod, typy parametrów, typy zwracane oraz oczekiwania dotyczące działania.
- Zachowanie semantyki: Podklasa nie może zmieniać znaczenia (semantyki) metod dziedziczonych w sposób, który jest sprzeczny z oczekiwaniami. Jeśli metoda w klasie bazowej dodaje dwa elementy, metoda w klasie pochodnej nie powinna ich odejmować.
- Brak konieczności sprawdzania typu: Kod, który korzysta z obiektów klasy bazowej, nie powinien potrzebować instrukcji
ifsprawdzających, czy ma do czynienia z konkretną klasą pochodną, aby uniknąć błędu. Samo to jest sygnałem, że LSP jest łamane. - Warunki wstępne (Preconditions): Warunki, które muszą być spełnione przed wywołaniem metody, nie mogą być wzmocnione w klasie pochodnej. Podklasa musi akceptować co najmniej te same dane wejściowe co klasa bazowa.
- Warunki końcowe (Postconditions): Warunki, które są prawdziwe po zakończeniu działania metody, nie mogą być osłabione w klasie pochodnej. Podklasa musi gwarantować co najmniej takie same (lub silniejsze) rezultaty jak klasa bazowa.
Przykład Naruszenia Zasady Podstawienia Liskov
Zobaczmy, jak łatwo można złamać LSP w praktyce. Wyobraźmy sobie prosty system HR, w którym zarządzamy pracownikami. Mamy abstrakcyjną klasę Employee oraz jej konkretne implementacje: Ceo i Programmer.
Diagram UML

Kod Naruszający Zasadę
Nasza bazowa klasa Employee definiuje dwie metody: calculateSalary() do obliczania pensji i showIdCard() do wyświetlania identyfikatora.
public abstract class Employee {
public void calculateSalary() {
System.out.println("Calculate for Employee");
}
public void showIdCard() {
System.out.println("Show card Employee");
}
}
public class Ceo extends Employee {
@Override
public void calculateSalary() {
System.out.println("Calculate for Ceo");
}
@Override
public void showIdCard() {
System.out.println("Show card Ceo");
}
}
public class Programmer extends Employee {
@Override
public void calculateSalary() {
System.out.println("Calculate for Programmer");
}
@Override
public void showIdCard() {
System.out.println("Show card Programmer");
}
}Mamy też klasę Finances, która odpowiada za wypłacanie pensji wszystkim pracownikom.
public class Finances {
public void paySalaries(List<Employee> employees) {
employees.forEach(employee -> employee.calculateSalary());
}
}Na pierwszy rzut oka wszystko wygląda dobrze. Klasa Finances może operować na liście obiektów Employee, nie martwiąc się o to, czy jest to Ceo, czy Programmer.
Problem: Pojawia się Stażysta
Teraz do firmy dołącza stażysta (Intern). Stażyści są pracownikami, więc naturalnym wydaje się, że klasa Intern powinna dziedziczyć po Employee. Jest jednak jeden haczyk: stażyści nie otrzymują wynagrodzenia.
public class Intern extends Employee {
@Override
public void calculateSalary() {
// Stażysta nie otrzymuje wynagrodzenia, więc metoda jest pusta
// lub rzuca wyjątek, np. UnsupportedOperationException
}
@Override
public void showIdCard() {
System.out.println("Show card Intern");
}
}Gdy teraz przekażemy listę pracowników zawierającą obiekt Intern do metody paySalaries, program nie zadziała zgodnie z oczekiwaniami. Metoda calculateSalary() dla stażysty nic nie robi, co narusza niepisany kontrakt: każdy Employee powinien mieć obliczaną pensję.
Dlaczego to narusza LSP?
- Niezgodność kontraktu: Klasa
Employeeniejawnie zakłada, że każda jej implementacja wykonuje logikę obliczania pensji. Pusta implementacja wInternłamie to założenie. - Nieoczekiwane zachowanie: Klasa
Financesoczekuje, że wywołaniecalculateSalary()zawsze przyniesie sensowny rezultat. W przypadku stażysty tak się nie dzieje, co może prowadzić do cichych błędów w systemie. - Potrzeba sprawdzania typu: Aby obejść ten problem, moglibyśmy zmodyfikować klasę
Financesw ten sposób:if (!(employee instanceof Intern)) { employee.calculateSalary(); }. To jednak jawny znak, że nasza abstrakcja jest wadliwa i łamie LSP.
Poprawne Zastosowanie Zasady Podstawienia Liskov
Aby naprawić nasz projekt, musimy przemyśleć hierarchię klas. Problem polega na tym, że założyliśmy, iż każdy pracownik otrzymuje wynagrodzenie. Zamiast tego powinniśmy oddzielić koncept bycia pracownikiem od konceptu otrzymywania pensji.
W tym celu tworzymy interfejs Payable, który będzie implementowany tylko przez te klasy, które faktycznie otrzymują wynagrodzenie.
Diagram UML (po poprawie)

Poprawiony Kod
- Tworzymy interfejs
Payable:
public interface Payable {
void calculateSalary();
}- Modyfikujemy hierarchię klas:
Employeepozostaje klasą bazową dla wszystkich pracowników, ale nie zawiera już metodycalculateSalary(). Tę metodę implementują tylko te klasy, które implementują interfejsPayable.
public abstract class Employee {
public void showIdCard() {
System.out.println("Show card Employee");
}
}
public class Ceo extends Employee implements Payable {
@Override
public void calculateSalary() {
System.out.println("Calculate for Ceo");
}
@Override
public void showIdCard() {
System.out.println("Show card Ceo");
}
}
public class Programmer extends Employee implements Payable {
@Override
public void calculateSalary() {
System.out.println("Calculate for Programmer");
}
@Override
public void showIdCard() {
System.out.println("Show card Programmer");
}
}
// Intern nie implementuje Payable, bo nie otrzymuje wynagrodzenia
public class Intern extends Employee {
@Override
public void showIdCard() {
System.out.println("Show card Intern");
}
}- Aktualizujemy klasę
Finances: Teraz operuje ona na liście obiektów typuPayable, a nieEmployee. Dzięki temu mamy pewność, że każdy obiekt na liście posiada metodęcalculateSalary()i jej wywołanie jest zasadne.
public class Finances {
public void paySalaries(List<Payable> employees) {
employees.forEach(employee -> employee.calculateSalary());
}
}Teraz nasz kod jest zgodny z LSP. Obiekty Ceo i Programmer mogą być używane wszędzie tam, gdzie oczekiwany jest obiekt Payable. Obiekt Intern nie jest podtypem Payable, więc nie może być przypadkowo użyty w kontekście wypłaty wynagrodzeń, co eliminuje błąd.
Porównanie Przykładów
| Aspekt | Wersja Naruszająca LSP | Wersja Zgodna z LSP |
| Hierarchia | Intern dziedziczy po Employee i ma pustą metodę calculateSalary(). | Intern dziedziczy tylko po Employee. Metoda calculateSalary() pochodzi z interfejsu Payable. |
Klasa Finances | Operuje na List<Employee>, co prowadzi do błędnego założenia. | Operuje na List<Payable>, co gwarantuje poprawność operacji. |
| Niezawodność | Niska. Dodanie nowego typu pracownika bez pensji psuje logikę. | Wysoka. System jest elastyczny i odporny na błędy wynikające z niepoprawnej hierarchii. |
| Konieczność sprawdzania typu | Potencjalnie tak (if instanceof). | Nie. Polimorfizm działa zgodnie z przeznaczeniem. |
Podsumowanie
Zasada Podstawienia Liskov jest kluczowa dla tworzenia elastycznych i niezawodnych hierarchii klas. Jej naruszenie prowadzi do kodu, który jest mylący, trudny w utrzymaniu i podatny na błędy. Poprzez staranne projektowanie abstrakcji i upewnienie się, że klasy pochodne w pełni respektują kontrakt swoich klas bazowych, tworzymy systemy, które są łatwiejsze do rozbudowy i bardziej przewidywalne w działaniu. Pamiętaj: dziedziczenie powinno modelować relację “jest pewnym rodzajem” (is-a) nie tylko na poziomie atrybutów, ale przede wszystkim na poziomie zachowania.
Bibliografia
- Liskov, B. (1988). Data Abstraction and Hierarchy. SIGPLAN Notices, 23(5).
- Martin, R. C. (2003). Agile Software Development, Principles, Patterns, and Practices. Prentice Hall.
- Martin, R. C. (2008). Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall.