SOLID 3

Zasada jednej odpowiedzialności

Zasada podstawienia Liskov – trzecia zasada SOLID.

W programowaniu obiektowym występuje wiele reguł i metod pisania, aby uzyskać kod jak najbardziej czytelny i zrozumiały dla innego programisty. Jedną z 5 zasad  wymyślonych przez Roberta C. Martina jest zasada podstawienia Liskov, która należy do 5 założeń SOLID. Zasada ta mówi, że: funkcje które używają wskaźników lub referencji do klas bazowych, muszą być w stanie używać również obiektów klas dziedziczących po klasach bazowych, bez dokładnej znajomości tych obiektów. W tym artykule postaram się wyjaśnić tą definicję i opisać daną metodę. Przedstawię przykład kodu programu przed użyciem tej metody i po.

Zasada podstawienia Liskov z ang. Liskov substitution principle została sformułowana po raz pierwszy przez Barbarę Liskov w książce Data Abstraction and Hierarchy. Zasada ta mówi o tym, że klasa dziedzicząca powinna tylko rozszerzać możliwości klasy bazowej. W pewnym sensie nie powinna zmieniać tego, co ona robiła już wcześniej. Inaczej — jeśli będziemy tworzyć egzemplarz klasy potomnej, to niezależnie od tego, co znajdzie się we wskaźniku na zmienną, wywoływanie metody, którą pierwotnie zdefiniowano w klasie bazowej, powinno dać te same rezultaty. Celem zasady podstawienia Liskov jest uniknięcie sytuacji niezamierzonego/nieprawidłowego działania stworzonego oprogramowania. Funkcje obiektów, które zostały przypisane do referencji klas bazowych (rzutowania w górę), powinny zachowywać się w taki sam sposób, zarówno w przypadku rzutowania, jak i bez. Ta zasada bez wątpienia jest stosowana w aplikacji Personal Budget.

Przykład 1

Poniżej znajduje się typowy przykład naruszenia zasady podstawienia Liskov

Rysunek 1. Schemat pomocniczy

  1. void przetwarzajFigurę(iFigura& iFigura)  
  2. {  
  3.     if(typeid(iFigura) == typeid(Prostokąt))  
  4.         przetwarzajProstokąt(static_cast<Prostokąt&>(iFigura));  
  5.       
  6.     else if(typeid(iFigura) == typeid(Okrąg))  
  7.         przetwarzajOkrąg(static_cast<Okrąg&>(iFigura));  
  8.       
  9.     else if(typeid(iFigura) == typeid(Kwadrat))  
  10.         przetwarzajKwadrat(static_cast<Kwadrat&>(iFigura));  
  11. }   

Kod 1. Kod bez zastosowania trzeciej zasady SOLID

Funkcja przetwarzajFigurę() wprowadza dodatkowe zależności w kodzie, ponieważ musi ona znać wszystkie klasy dziedziczące po klasie Figura. Oznacza to, że każde utworzenie nowej klasy dziedziczącej po klasie Figura, będzie prawdopodobnie wiązało się ze zmianą tej funkcji. Jeśli spojrzymy na klasy Prostokąt oraz Kwadrat, można zauważyć, że klasa Kwadrat dziedziczy po klasie Prostokąt metody do ustawiania/pobierania wysokości oraz szerokości. W przypadku klasy Prostokąt obecność tych metod jest naturalna (każdy prostokąt może mieć wysokość różną od szerokości). Nie jest tak w przypadku klasy Kwadrat, ponieważ zmiana jego szerokości wiąże się ze zmianą wysokości. Oznacza to, że wywołanie metody ustawSzerokosc na klasie Kwadrat spowodowałoby zmianę wartości zwracanej przez metodę pobierzWysokosc. W takim przypadku, oprogramowanie musiałoby sprawdzać typ obiektu który zmienia. Musiałby wiedzieć w jaki sposób obiekt będzie się zachowywał po wprowadzeniu zmian. Taki styl programowania wprowadza dodatkowe zależności w kodzie i utrudnia jego późniejsze utrzymanie i rozwijanie.

Przykład 2

Kolejnym przykładem jest klasa Vehicle. Posiada ona dwie metody turnOnEngine() – uruchomienie silnika oraz move() – jedziemy oraz dwie klasy Car i Bike, które dziedziczą po klasie Vehicle. Jak widać jest to idealne relacja z realnego świata, gdyż samochód jak i rower są pojazdami. Jednakże z punktu widzenia struktur naszych klas jest to nielogiczne. Jak użyjemy roweru to mamy możliwość uruchomienia silnika, co nie jest prawdą, ponieważ rower tego silnika nie posiada. Hierarchia klas, która ma sens w realnym świecie (rower i samochód są pojazdami), w naszym programie jest to nie logiczne.

  1. <?php  
  2.   
  3. class Vehicle  
  4. {  
  5.     public function turnOnEngine()  
  6.     {  
  7.         echo 'Uruchamiamy silnik<br>';  
  8.     }  
  9.   
  10.     public function move()  
  11.     {  
  12.         echo 'Jedziemy';  
  13.     }  
  14. }  
  15.   
  16. class Car extends Vehicle  
  17. {  
  18.   
  19. }  
  20.   
  21. class Bike extends Vehicle  
  22. {  
  23.   
  24. }  
  25.   
  26. $vehicle = new Bike();  
  27. $vehicle->turnOnEngine();  
  28. $vehicle->move();  
  29.   
  30. ?>

Kod 2. Kod bez zastosowania trzeciej zasady SOLID

Zatem aby program było poprawny logicznie i według zasady podstawienia Liskow należy stworzyć dodatkowe klasy VehicleWithEngine i VehicleWithoutEngine, które dziedziczą z klasy Vehicle:

  1. <?php  
  2.   
  3. class Vehicle  
  4. {  
  5.     public function move()  
  6.     {  
  7.         echo 'Jedziemy';  
  8.     }  
  9. }  
  10.   
  11. class VehicleWithEngine extends Vehicle  
  12. {  
  13.     public function turnOnEngine()  
  14.     {  
  15.         echo 'Uruchamiamy silnik<br>';  
  16.     }     
  17. }  
  18.   
  19. class VehicleWithoutEngine extends Vehicle  
  20. {  
  21.   
  22. }  
  23.   
  24. class Car extends VehicleWithEngine  
  25. {  
  26.   
  27. }  
  28.   
  29. class Motorbike extends VehicleWithEngine  
  30. {  
  31.   
  32. }  
  33.   
  34. class Bike extends VehicleWithoutEngine  
  35. {  
  36.   
  37. }  
  38.   
  39. $vehicleWithEngine = new Car();  
  40. $vehicleWithEngine->turnOnEngine();  
  41. $vehicleWithEngine->move();  
  42.   
  43. $vehicleWithoutEngine = new Bike();  
  44. $vehicleWithoutEngine->move();  
  45.   
  46. ?>  

Kod 3. Kod z zastosowaniem trzeciej zasady SOLID

Powyższy kod nie narusza zasad podstawienia Liskov bo można wykorzystać klasę dziedzicząca albo obiekt klasy bazowej.

Podsumowując, trzecia zasada SOLID pomaga uniknąć rozczarowań wynikających z odmiennego niż zamierzone działania programu. Jest nierozerwalnym elementem, gdy programujemy z użyciem interfejsów. Dba o to, aby kod był spójny i nieskomplikowany.