Zasada substytucji Barbary Liskov jest jednym z podstawowych założeń programowania obiektowego oraz jedną z pięciu zasad SOLID (więcej informacji o pozostałych dobrych praktykach programowania SOLID, znajdziesz tutaj). Mówi ona o tym, że w klasie dziedziczącej powinno być możliwe zastąpienie obiektu klasy bazowej obiektem klasy dziedziczącej, bez wpływu na poprawność działania programu.
Przykładem może być klasa Zwierzę
i jej podklasy Kot
i Pies
. Załóżmy, że istnieje metoda o nazwie dajGłos
, która zwraca dźwięk, jaki wydaje zwierzę. Zgodnie z zasadą Liskov, metoda ta powinna działać poprawnie zarówno dla obiektów klasy bazowej (Zwierzę
), jak i jej podtypów (Kot
i Pies
). Oznacza to, że jeśli zastąpimy obiekt klasy bazowej obiektem podtypu, to nadal otrzymamy poprawny wynik.
Przykładowo, jeśli mamy klasę Zwierzę
z metodą dajGłos
, która zwraca ciąg znaków „Nieznany dźwięk”, oraz klasy Kot
i Pies
dziedziczące po klasie Zwierzę
i posiadające swoje implementacje tej metody, to powinno być możliwe zastąpienie obiektu typu Zwierzę
obiektem typu Kot
lub Pies
bez wpływu na poprawność działania programu. Innymi słowy, wywołanie metody dajGłos
na obiekcie typu Kot
powinno zwrócić ciąg znaków reprezentujący dźwięk kotka, a wywołanie tej samej metody na obiekcie typu Pies
powinno zwrócić ciąg znaków reprezentujący dźwięk psa.
Przykład w PHP:
class Zwierze { public function dajGlos(): string { return "Nieznany dźwięk"; } } class Kot extends Zwierze { public function dajGlos(): string { return "Miau"; } } class Pies extends Zwierze { public function dajGlos(): string { return "Hau hau"; } } function zrobDzwiek(Zwierze $zwierze) { echo $zwierze->dajGlos(); } $zwierze1 = new Kot(); $zwierze2 = new Pies(); zrobDzwiek($zwierze1); // wynik: Miau zrobDzwiek($zwierze2); // wynik: Hau hau
W powyższym przykładzie mamy klasę bazową Zwierze
z metodą dajGlos()
, która zwraca „Nieznany dźwięk”. Klasa Kot
i Pies
dziedziczą po klasie Zwierze
i mają swoje implementacje metody dajGlos()
, zwracające odpowiednio „Miau” i „Hau hau”. Funkcja zrobDzwiek()
przyjmuje obiekt typu Zwierze
i wywołuje na nim metodę dajGlos()
. Następnie tworzymy obiekty Kot
i Pies
i przekazujemy je do funkcji zrobDzwiek()
. Dzięki zasadzie Liskov, metoda dajGlos()
zachowuje się poprawnie dla obiektów typu Kot
i Pies
, ponieważ są one podtypami klasy Zwierze
.
Inny przykład zastosowania zasady substytucji Liskov w PHP:
<?php class Rectangle { protected $width; protected $height; public function setWidth($width) { $this->width = $width; } public function setHeight($height) { $this->height = $height; } public function getArea() { return $this->width * $this->height; } } class Square extends Rectangle { public function setWidth($width) { $this->width = $width; $this->height = $width; } public function setHeight($height) { $this->width = $height; $this->height = $height; } } function printArea(Rectangle $rectangle) { $rectangle->setWidth(4); $rectangle->setHeight(5); return "Area: " . $rectangle->getArea() . "\n"; } printArea(new Rectangle()); // Output: Area: 20 printArea(new Square()); // Output: Area: 25
W tym przykładzie mamy klasę Rectangle
, która reprezentuje prostokąt o określonej szerokości i wysokości. Klasa Square
dziedziczy po klasie Rectangle
i reprezentuje kwadrat, czyli specjalny rodzaj prostokąta, w którym wszystkie boki są równe.
Metody setWidth
i setHeight
w klasie Square
przysłaniają metody o takich samych nazwach w klasie Rectangle
. Jednakże, w klasie Square
, ustawienie jednej z wymiarów powoduje ustawienie również drugiego wymiaru na tę samą wartość, ponieważ kwadrat musi mieć boki równe.
Funkcja printArea
przyjmuje obiekt klasy Rectangle
i wywołuje na nim metody setWidth
, setHeight
i getArea
. Powinna działać dla dowolnego prostokąta lub kwadratu, ponieważ obie te klasy są typami Rectangle
. Jednakże, wyniki dla Rectangle
i Square
są różne, co oznacza, że klasa Square
nie spełnia zasady substytucji Liskov. Aby to poprawić, można by stworzyć interfejs Shape
, z którego będą dziedziczyć zarówno Rectangle
, jak i Square
.
Oto przykładowa definicja interfejsu Shape
, który może zostać wykorzystany do poprawy zasady substytucji Liskov w poprzednim przykładzie:
interface Shape { public function setWidth($width); public function setHeight($height); public function getArea(); }
Ten interfejs definiuje trzy metody: setWidth
, setHeight
i getArea
, które powinny być zaimplementowane przez każdą klasę, która dziedziczy po Shape
. Dzięki temu można upewnić się, że każda klasa, która implementuje ten interfejs, będzie działać zgodnie z oczekiwaniami, bez względu na to, czy to będzie Rectangle
czy Square
.
W przykładzie wcześniej, można by zmienić definicję printArea
tak, aby przyjmowała obiekt typu Shape
zamiast Rectangle
. W ten sposób można by używać tej funkcji dla dowolnego obiektu, który implementuje interfejs Shape
, co zapewniłoby spełnienie zasady substytucji Liskov.
Korzyści stosowania zasady Liskov
- Zapewnienie poprawności i niezawodności programów – dzięki zasadzie Liskov możemy być pewni, że podstawienie jednego obiektu typu bazowego przez obiekt typu pochodnego nie wpłynie na poprawność działania programu. To przyczynia się do zapewnienia niezawodności i poprawności programu.
- Ułatwienie rozszerzania aplikacji – poprawna implementacja zasady Liskov umożliwia łatwe rozszerzanie aplikacji przez dodawanie nowych klas pochodnych i nadpisywanie ich metod. To ułatwia rozwijanie aplikacji, umożliwiając programistom dodawanie nowych funkcjonalności bez wpływu na już istniejący kod.
- Zwiększenie czytelności kodu – zasada Liskov wymaga, aby klasy pochodne działały w sposób spójny z klasą bazową, co prowadzi do bardziej czytelnego kodu i ułatwia jego zrozumienie.
- Ułatwienie testowania aplikacji – poprawne zastosowanie zasady Liskov umożliwia łatwiejsze i bardziej efektywne testowanie aplikacji. Dzięki zasadzie Liskov można przeprowadzać testy jednostkowe na klasach bazowych, a następnie przetestować klasę pochodną, korzystając z tych samych testów.
- Ułatwienie pracy zespołowej – zasada Liskov umożliwia programistom pracę w zespole nad wieloma klasami i obiektami, bez obawy o wpływ na działanie aplikacji. To przyczynia się do zwiększenia efektywności pracy i ułatwia współpracę w zespole.
Zasada Liskov jest jednym z fundamentalnych założeń programowania obiektowego i jej poprawne zastosowanie, tak jak widać powyżej przynosi wiele korzyści.
Niemniej jednak, jej niewłaściwe stosowanie lub nieprzestrzeganie może prowadzić do pewnych problemów, takich jak:
- Skomplikowanie kodu – zasada Liskov wymaga, aby klasy pochodne działały w sposób spójny z klasą bazową, co może prowadzić do dodatkowego kodu i skomplikowania kodu, zwłaszcza w przypadku bardziej złożonych systemów.
- Trudności w utrzymaniu – kiedy klasy pochodne nie są poprawnie zaprojektowane, utrzymanie kodu może być trudniejsze i czasochłonne, zwłaszcza w przypadku bardziej złożonych systemów.
- Trudności w implementacji – zasada Liskov wymaga, aby klasy pochodne działały w sposób spójny z klasą bazową, co może utrudnić implementację nowych klas pochodnych lub zmiany istniejących klas.
- Złożoność testowania – w przypadku niepoprawnego zastosowania zasady Liskov testowanie aplikacji może być trudne i czasochłonne, ponieważ wymaga dodatkowych testów, aby upewnić się, że klasa pochodna działa poprawnie.
W sumie, stosowanie zasady Liskov wymaga starannego projektowania i implementacji klas pochodnych, co może być trudne i czasochłonne, ale jest ważnym elementem programowania obiektowego i przyczynia się do poprawy jakości i niezawodności aplikacji.
[…] dla klientów HTTP w języku PHP. Celem jest umożliwienie zastępowania klientów HTTP zgodnie z zasadą zastępowania Liskov . Oznacza to, że wszyscy klienci MUSZĄ zachowywać się w ten sam sposób podczas wysyłania […]
[…] – Liskov Substitution Principle (Zasada podstawienia Liskov) – obiekty klasy bazowej powinny być zastępowalne przez obiekty klas […]