Zasada podstawienia Liskov (ang. LSP – Liskov substitution principle) mówi w skrócie, że klasa dziedzicząca musi móc zastąpić klasę rodzica bez wywołania błędu w programie.
Żeby zasada LSP była spełniona należy ponadto przestrzegać 2 poniższych reguł:
1. Preconditions w klasie dziedziczącej (podtypie) nie mogą być bardziej rygorystyczne, niż u rodzica (zastosowanie „dziecka” zamiast „rodzica” spowoduje błąd, np. warunek na imię z dużej litery u dziecka, kiedy u rodzica nie ma takiego warunku).
2. Postconditions w klasie dziedziczącej (podtypie) nie mogą być słabsze, niż u rodzica (np. błąd przez zwracanie 0 u dziecka, kiedy rodzic nie zakłada takiej możliwości).
Preconditions określają, co musi być spełnione, aby metoda działała poprawnie. Postconditions opisują, co jest prawdą po wywołaniu metody, w tym co jest zwracane (zakładając, że zostały spełnione preconditions).
Przykład programu przed zastosowaniem zasady LSP:
class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int calculateArea() {
return width * height;
}
}
class Square extends Rectangle {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height) {
super.setWidth(height);
super.setHeight(height);
}
}
W tym przykładzie mamy klasę Rectangle i dziedziczącą z niej klasę Square. Niestety, łamie to zasadę Liskov, ponieważ metody setWidth i setHeight w klasie Square zmieniają zachowanie klasy bazowej Rectangle.
Przykład programu po zastosowaniem zasady LSP:
class Shape {
// Metoda do obliczania pola powierzchni, która będzie zaimplementowana w klasach pochodnych
public abstract int calculateArea();
}
class Rectangle extends Shape {
protected int width;
protected int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int calculateArea() {
return width * height;
}
}
class Square extends Shape {
private int side;
public Square(int side) {
this.side = side;
}
@Override
public int calculateArea() {
return side * side;
}
}
W tym przypadku Square nie dziedziczy już bezpośrednio z Rectangle. Oba kształty dzielą teraz wspólną klasę bazową Shape. Oba kształty posiadają teraz swoje specyficzne metody (setSide dla kwadratu), ale obie implementują wspólny interfejs Shape. To spełnia zasadę Liskov, ponieważ teraz obiekt Square można używać zamiennie z obiektem Rectangle bez obawy o złamanie oczekiwań (np. zmiana szerokości wpływająca na wysokość).
