Śledziłem to wysoko głosowane pytanie dotyczące możliwego naruszenia zasady substytucji Liskowa. Wiem, czym jest zasada podstawienia Liskowa, ale wciąż nie jest jasne, co może pójść źle, gdybym jako programista nie myślał o tej zasadzie podczas pisania kodu zorientowanego obiektowo.
27
Odpowiedzi:
Myślę, że zostało to dobrze powiedziane w tym pytaniu, które jest jednym z powodów, dla których głosowano tak wysoko.
Wyobraź sobie, że:
W tej metodzie od czasu do czasu wywołuje wybuch. przekazany tej metodzie.
Z powodu naruszeń substytucji Liskowa kod używający tego typu będzie musiał mieć wyraźną wiedzę na temat wewnętrznego działania typów pochodnych, aby traktować je inaczej. To ściśle łączy kod i ogólnie utrudnia konsekwentne stosowanie implementacji.
źródło
Jeśli nie wypełnisz kontraktu, który został zdefiniowany w klasie podstawowej, rzeczy mogą po cichu zawieść, gdy otrzymasz wyniki, które są wyłączone.
LSP w stanach Wikipedii
Jeśli któryś z nich się nie utrzyma, dzwoniący może otrzymać wynik, którego się nie spodziewał.
źródło
Rozważ klasyczny przypadek z annałów wywiadu: wyprowadziłeś Circle z Ellipse. Czemu? Oczywiście, że okrąg jest elipsą!
Z wyjątkiem ... elipsa ma dwie funkcje:
Oczywiście należy je ponownie zdefiniować dla Koła, ponieważ Okrąg ma jednolity promień. Masz dwie możliwości:
Większość języków OO nie obsługuje drugiego i nie bez powodu: zaskakujące byłoby stwierdzenie, że Twój krąg nie jest już Kręgiem. Pierwsza opcja jest więc najlepsza. Ale rozważ następującą funkcję:
Wyobraź sobie, że jakaś funkcja wywołuje e.set_alpha_radius. Ale ponieważ e był naprawdę Kręgiem, zaskakująco ma również ustawiony promień beta.
I tu leży zasada podstawienia: podklasa musi być substytucyjna dla nadklasy. W przeciwnym razie zdarzają się zaskakujące rzeczy.
źródło
Słowami laika:
Twój kod będzie zawierał strasznie dużo klauzul CASE / switch .
Każda z tych klauzul CASE / switch będzie wymagać dodawania nowych przypadków od czasu do czasu, co oznacza, że podstawa kodu nie jest tak skalowalna i łatwa w utrzymaniu, jak powinna.
LSP pozwala kodowi działać bardziej jak sprzęt:
Nie musisz modyfikować iPoda, ponieważ kupiłeś nową parę głośników zewnętrznych, ponieważ zarówno stare, jak i nowe głośniki zewnętrzne przestrzegają tego samego interfejsu, można je zamieniać bez utraty pożądanej funkcjonalności iPoda.
źródło
typeof(someObject)
aby zdecydować, co możesz zrobić, to na pewno, ale to zupełnie inna anty-wzorzec.aby dać przykład z życia za pomocą UndoManagera Java
dziedziczy, z
AbstractUndoableEdit
którego kontraktu wynika, że ma 2 stany (cofnięte i powtórzone) i może przechodzić między nimi pojedynczymi połączeniami doundo()
iredo()
jednak UndoManager ma więcej stanów i działa jak bufor cofania (każde wywołanie cofania
undo
niektórych, ale nie wszystkich edycji, osłabia warunek końcowy )prowadzi to do hipotetycznej sytuacji, w której dodajesz UndoManager do CompoundEdit przed wywołaniem,
end()
a następnie wywoływanie cofania w tym CompoundEdit spowoduje, że będzie on wywoływałundo()
każdą edycję, po częściowym cofnięciu edycjiWyrzuciłem własny,
UndoManager
aby tego uniknąć (prawdopodobnie powinienem zmienić jego nazwę naUndoBuffer
)źródło
Przykład: Pracujesz z frameworkiem interfejsu użytkownika i tworzysz własne niestandardowe sterowanie interfejsem użytkownika, podklasując
Control
klasę podstawową.Control
Klasa bazowa określa sposóbgetSubControls()
, który należy zwracać zbiór zagnieżdżonych kontroli (jeśli występuje). Ale zastępujesz metodę, by faktycznie zwrócić listę dat urodzin prezydentów Stanów Zjednoczonych.Co więc może pójść nie tak? Oczywiste jest, że renderowanie kontrolki zakończy się niepowodzeniem, ponieważ nie zwraca się listy kontrolek zgodnie z oczekiwaniami. Najprawdopodobniej nastąpi awaria interfejsu użytkownika. Jesteś zerwania kontraktu który ma podklasy Kontroli przestrzegać.
źródło
Możesz także na to spojrzeć z modelowego punktu widzenia. Kiedy mówisz, że instancja klasy
A
jest również instancją klasyB
, sugerujesz, że „obserwowalne zachowanie instancji klasyA
można również sklasyfikować jako obserwowalne zachowanie instancji klasyB
” (Jest to możliwe tylko wtedy, gdy klasaB
jest mniej konkretna niż klasaA
.)Zatem naruszenie LSP oznacza, że w twoim projekcie istnieje pewna sprzeczność: definiujesz pewne kategorie dla swoich obiektów, a następnie nie szanujesz ich w swojej implementacji, coś musi być nie tak.
Jak zrobić pudełko z tagiem: „To pudełko zawiera tylko niebieskie kule”, a następnie wrzucić do niego czerwoną piłkę. Jaki jest pożytek z takiego tagu, jeśli zawiera nieprawidłowe informacje?
źródło
Niedawno odziedziczyłem bazę kodów, w której znajdują się główni naruszający Liskov. W ważnych klasach. Sprawiło mi to ogromny ból. Pozwól mi wyjaśnić dlaczego.
Mam
Class A
, co wynika zClass B
.Class A
iClass B
udostępniamy kilka właściwości, któreClass A
zastępują własną implementację. Ustawienie lub uzyskanieClass A
właściwości ma inny efekt niż ustawienie lub pobranie dokładnie tej samej właściwościClass B
.Pomijając fakt, że jest to bardzo okropny sposób na tłumaczenie w .NET, istnieje wiele innych problemów z tym kodem.
W tym przypadku
Name
jest używany jako indeks i zmienna kontroli przepływu w wielu miejscach. Powyższe klasy są zaśmiecone w całej bazie kodu zarówno w postaci surowej, jak i pochodnej. Naruszenie zasady podstawienia Liskowa w tym przypadku oznacza, że muszę znać kontekst każdego pojedynczego wywołania każdej z funkcji zajmujących klasę podstawową.Zastosowania kodów obiektów zarówno
Class A
aClass B
, więc nie mogę po prostu zrobićClass A
abstrakcyjny, aby zmusić ludzi do używaniaClass B
.Istnieje kilka bardzo przydatnych funkcji narzędziowych, które działają
Class A
i inne bardzo użytecznych funkcji narzędziowych, które działająClass B
. Idealnie chciałabym, aby móc korzystać z żadnej funkcji użytkowej, która może działać naClass A
naClass B
. Wiele funkcji, które podejmują,Class B
może łatwo przyjąć,Class A
gdyby nie naruszenie LSP.Najgorsze w tym jest to, że ten konkretny przypadek jest naprawdę trudny do refaktoryzacji, ponieważ cała aplikacja opiera się na tych dwóch klasach, działa przez cały czas na obu klasach i pękłaby na sto sposobów, jeśli to zmienię (co zamierzam zrobić tak czy inaczej).
Będę musiał to naprawić, aby utworzyć
NameTranslated
właściwość, która będzieClass B
wersjąName
właściwości i bardzo, bardzo ostrożnie zmieniać każde odwołanie doName
właściwości pochodnej, aby użyć mojej nowejNameTranslated
właściwości. Jednak błędne podanie choćby jednego z tych odniesień może spowodować wysadzenie całej aplikacji.Biorąc pod uwagę, że podstawa kodu nie ma wokół siebie testów jednostkowych, jest to prawie najbardziej niebezpieczny scenariusz, z którym może spotkać się programista. Jeśli nie zmienię naruszenia, muszę wydać ogromne ilości energii mentalnej na śledzenie, na jakim typie obiektu działa każda metoda, a jeśli naprawię naruszenie, mógłbym spowodować, że cały produkt wybuchnie w nieodpowiednim czasie.
źródło
BaseName
orazTranslatedName
miał dostęp do stylu klasy AName
i znaczenia klasy B? Wtedy każda próba dostępuName
do zmiennej typuB
zostanie odrzucona z błędem kompilatora, dzięki czemu można upewnić się, że wszystkie odwołania zostały przekonwertowane na jedną z pozostałych formularzy.Jeśli chcesz poczuć problem naruszenia LSP, zastanów się, co się stanie, jeśli masz tylko .dll / .jar klasy bazowej (bez kodu źródłowego) i musisz zbudować nową klasę pochodną. Nigdy nie możesz wykonać tego zadania.
źródło