Słyszałem, że Liskov Substitution Principle (LSP) jest podstawową zasadą projektowania obiektowego. Co to jest i jakie są przykłady jego użycia?
908
Słyszałem, że Liskov Substitution Principle (LSP) jest podstawową zasadą projektowania obiektowego. Co to jest i jakie są przykłady jego użycia?
Odpowiedzi:
Świetnym przykładem ilustrującym LSP (podanym przez wuja Boba w podcastu, który ostatnio słyszałem) było to, że czasami coś, co brzmi poprawnie w języku naturalnym, nie działa w kodzie.
W matematyce a
Square
jest aRectangle
. Rzeczywiście jest to specjalizacja prostokąta. „Jest” powoduje, że chcesz modelować to z dziedziczeniem. Jednak jeśli w kodzie, z którego sięSquare
wywodziszRectangle
, aSquare
powinno być użyteczne wszędzie tam, gdzie oczekujeszRectangle
. To powoduje dziwne zachowanie.Wyobraź sobie, że posiadasz
SetWidth
iSetHeight
metody w swojejRectangle
klasie podstawowej; wydaje się to całkowicie logiczne. Jeśli jednak twojeRectangle
odniesienie wskazywało na aSquare
, toSetWidth
iSetHeight
nie ma sensu, ponieważ ustawienie jednego zmieniłoby drugie, aby je dopasować. W tym przypadkuSquare
test Liskowa nie powiedzie się,Rectangle
a abstrakcjaSquare
dziedziczeniaRectangle
jest zła.Wszyscy powinniście sprawdzić inne bezcenne Motywacyjne plakaty SOLIDNE zasady .
źródło
Square.setWidth(int width)
został wdrożony w ten sposóbthis.width = width; this.height = width;
:? W takim przypadku gwarantuje się, że szerokość jest równa wysokości.Zasada substytucji Liskowa (LSP, lsp) to koncepcja programowania obiektowego, która stwierdza:
W jego sercu LSP dotyczy interfejsów i umów, a także tego, jak zdecydować, kiedy rozszerzyć klasę, a nie zastosować innej strategii, takiej jak kompozycja, aby osiągnąć swój cel.
Najskuteczniejszym sposobem Widziałem, aby zilustrować ten punkt był w Head First OOA & D . Przedstawiają scenariusz, w którym jesteś deweloperem projektu, który ma stworzyć ramy dla gier strategicznych.
Prezentują klasę reprezentującą tablicę, która wygląda następująco:
Wszystkie metody przyjmują współrzędne X i Y jako parametry w celu zlokalizowania położenia kafelka w dwuwymiarowej tablicy
Tiles
. Umożliwi to twórcy gry zarządzanie jednostkami na planszy w trakcie gry.Książka dalej zmienia wymagania, aby powiedzieć, że rama gry musi również obsługiwać plansze 3D, aby pomieścić gry, które mają lot. Tak więc wprowadzono
ThreeDBoard
klasę, która się rozszerzaBoard
.Na pierwszy rzut oka wydaje się to dobrą decyzją.
Board
zawiera zarównoHeight
aWidth
właściwości iThreeDBoard
zapewnia oś z.Rozkłada się, gdy spojrzysz na wszystkich odziedziczonych członków
Board
. MetodyAddUnit
,GetTile
,GetUnits
i tak dalej, ma wszystkie parametry X i Y wBoard
klasy, leczThreeDBoard
wymaga również parametr Z.Musisz więc ponownie zaimplementować te metody za pomocą parametru Z. Parametr Z nie ma kontekstu dla
Board
klasy, a odziedziczone metody zBoard
klasy tracą swoje znaczenie. Jednostka kodu próbująca wykorzystaćThreeDBoard
klasę jako klasę podstawowąBoard
byłaby bardzo pechowa.Może powinniśmy znaleźć inne podejście. Zamiast powiększenia
Board
,ThreeDBoard
powinien składać się zBoard
obiektów. JedenBoard
obiekt na jednostkę osi Z.To pozwala nam korzystać z dobrych, obiektowych zasad, takich jak enkapsulacja i ponowne użycie, i nie narusza LSP.
źródło
zróbmy prosty przykład w Javie:
Zły przykład
Kaczka może latać, ponieważ jest ptakiem, ale co z tym:
Struś jest ptakiem, ale nie może latać, klasa strusia jest podtypem ptaka Ptak, ale nie może używać metody latania, co oznacza, że łamiemy zasadę LSP.
Dobry przykład
źródło
Bird bird
. Musisz rzucić obiekt na FlyingBirds, aby użyć muchy, co nie jest miłe, prawda?Bird bird
, to znaczy, że nie może użyćfly()
. Otóż to. Zdanie aDuck
nie zmienia tego faktu. Jeśli klient takFlyingBirds bird
, to nawet jeśli go przejdzieDuck
, powinien zawsze działać w ten sam sposób.LSP dotyczy niezmienników.
Klasyczny przykład podaje następująca deklaracja pseudokodu (pominięte implementacje):
Teraz mamy problem, chociaż interfejs pasuje. Powodem jest to, że naruszyliśmy niezmienniki wynikające z matematycznej definicji kwadratów i prostokątów. Sposób działania pobierających i ustawiających
Rectangle
powinien spełniać następujące niezmienniki:Jednak ten niezmiennik musi zostać naruszony przez poprawną implementację
Square
, dlatego nie jest prawidłowym zamiennikiemRectangle
.źródło
Robert Martin ma doskonały artykuł na temat zasady substytucji Liskowa . Omawia subtelne i niezbyt subtelne sposoby naruszania zasady.
Niektóre istotne części artykułu (zauważ, że drugi przykład jest mocno skondensowany):
źródło
Now the rule for the preconditions and postconditions for derivatives, as stated by Meyer is: ...when redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one.
Jeśli warunek wstępny klasy dziecięcej jest silniejszy niż warunek podstawowy klasy rodzicielskiej, nie można zastąpić rodzica dzieckiem bez naruszenia warunku wstępnego. Stąd LSP.LSP jest konieczny tam, gdzie jakiś kod uważa, że wywołuje metody typu
T
, i może nieświadomie wywoływać metody typuS
, w którymS extends T
(tj.S
Dziedziczy, wywodzi się z podtypu lub jest jego podtypemT
).Dzieje się tak na przykład wtedy, gdy funkcja z parametrem wejściowym typu
T
jest wywoływana (tzn. Wywoływana) z wartością argumentu typuS
. Lub, gdy identyfikator typuT
, ma przypisaną wartość typuS
.LSP wymaga oczekiwań (tj. Niezmienników) dla metod typu
T
(np.Rectangle
), Nie należy ich naruszać, gdy zamiast tego wywoływane są metody typuS
(np.Square
).Nawet typ z niezmiennymi polami wciąż ma niezmienniki, np. Niezmienne układy prostokątów oczekują, że wymiary będą niezależnie modyfikowane, ale niezmienne układacze kwadratów naruszają to oczekiwanie.
LSP wymaga, aby każda metoda tego podtypu
S
miała przeciwwariantny parametr wejściowy i wyjściowy efekt kowariantny.Kontrawariant oznacza, że wariancja jest sprzeczna z kierunkiem dziedziczenia, tj. Typ
Si
każdego parametru wejściowego każdej metody podtypuS
, musi być taki sam lub nadtyp typuTi
odpowiedniego parametru wejściowego odpowiedniej metody nadtypuT
.Kowariancja oznacza, że wariancja jest w tym samym kierunku dziedziczenia, tzn. Rodzaj
So
wyniku każdej metody podtypuS
musi być taki sam lub podtypu typuTo
odpowiedniego wyniku odpowiedniej metody nadtypuT
.Wynika to z faktu, że jeśli program wywołujący myśli, że ma typ
T
, myśli, że wywołuje metodęT
, wówczas dostarcza argumenty typuTi
i przypisuje dane wyjściowe do typuTo
. Gdy faktycznie wywołuje odpowiednią metodęS
, każdyTi
argument wejściowy jest przypisywany doSi
parametru wejściowego, a daneSo
wyjściowe są przypisywane do typuTo
. Zatem jeśliSi
nie byłyby sprzeczne z wrtTi
, to podtypXi
, który nie byłby podtypem,Si
mógłby zostać przypisanyTi
.Ponadto w przypadku języków (np. Scala lub Cejlon), które mają adnotacje wariancji w miejscu definicji parametrów polimorfizmu typu (tj. Rodzajowych), ko- lub przeciwny kierunek adnotacji wariancji dla każdego parametru typu
T
musi być przeciwny lub taki sam odpowiednio do każdego parametru wejściowego lub wyjściowego (każdej metodyT
), który ma typ parametru type.Dodatkowo dla każdego parametru wejściowego lub wyjściowego, który ma typ funkcji, wymagany kierunek wariancji jest odwrócony. Ta reguła jest stosowana rekurencyjnie.
Podpisywanie jest właściwe tam, gdzie niezmienniki można wyliczyć.
Trwa wiele badań nad tym, jak modelować niezmienniki, aby były one wymuszane przez kompilator.
Typestate (patrz strona 3) deklaruje i wymusza niezmienniki stanu ortogonalne do wpisywania. Alternatywnie niezmienniki można wymusić, przekształcając twierdzenia na typy . Na przykład, aby potwierdzić, że plik jest otwarty przed jego zamknięciem, wówczas File.open () może zwrócić typ OpenFile, który zawiera metodę close (), która nie jest dostępna w File. Tic-krzyżyk API może być kolejny przykład stosując typowanie wymusić niezmienników w czasie kompilacji. System typów może być nawet kompletny w Turinga, np . Scala . Języki i dowody twierdzeń o typie zależnym formalizują modele pisania wyższego rzędu.
Ze względu na potrzebę abstrakcji semantyki zamiast rozszerzenia , spodziewam się, że zastosowanie typowania do modelowania niezmienników, tj. Ujednoliconej semantyki denotacyjnej wyższego rzędu, jest lepsze niż typestate. „Rozszerzenie” oznacza nieograniczony, permutowany skład nieskoordynowanego, modułowego rozwoju. Ponieważ wydaje mi się, że jest antytezą zjednoczenia, a tym samym stopni swobody, mieć dwa wzajemnie zależne modele (np. Typy i typowanie) do wyrażania wspólnej semantyki, których nie można zjednoczyć ze sobą w celu rozszerzenia kompozycji . Na przykład rozszerzenie podobne do problemu wyrażenia zostało ujednolicone w dziedzinie podtytułu, przeciążenia funkcji i parametrycznych domen typowania.
Moja teoretyczna pozycja jest taka, że aby istniała wiedza (patrz sekcja „Centralizacja jest ślepa i nieodpowiednia”), nigdy nie będzie ogólnego modelu, który mógłby wymusić 100% pokrycie wszystkich możliwych niezmienników w języku komputerowym kompletnym Turinga. Aby istniała wiedza, istnieje wiele nieoczekiwanych możliwości, tzn. Nieporządek i entropia muszą zawsze rosnąć. To jest siła entropii. Aby udowodnić wszystkie możliwe obliczenia potencjalnego rozszerzenia, należy z góry obliczyć wszystkie możliwe rozszerzenia.
Dlatego istnieje Twierdzenie Haltinga, tzn. Nie można rozstrzygnąć, czy każdy możliwy program w języku programowania Turinga zakończy się. Można udowodnić, że jakiś określony program kończy się (taki, w którym wszystkie możliwości zostały zdefiniowane i obliczone). Nie można jednak udowodnić, że wszelkie możliwe rozszerzenia tego programu kończą się, chyba że możliwości rozszerzenia tego programu nie są kompletne w Turingu (np. Przez wpisywanie zależne). Ponieważ podstawowym wymogiem dla kompletności Turinga jest nieograniczona rekurencja , intuicyjne jest zrozumienie, w jaki sposób twierdzenia Gödela i paradoks Russella odnoszą się do rozszerzenia.
Interpretacja tych twierdzeń uwzględnia je w uogólnionym pojęciowym rozumieniu siły entropicznej:
źródło
Istnieje lista kontrolna do ustalenia, czy naruszasz Liskov.
Lista kontrolna:
Ograniczenie historii : Podczas przesłonięcia metody nie wolno modyfikować właściwości niemodyfikowalnych w klasie podstawowej. Spójrz na ten kod i zobaczysz, że Nazwa jest zdefiniowana jako niemodyfikowalna (zestaw prywatny), ale SubType wprowadza nową metodę, która pozwala ją modyfikować (poprzez odbicie):
Istnieją jeszcze 2 inne elementy: Kontrawariancja argumentów metody i Kowariancja typów zwracanych . Ale nie jest to możliwe w C # (jestem programistą C #), więc nie obchodzi mnie to.
Odniesienie:
źródło
Widzę prostokąty i kwadraty w każdej odpowiedzi i jak naruszać LSP.
Chciałbym pokazać, w jaki sposób można dostosować LSP do rzeczywistego przykładu:
Ten projekt jest zgodny z LSP, ponieważ zachowanie pozostaje niezmienione niezależnie od implementacji, którą wybraliśmy.
I tak, możesz naruszyć LSP w tej konfiguracji, wykonując jedną prostą zmianę:
Teraz podtypy nie mogą być używane w ten sam sposób, ponieważ nie dają już tego samego wyniku.
źródło
Database::selectQuery
obsługi tylko podzbioru SQL obsługiwanego przez wszystkie silniki DB. To nie jest praktyczne ... To powiedziawszy, przykład jest nadal łatwiejszy do zrozumienia niż większość innych tutaj używanych.LSP jest regułą dotyczącą umowy klauzul: jeśli klasa podstawowa spełnia kontrakt, wówczas klasy pochodne LSP muszą również spełniać tę umowę.
W pseudo-python
spełnia LSP, jeśli za każdym razem, gdy wywołujesz Foo na obiekcie pochodnym, daje dokładnie takie same wyniki jak wywoływanie Foo na obiekcie bazowym, o ile arg jest taki sam.
źródło
2 + "2"
). Być może mylisz „silnie wpisany” z „statycznie wpisany”?Długa historia krótkiego, zostawmy prostokąty prostokątów i kwadratów kwadratów, praktyczny przykład przy przedłużaniu klasę nadrzędną, trzeba też zachować dokładną nadrzędnego API lub jego przedłużenia.
Załóżmy, że masz podstawową pozycję ItemsRepository.
I rozszerzająca go podklasa:
Wtedy możesz mieć klienta pracującego z API Base ItemsRepository i polegającego na nim.
LSP jest uszkodzony, gdy zastępując nadrzędnego klasę z PODKLASA przerw zamówienia API .
Możesz dowiedzieć się więcej na temat pisania oprogramowania, które można konserwować w moim kursie: https://www.udemy.com/enterprise-php/
źródło
Kiedy po raz pierwszy przeczytałem o LSP, założyłem, że miał on na celu bardzo ścisły sens, zasadniczo utożsamiając go z implementacją interfejsu i rzutowaniem bezpiecznym dla typu. Co oznaczałoby, że LSP jest albo zapewniony, albo nie przez sam język. Na przykład, w tym ścisłym znaczeniu, ThreeDBoard jest z pewnością substytutem dla Board, jeśli chodzi o kompilator.
Po przeczytaniu więcej na temat tej koncepcji odkryłem, że LSP jest ogólnie interpretowany szerzej.
Krótko mówiąc, co oznacza, że kod klienta „wie”, że obiekt za wskaźnikiem jest typu pochodnego, a nie typ wskaźnika, nie ogranicza się do bezpieczeństwa typu. Zgodność z LSP można również przetestować poprzez zbadanie rzeczywistego zachowania obiektów. Oznacza to badanie wpływu argumentów stanu i metody obiektu na wyniki wywołań metody lub rodzajów wyjątków zgłaszanych przez obiekt.
Wracając do przykładu, teoretycznie można sprawić , że metody Board będą działać dobrze na ThreeDBoard. W praktyce jednak bardzo trudno będzie zapobiec różnicom w zachowaniu, które klient może nie obsługiwać poprawnie, bez ingerowania w funkcje, które ma dodać ThreeDBoard.
Mając tę wiedzę, ocena przestrzegania LSP może być doskonałym narzędziem w określaniu, kiedy skład jest bardziej odpowiednim mechanizmem rozszerzania istniejącej funkcjonalności, a nie dziedziczeniem.
źródło
Myślę, że każdy w pewnym sensie opisał, czym technicznie jest LSP: Zasadniczo chcesz być w stanie oderwać się od szczegółów podtypu i bezpiecznie korzystać z nadtypów.
Więc Liskov ma 3 podstawowe zasady:
Reguła podpisu: Powinna istnieć poprawna implementacja każdej operacji nadtypu w podtypie składniowo. Coś, co kompilator będzie mógł sprawdzić. Istnieje niewielka reguła dotycząca zgłaszania mniejszej liczby wyjątków i bycia co najmniej tak samo dostępnym, jak metody nadtypu.
Metoda Reguła: Implementacja tych operacji jest poprawna semantycznie.
Reguła właściwości: Wykracza to poza indywidualne wywołania funkcji.
Wszystkie te właściwości muszą zostać zachowane, a dodatkowa funkcjonalność podtypu nie powinna naruszać właściwości nadtypu.
Jeśli załatwisz te trzy rzeczy, oderwasz się od podstawowych rzeczy i piszesz luźno powiązany kod.
Źródło: Programowanie w Javie - Barbara Liskov
źródło
Ważnym przykładem zastosowania LSP jest testowanie oprogramowania .
Jeśli mam klasę A, która jest podklasą B zgodną z LSP, mogę ponownie użyć zestawu testów B do przetestowania A.
Aby w pełni przetestować podklasę A, prawdopodobnie muszę dodać jeszcze kilka przypadków testowych, ale przynajmniej mogę ponownie użyć wszystkich przypadków testowych nadklasy B.
Sposobem na osiągnięcie tego jest zbudowanie tego, co McGregor nazywa „równoległą hierarchią testowania”: moja
ATest
klasa odziedziczyBTest
. Potrzebna jest zatem pewna forma iniekcji, aby upewnić się, że przypadek testowy działa z obiektami typu A, a nie typu B (wystarczy prosty wzór szablonu).Zauważ, że ponowne użycie pakietu super-testów dla wszystkich implementacji podklasy jest w rzeczywistości sposobem na sprawdzenie, czy te implementacje podklasy są zgodne z LSP. Zatem można również argumentować, że należy uruchomić pakiet testów nadklasy w kontekście dowolnej podklasy.
Zobacz także odpowiedź na pytanie Stackoverflow „ Czy mogę zaimplementować serię testów wielokrotnego użytku w celu przetestowania implementacji interfejsu? ”
źródło
Zilustrujmy w Javie:
Tutaj nie ma problemu, prawda? Samochód jest zdecydowanie urządzeniem transportowym i tutaj możemy zobaczyć, że zastępuje on metodę startEngine () swojej nadklasy.
Dodajmy kolejne urządzenie transportowe:
Teraz wszystko nie idzie zgodnie z planem! Tak, rower jest urządzeniem transportowym, jednak nie ma silnika i dlatego nie można zaimplementować metody startEngine ().
Rozwiązaniem tych problemów jest poprawna hierarchia dziedziczenia, aw naszym przypadku rozwiązalibyśmy problem, różnicując klasy urządzeń transportowych z silnikami i bez. Chociaż rower jest środkiem transportu, nie ma silnika. W tym przykładzie nasza definicja urządzenia transportowego jest błędna. Nie powinien mieć silnika.
Możemy zmienić naszą klasę TransportDevice w następujący sposób:
Teraz możemy rozszerzyć TransportDevice dla urządzeń niezmotoryzowanych.
I rozszerz Urządzenia transportowe dla urządzeń zmotoryzowanych. Tutaj bardziej odpowiednie jest dodanie obiektu Engine.
W ten sposób nasza klasa samochodów staje się bardziej wyspecjalizowana, przy jednoczesnym przestrzeganiu zasady substytucji Liskowa.
Nasza klasa rowerów jest również zgodna z zasadą substytucji Liskowa.
źródło
Takie sformułowanie LSP jest zdecydowanie zbyt silne:
Co w zasadzie oznacza, że S to kolejna, całkowicie zamknięta implementacja dokładnie tej samej rzeczy co T. I mógłbym być odważny i zdecydować, że wydajność jest częścią zachowania P ...
Zasadniczo każde użycie późnego wiązania narusza LSP. Chodzi o to, że OO polega na uzyskaniu innego zachowania, gdy zamieniamy jeden obiekt na inny!
Formuła cytowana przez wikipedię jest lepsza, ponieważ właściwość zależy od kontekstu i niekoniecznie obejmuje całe zachowanie programu.
źródło
W bardzo prostym zdaniu możemy powiedzieć:
Klasa potomna nie może naruszać jej charakterystyk klasy podstawowej. Musi sobie z tym poradzić. Można powiedzieć, że jest to tak samo jak podtyp.
źródło
Przykład:
Poniżej znajduje się klasyczny przykład, w którym naruszono zasadę substytucji Liskowa. W tym przykładzie zastosowano 2 klasy: Prostokąt i Kwadrat. Załóżmy, że obiekt Rectangle jest używany gdzieś w aplikacji. Rozszerzamy aplikację i dodajemy klasę Square. Klasa kwadratowa jest zwracana przez wzorzec fabryczny, oparty na niektórych warunkach i nie wiemy dokładnie, jaki typ obiektu zostanie zwrócony. Ale wiemy, że to prostokąt. Otrzymujemy obiekt prostokąta, ustawiamy szerokość na 5 i wysokość na 10 i otrzymujemy obszar. W przypadku prostokąta o szerokości 5 i wysokości 10 obszar powinien wynosić 50. Zamiast tego wynik wyniesie 100
Zobacz także: Zasada otwartego zamknięcia
Kilka podobnych koncepcji lepszej struktury: Konwencja o konfiguracji
źródło
Zasada substytucji Liskowa
źródło
Niektóre uzupełnienia:
Zastanawiam się, dlaczego nikt nie napisał o niezmienniku, warunkach wstępnych i warunkach końcowych klasy podstawowej, które muszą być przestrzegane przez klasy pochodne. Aby pochodna klasa D była całkowicie odporna na działanie klasy podstawowej B, klasa D musi spełniać pewne warunki:
Tak więc pochodna musi być świadoma trzech powyższych warunków narzuconych przez klasę podstawową. Dlatego zasady podtypów są z góry ustalone. Co oznacza, że stosunek „JEST A” będzie przestrzegany tylko wtedy, gdy podtyp przestrzega pewnych zasad. Zasady te, w postaci niezmienników, warunków wstępnych i warunków dodatkowych, powinny zostać określone w formalnej „ umowie projektowej ”.
Dalsze dyskusje na ten temat dostępne na moim blogu: Liskov Substytucja
źródło
LSP w prostych słowach stwierdza, że obiekty tej samej nadklasy powinny mieć możliwość wymiany między sobą bez niszczenia czegokolwiek.
Na przykład, jeśli mamy
Cat
orazDog
klasę pochodzącą zAnimal
klasy, wszelkie funkcje wykorzystujące klasę zwierzę powinno mieć możliwość korzystania zCat
lubDog
i zachowywać się normalnie.źródło
Czy wdrożenie ThreeDBoard pod względem tablicy będzie tak przydatne?
Być może możesz chcieć traktować plastry ThreeDBoard w różnych płaszczyznach jako planszę. W takim przypadku możesz wyodrębnić interfejs (lub klasę abstrakcyjną) dla tablicy, aby umożliwić wiele implementacji.
Jeśli chodzi o interfejs zewnętrzny, możesz wyróżnić interfejs Board zarówno dla TwoDBoard, jak i ThreeDBoard (chociaż żadna z powyższych metod nie pasuje).
źródło
Kwadrat to prostokąt, którego szerokość równa się wysokości. Jeśli kwadrat ustawia dwa różne rozmiary dla szerokości i wysokości, narusza to niezmiennik kwadratowy. Można to obejść poprzez wprowadzenie efektów ubocznych. Ale jeśli prostokąt miał setSize (wysokość, szerokość) z warunkiem wstępnym 0 <wysokość i 0 <szerokość. Pochodna metoda podtypu wymaga wysokość == szerokość; silniejszy warunek wstępny (i to narusza lsp). To pokazuje, że chociaż kwadrat jest prostokątem, nie jest prawidłowym podtypem, ponieważ warunek wstępny jest wzmocniony. Obejście (ogólnie rzecz biorąc, zła rzecz) powoduje efekt uboczny, co osłabia stan postu (co narusza lsp). setWidth na podstawie ma warunek słupka 0 <szerokość. Wyprowadzony osłabia go o wysokości == szerokości.
Dlatego kwadrat o zmiennym rozmiarze nie jest prostokątem o zmiennym rozmiarze.
źródło
Zasada ta została wprowadzona przez Barbarę Liskov w 1987 roku i rozszerza zasadę otwartego zamknięcia, koncentrując się na zachowaniu nadklasy i jej podtypów.
Jego znaczenie staje się oczywiste, gdy weźmiemy pod uwagę konsekwencje jego naruszenia. Rozważ aplikację korzystającą z następującej klasy.
Wyobraź sobie, że pewnego dnia klient oprócz prostokątów wymaga także manipulowania kwadratami. Ponieważ kwadrat jest prostokątem, klasę kwadratu należy wyprowadzić z klasy Prostokąt.
W ten sposób napotkamy jednak dwa problemy:
Kwadrat nie potrzebuje zmiennych wysokości i szerokości dziedziczonych z prostokąta, co może powodować znaczne marnotrawstwo pamięci, jeśli musimy stworzyć setki tysięcy kwadratowych obiektów. Właściwości ustawiania szerokości i wysokości dziedziczone z prostokąta są nieodpowiednie dla kwadratu, ponieważ szerokość i wysokość kwadratu są identyczne. Aby ustawić zarówno wysokość, jak i szerokość na tę samą wartość, możemy utworzyć dwie nowe właściwości w następujący sposób:
Teraz, gdy ktoś ustawi szerokość kwadratowego obiektu, jego wysokość odpowiednio się zmieni i na odwrót.
Przejdźmy do przodu i rozważmy tę inną funkcję:
Gdybyśmy przekazali odwołanie do obiektu kwadratowego do tej funkcji, naruszylibyśmy LSP, ponieważ funkcja nie działa dla pochodnych jej argumentów. Szerokość i wysokość właściwości nie są polimorficzne, ponieważ nie zostały zadeklarowane jako wirtualne w prostokącie (kwadratowy obiekt zostanie uszkodzony, ponieważ wysokość nie zostanie zmieniona).
Jednak deklarując, że właściwości setera są wirtualne, napotkamy kolejne naruszenie, OCP. W rzeczywistości utworzenie kwadratu klasy pochodnej powoduje zmiany w prostokącie klasy podstawowej.
źródło
Najczystszym wyjaśnieniem LSP, które do tej pory znalazłem, jest „Zasada podstawienia Liskowa mówi, że obiekt klasy pochodnej powinien być w stanie zastąpić obiekt klasy podstawowej bez powodowania błędów w systemie lub modyfikowania zachowania klasy podstawowej „ stąd . W artykule podano przykładowy kod naruszenia LSP i jego naprawienia.
źródło
Powiedzmy, że używamy prostokąta w naszym kodzie
W naszej klasie geometrii dowiedzieliśmy się, że kwadrat jest specjalnym rodzajem prostokąta, ponieważ jego szerokość jest taka sama jak jego wysokość. Stwórzmy również
Square
klasę na podstawie tych informacji:Gdybyśmy wymienić
Rectangle
zSquare
naszego pierwszego kodu, a potem będzie przerwa:To dlatego, że
Square
ma nowy warunek nie mieliśmy wRectangle
klasie:width == height
. Według LSPRectangle
instancje powinny być zastępowalneRectangle
instancjami podklasy. Jest tak, ponieważ te instancje przechodzą sprawdzanie typu dlaRectangle
instancji i dlatego powodują nieoczekiwane błędy w kodzie.To był przykład części „warunków wstępnych, których nie można wzmocnić w podtypie” w artykule wiki . Podsumowując, naruszenie LSP prawdopodobnie spowoduje błędy w kodzie w pewnym momencie.
źródło
LSP mówi, że „Obiekty powinny być zastępowalne według ich podtypów”. Z drugiej strony zasada ta wskazuje
a poniższy przykład pomaga lepiej zrozumieć LSP.
Bez LSP:
Naprawianie przez LSP:
źródło
Zachęcam do zapoznania się z artykułem: Naruszenie zasady substytucji Liskowa (LSP) .
Możesz znaleźć wyjaśnienie, czym jest Zasada Zastępstwa Liskowa, ogólne wskazówki pomagające odgadnąć, czy już ją naruszyłeś, oraz przykład podejścia, które pomoże ci zwiększyć bezpieczeństwo w hierarchii klas.
źródło
ZASADA SUBSTYTUCJI LISKOWA (z książki Marka Seemanna) stwierdza, że powinniśmy być w stanie zastąpić jedną implementację interfejsu inną bez przerywania ani klienta, ani implementacji. Ta zasada pozwala sprostać wymaganiom, które pojawią się w przyszłości, nawet jeśli możemy ” przewidzieć je dzisiaj.
Jeśli odłączymy komputer od ściany (implementacja), ani gniazdko sieciowe (interfejs), ani komputer (klient) nie ulegną awarii (w rzeczywistości, jeśli jest to laptop, może nawet działać na baterie przez pewien czas) . Jednak w przypadku oprogramowania klient często oczekuje, że usługa będzie dostępna. Jeśli usługa została usunięta, otrzymujemy wyjątek NullReferenceException. Aby poradzić sobie z tego typu sytuacją, możemy stworzyć implementację interfejsu, który „nic nie robi”. Jest to wzorzec projektowy znany jako Null Object [4] i odpowiada w przybliżeniu odłączeniu komputera od ściany. Ponieważ używamy luźnego sprzężenia, możemy zastąpić prawdziwą implementację czymś, co nic nie robi bez powodowania problemów.
źródło
Zasada podstawienia Likowa stwierdza, że jeśli moduł programu korzysta z klasy Base, wówczas odwołanie do klasy Base można zastąpić klasą Derived bez wpływu na funkcjonalność modułu programu.
Cel - typy pochodne muszą całkowicie zastępować typy podstawowe.
Przykład - typy zwracanych wariantów w java.
źródło
Oto fragment tego postu który ładnie wyjaśnia rzeczy:
[..] aby zrozumieć niektóre zasady, ważne jest, aby zdawać sobie sprawę z tego, kiedy zostały naruszone. To właśnie teraz zrobię.
Co oznacza naruszenie tej zasady? Oznacza to, że obiekt nie spełnia umowy narzuconej przez abstrakcję wyrażoną za pomocą interfejsu. Innymi słowy, oznacza to, że źle zidentyfikowałeś swoje abstrakcje.
Rozważ następujący przykład:
Czy to naruszenie LSP? Tak. Wynika to z faktu, że umowa konta mówi nam, że konto zostanie wycofane, ale nie zawsze tak jest. Co powinienem zrobić, aby to naprawić? Właśnie modyfikuję umowę:
Voilà, teraz umowa jest spełniona.
To subtelne naruszenie często narzuca klientowi zdolność do odróżnienia zastosowanych konkretnych obiektów. Na przykład biorąc pod uwagę pierwszą umowę Konta, może wyglądać następująco:
I to automatycznie narusza zasadę otwartego zamknięcia [to znaczy wymogu wypłaty pieniędzy. Ponieważ nigdy nie wiadomo, co się stanie, jeśli obiekt naruszający umowę nie ma wystarczającej ilości pieniędzy. Prawdopodobnie nic nie zwraca, prawdopodobnie zostanie zgłoszony wyjątek. Musisz więc sprawdzić, czy to
hasEnoughMoney()
nie jest częścią interfejsu. Zatem ta wymuszona kontrola zależna od konkretnej klasy stanowi naruszenie OCP].Ten punkt dotyczy także błędnego przekonania, które dość często spotykam na temat naruszenia LSP. Mówi: „jeśli zachowanie rodzica zmieniło się u dziecka, narusza to LSP”. Nie robi to jednak - o ile dziecko nie naruszy umowy rodzica.
źródło