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).
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 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 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.
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ć.
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ć.
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 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.