Start Kontakt

Zasada SOLID (2/4)

W poprzednim artykule omówiono dwie pierwsze zasady SOLID: Single Responsibility Principle oraz Open/Closed Principle. A oto następna z nich (najbardziej rozbudowana i sformalizowana): Liskov Substitution Principle (zasada podstawienia Liskov).

Liskov Substitution Principle

Zasada podstawienia Liskov wzięła swoją nazwę od jej twórcy - Barbary Liskov. Zasada ta, w przeciwieństwie do innych, nie jest ogólna, lecz bardzo konkretna i sformalizowana, o czym zaraz się przekonamy.

Barbara Liskov jest informatykiem z USA i pracuje w MIT. To jedna z 50 najważniejszych kobiet w nauce (wyróżnienie magazynu Discover w 2002 roku). Zdefiniowała zasadę podstawienia będącą składnikiem zasady SOLID. Otrzymała nagrodę Turinga i medal von Neumanna. Od 2012 roku zajmuje miejsce w Galerii Sław Wynalazców Narodowych (National Inventors Hall of Fame).

Chodzi o to, by się upewnić, czy klasy dziedziczone są zgodne z kodem działającym poprawnie z klasami nadrzędnymi. Inaczej mówiąc, kod klasy dziedziczonej powinien działać poprawnie po podstawieniu go w miejsce klasy nadrzędnej. Zasada podstawienia Liskov składa się z kilku składowych. Przeanalizujemy je po kolei.

Typy parametrów

Typy parametrów metody w klasie podrzędnej muszą być takie same lub bardziej abstrakcyjne od typów parametrów odpowiedniej metody klasy nadrzędnej (czyli musi zachodzić tzw. kowariancja).

Załóżmy, że w grze wykorzystujemy metodę move(Goblin g) pozwalającą poruszać postacią goblina. Jeśli utworzymy klasę podrzędną i przeciążymy tę metodę w taki sposób, by mogła obsługiwać dowolny typ postaci, tzn. move(Character c), wszystko będzie działać poprawnie. Metoda obsłuży każdy typ postaci, włącznie z typem Goblin, który jest obsługiwany w klasie nadrzędnej. Spełniliśmy wymaganie Liskov: typ parametru w metodzie klasy dziedziczonej jest bardziej abstrakcyjny od typu metody z klasy głównej.

Gdybyśmy jednak metodę przeciążyli tak, by obsługiwała bardziej wyspecjalizowany typ goblina, na przykład GoblinFromNorth, mogłoby to spowodować problemy, ponieważ główny kod używając nowej klasy podrzędnej mógłby założyć, że metoda move obsłuży również typ Goblin, co nie byłoby prawdą.

Typ zwracany

Typ zwracany przez metodę w klasie podrzędnej musi być taki sam lub bardziej wyspecjalizowany od typu zwracanego w klasie nadrzędnej (a więc zachodzi kontrawariancja).

W grze używamy metodę create() tworzącą goblina i zwracającą obiekt typu Goblin. Jeśli w klasie dziedziczącej będziemy chcieli tworzyć gobliny z północy, czyli zwracać obiekt typu GoblinFromNorth, wszystko będzie działać poprawnie, bo przecież gobliny z północy są goblinami, a jedynie pochodzą z północnych krańców krainy.

Gdybyśmy jednak w klasie dziedzczącej chcieli tworzyć ogólne postacie typu Character, kod przestałby działać poprawnie, bo przecież taką postacią mógłby być Hobbit lub Elf, czyli coś zupełnie innego niż obiekt Goblin zwracany przez metodę klasy nadrzędnej.

Wyjątki

Metoda klasy pochodnej może zgłaszać tylko takie typy wyjątków, które są takie same, jak w metodzie klasy głównej, lub bardziej wyspecjalizowane.

Kod główny obsługujący wyjątki musi uwzględniac takie typy wyjątków, które są znane klasie nadrzędnej. Jeśli pojawiłyby się nieznane rodzaje wyjątków, kod przestałby działać poprawnie, bo nie potrafiłby ich właściwie obsłużyć.

Warunki wstępne

Klasa pochodna nie może mieć warunków wstępnych bardziej rygorystycznych od klasy nadrzędnej.

Powiedzmy, że w kodzie gry stosujemy metodę checkPower(Character c) zwracającej informację o sile danej postaci. Jeśli w klasie pochodnej ta metoda wymagałaby parametru typu Goblin, warunek wstępny stałby się bardziej rygorystyczny, ponieważ parametr zostałby ograniczony tylko do jednego typu. Kod, który chciałby użyć obiektu typu Elf, przestałby poprawnie działać.

Warunki końcowe

Klasa pochodna nie może mieć warunków końcowych mniej rygorystycznych od klasy nadrzędnej.

W naszej grze w trakcie walki wyświetlamy na ekranie pewną animację ruchu postaci. Animacja jest wykonywana jako oddzielny wątek. W momencie, gdy jedna z postaci ginie, wątek zostaje natychmiast zakończony. Jeśli w klasie podrzędnej zdecydowalibyśmy, by wątek nie był natychmiast usuwany (aby można go było ponownie użyć), pojawiłby się problem, ponieważ w programie zaczęłyby się pojawiać niepotrzebne wątki czekające na zamknięcie.

Niezmienniki klasy

Niezmienniki klasy nadrzędnej muszą zostać zachowane.

Niezmienniki klasy są pewnymi warunkami, które muszą zostać spełnione, by stan obiektu był poprawny. Na przykład niezmiennikami klasy Goblin jest wysoki stopień agresji nieschodzący poniżej pewnego poziomu, zielony kolor skóry, długie ręce itd. Najlepsza metoda zachowania niezmienników klasy nadrzędnej w klasie podrzędnej polega na tym, by ich w ogóle nie zmieniać, a zamiast tego dodawać nowe metody i pola.