
SOLIDny tydzień – L jak Liskov Substitution
Czwartek stoi pod znakiem najbardziej niezrozumiałej reguły z grupy SOLID. Opowiem dzisiaj o literze L – Liskov Substitution Principle, a więc regule podstawienia Liskov.
W skrócie
Reguła podstawienia Liskov teoretycznie jest prawie tak prosta jak pozostałe reguły SOLID. Jednak z jakiegoś powodu bardzo dużo osób ma z nią problem.
Ogólnie rzecz ujmując chodzi o to, że powinna być możliwość użycia klasy pochodnej w miejscu klasy bazowej bez wiedzy jaka dokładnie klasa została użyta. Oznacza to, że klasa pochodna nie może zmieniać zachowania względem klasy bazowej.
Czyli, że co?
Ta konkretna zasada jest na tyle specyficzna jeśli chodzi o zasady, że ciężko mi do niej znaleźć sensowne życiowe porównanie. I być może to jest powód dlaczego akurat ta reguła jest tak trudna do zrozumienia i zastosowania przez wielu programistów. Takie ogólne stwierdzenie, które mogłoby pasować do tej reguły to „<cicha> podmiana typów”.
Zasada podstawienia Liskov w pewnym sensie podobna do poprzedniej reguły. Jednak skupia się na samym obiekcie przekazywanym do docelowego miejsca, a nie na miejscu w jakim się znajdzie.
Tak w bardzo uogólnionym przykładzie wyobraź sobie, że Twoja klasa korzysta z innej klasy. Spodziewasz się, że będzie się zachowywać w konkretny sposób. Tzn. będzie przyjmować konkretne parametry, zwracać wartości konkretnego typu, ale też będzie chociażby rzucać konkretne wyjątki albo dawać konkretne efekty uboczne. I te ostatnie warunki są najczęściej łamane przy próbie korzystania z reguły podstawienia Liskov. Dlatego reguła podstawienia Liskov różni się od standardowego polimorfizmu pokazywanego chociażby na studiach. Bo bawiąc się w polimorfizm często w klasie pochodnej nadpisuje się metodę klasy bazowej zmieniając jej zachowanie. I jest to moment kiedy reguła została złamana ponieważ korzystając z takiej klasy pochodnej będzie się ona zachowywać inaczej niż klasa bazowa, a więc nie można być pewnym, że miejsce jej użycia nie spowoduje problemów przez tą zmianę zachowania bez jawnego sprawdzania typu przekazanego obiektu.
Kodzik
Sztandarowym przykładem kodu wykorzystującego tę zasadę jest przykład z kwadratem i prostokątem. Dlatego z niego nie skorzystam ;)
Przykład będzie abstrakcyjny z punktu widzenia aplikacji. Załóżmy, że masz zwykły czajnik elektryczny i czajnik, który dodatkowo wysyła powiadomienie, że ktoś go włączył. I chcemy użyć go w klasie Kuchnia:
class Czajnik { public virtual void Gotuj() { // włącz grzałkę } } class SuperCzajnik : Czajnik { public override void Gotuj() { base.Gotuj(); // wyślij powiadomienie } } class Kuchnia { private Czajnik _czajnik; public Kuchnia(Czajnik czajnik) { _czajnik = czajnik; } public void ZrobHerbate() { // weź kubek, herbatę, etc. _czajnik.Gotuj(); } }
Na pierwszy rzut oka zwykły polimorfizm. Ale ważne w tym przykładzie jest to, że nie nadpisuję metody z klasy bazowej, a ją używam. Także po pierwsze jak najbardziej mogę w miejsce zwykłego czajnika wstawić super-czajnik, który wysyła powiadomienia, ale też mogę być pewny, że zrobi to samo co zwykły czajnik. Bo wstawiając do kuchni taki czajnik zakładasz, że w podstawowym zadaniu użyjesz go dokładnie tak samo. Jeśli to nie Ty kupiłeś taki czajnik to idąc do kuchni i chcąc zrobić herbatę używasz tego czajnika jak zawsze i fakt wysyłania powiadomień nie może zmusić Cię do zmiany zachowania.
Aha, faktycznie było to trudne dla mnie do zrozumienia.
Nie wiem czemu, ale mam wrażenie, że do tej pory nie spotkałem się z wytłumaczeniem takim jak u Ciebie. Mianowicie: wszyscy piszą, że „klasa pochodna nie może zmieniać zachowania względem klasy bazowej”. Ale do tej pory nikt mi nie zwrócił uwagi, nie wytłumaczył, albo sam na to nie wpadłem, że użycie metody z klasy bazowej w niezmienionej postaci ( base.Gotuj();) a po tym wywołaniu dopisanie innego zachowania, nie powoduje zmiany zachowania względem klasy bazowej tylko rozszerzenie tego zachowania o nowe.
Pozdrawiam