Co może pójść nie tak, jeśli zostanie naruszona zasada substytucji Liskowa?

27

Ś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.

Maniak
źródło
6
Co może pójść źle, jeśli nie będziesz przestrzegać LSP? Scenariusz najgorszego przypadku: w końcu przywołujesz Code-thulhu! ;)
FrustratedWithFormsDesigner
1
Jako autor tego oryginalnego pytania muszę dodać, że było to pytanie o charakterze akademickim. Chociaż naruszenia mogą powodować błędy w kodzie, nigdy nie miałem poważnego błędu lub problemu konserwacyjnego, który mogę przypisać naruszeniu LSP.
Paul T Davies
2
@Paul Więc nigdy nie miałeś problemów ze swoimi programami z powodu skomplikowanych hierarchii OO (których sam nie zaprojektowałeś, ale być może musiałeś przedłużyć), w których umowy były łamane w lewo i prawo przez ludzi, którzy nie byli pewni co do celu klasy podstawowej najpierw? Zazdroszczę ci! :)
Andres F.,
@PaulTDavies dotkliwość konsekwencji zależy od tego, czy użytkownicy (programiści korzystający z biblioteki) mają szczegółową wiedzę na temat implementacji biblioteki (tj. Mają dostęp do kodu biblioteki i znają go). W końcu użytkownicy przeprowadzą dziesiątki kontroli warunkowych lub zbudują opakowania wokół biblioteki, aby uwzględnić nie-LSP (zachowanie specyficzne dla klasy). Najgorszy scenariusz miałby miejsce, gdyby biblioteka była produktem komercyjnym o zamkniętym źródle.
rwong
@Andres i rwong, zilustruj te problemy odpowiedzią. Przyjęta odpowiedź w dużej mierze popiera Paula Daviesa, ponieważ konsekwencje wydają się niewielkie (wyjątek), które zostaną szybko zauważone i naprawione, jeśli masz dobry kompilator, analizator statyczny lub minimalny test jednostkowy.
user949300,

Odpowiedzi:

31

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.

Teraz, gdy wywołujesz Close () w zadaniu, istnieje szansa, że ​​wywołanie nie powiedzie się, jeśli jest to ProjectTask ze statusem uruchomienia, a nie byłoby, gdyby było to zadanie podstawowe.

Wyobraź sobie, że:

public void ProcessTaskAndClose(Task taskToProcess)
{
    taskToProcess.Execute();
    taskToProcess.DateProcessed = DateTime.Now;
    taskToProcess.Close();
}

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.

Jimmy Hoffa
źródło
Czy to oznacza, że ​​klasa podrzędna nie może mieć własnych metod publicznych, które nie zostały zadeklarowane w klasie nadrzędnej?
Songo,
@Songo: Niekoniecznie: może, ale metody te są „nieosiągalne” ze wskaźnika podstawowego (odwołania lub zmiennej lub innego języka, który go wywołuje) i potrzebujesz informacji o typie wykonawczym, aby zapytać, jaki typ ma obiekt zanim będzie można wywołać te funkcje. Jest to jednak kwestia silnie związana ze składnią języków i semantyką.
Emilio Garavaglia
2
Nie. Dzieje się tak, gdy odwołuje się do klasy potomnej, jakby była rodzajem klasy nadrzędnej, w którym to przypadku członkowie, którzy nie zostali zadeklarowani w klasie nadrzędnej, są niedostępni.
Chewy Gumball
1
@Phil Yep; to jest definicja ścisłego sprzężenia: zmiana jednej rzeczy powoduje zmiany w innych rzeczach. Luźno sprzężona klasa może mieć zmienioną implementację bez konieczności zmiany kodu poza nią. Właśnie dlatego umowy są dobre, pokazują, jak nie wymagać zmian w stosunku do konsumentów obiektu: spełnij umowę, a konsumenci nie będą potrzebować żadnych modyfikacji, dzięki czemu uzyskasz luźne połączenie. Gdy Twoi konsumenci muszą kodować implementację zamiast umowy, jest to ścisłe powiązanie i jest wymagane w przypadku naruszenia LSP.
Jimmy Hoffa
1
@ user949300 Sukces każdego oprogramowania w realizacji zadania nie jest miarą jego jakości, kosztów długoterminowych ani krótkoterminowych. Zasady projektowania to próby wprowadzenia wytycznych w celu zmniejszenia długoterminowych kosztów oprogramowania, a nie sprawienia, by oprogramowanie działało. Ludzie mogą przestrzegać wszystkich zasad, których pragną, wciąż nie wdrażając działającego rozwiązania lub nie stosować się do nich i wdrażać działającego rozwiązania. Chociaż kolekcje java mogą działać dla wielu osób, nie oznacza to, że koszt pracy z nimi na dłuższą metę jest tak tani, jak mógłby być.
Jimmy Hoffa
13

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

  • W podtypie nie można wzmocnić warunków wstępnych.
  • Warunki podrzędne nie mogą zostać osłabione w podtypie.
  • Niezmienniki nadtypu muszą być zachowane w podtypie.

Jeśli któryś z nich się nie utrzyma, dzwoniący może otrzymać wynik, którego się nie spodziewał.

Zavior
źródło
1
Czy potrafisz wymyślić jakieś konkretne przykłady, które mogą to wykazać?
Mark Booth,
1
@MarkBooth Problem z elipsą koła / prostokątem kwadratowym może być przydatny do jego zademonstrowania; artykuł w Wikipedii jest dobrym miejscem do rozpoczęcia: en.wikipedia.org/wiki/Circle-ellipse_problem
Ed Hastings
7

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:

Ellipse.set_alpha_radius(d)
Ellipse.set_beta_radius(d)

Oczywiście należy je ponownie zdefiniować dla Koła, ponieważ Okrąg ma jednolity promień. Masz dwie możliwości:

  1. Po wywołaniu set_alpha_radius lub set_beta_radius, oba są ustawione na tę samą kwotę.
  2. Po wywołaniu set_alpha_radius lub set_beta_radius obiekt nie jest już okręgiem.

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ę:

some_function(Ellipse byref e)

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.

Kaz Dragon
źródło
1
Myślę, że możesz wpaść w kłopoty, jeśli używasz zmiennych obiektów. Okrąg jest także elipsą. Ale jeśli zamienisz elipsę, która jest także okręgiem, na inną elipsę (co robisz za pomocą metody ustawiającej), nie ma gwarancji, że nowa elipsa będzie również okręgiem (koła są odpowiednim podzbiorem elips).
Giorgio
2
W świecie czysto funkcjonalnym (z niezmiennymi obiektami) metoda set_alpha_radius (d) miałaby elipsę typu zwracanego (zarówno w elipsie, jak i w klasie okręgu).
Giorgio
@Giorgio Tak, powinienem wspomnieć, że ten problem występuje tylko w przypadku obiektów zmiennych.
Kaz Dragon
@KazDragon: Dlaczego ktokolwiek miałby zamieniać elipsę na obiekt koła, skoro wiemy, że elipsa NIE JEST okręgiem? Jeśli ktoś to zrobi, nie będzie miał właściwego zrozumienia bytów, które próbuje modelować. Ale czy pozwalając na tę zamianę, czy nie zachęcamy do luźnego zrozumienia systemu bazowego, który próbujemy modelować w naszym oprogramowaniu, tworząc w ten sposób złe oprogramowanie?
Maverick
@ maverick Uważam, że przeczytałeś relację, którą opisałem wstecz. Proponowany związek to odwrotność: okrąg jest elipsą. W szczególności okrąg jest elipsą, w której promienie alfa i beta są identyczne. Oczekuje się zatem, że każda funkcja oczekująca elipsy jako parametru mogłaby równie dobrze zatoczyć koło. Zastanów się: replace_area (elipsa). Podanie do tego okręgu dałoby ten sam wynik. Problem polega jednak na tym, że zachowanie funkcji mutacji Ellipse nie jest możliwe do zastąpienia przez funkcje w Circle.
Kaz Dragon
6

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.

Tulains Córdova
źródło
2
-1:
ogólnie
3
@Thomas Nie zgadzam się. To dobra analogia. Mówi o tym, że nie łamie oczekiwań, o co chodzi w LSP. (choć część dotycząca sprawy / przełącznika jest nieco słaba, zgadzam się)
Andres F.,
2
A potem Apple złamał LSP, zmieniając złącza. Ta odpowiedź trwa.
Mag
Nie rozumiem, co instrukcje switch mają wspólnego z LSP. jeśli masz na myśli zamianę, typeof(someObject)aby zdecydować, co możesz zrobić, to na pewno, ale to zupełnie inna anty-wzorzec.
sara
Drastyczne zmniejszenie liczby instrukcji zamiany jest pożądanym efektem ubocznym LSP. Ponieważ obiekty mogą oznaczać dowolny inny obiekt, który rozszerza ten sam interfejs, nie trzeba zajmować się specjalnymi przypadkami.
Tulains Córdova
1

aby dać przykład z życia za pomocą UndoManagera Java

dziedziczy, z AbstractUndoableEditktórego kontraktu wynika, że ​​ma 2 stany (cofnięte i powtórzone) i może przechodzić między nimi pojedynczymi połączeniami do undo()iredo()

jednak UndoManager ma więcej stanów i działa jak bufor cofania (każde wywołanie cofania undoniektó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 edycji

Wyrzuciłem własny, UndoManageraby tego uniknąć (prawdopodobnie powinienem zmienić jego nazwę na UndoBuffer)

maniak zapadkowy
źródło
1

Przykład: Pracujesz z frameworkiem interfejsu użytkownika i tworzysz własne niestandardowe sterowanie interfejsem użytkownika, podklasując Controlklasę podstawową. ControlKlasa bazowa określa sposób getSubControls(), 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ć.

JacquesB
źródło
0

Możesz także na to spojrzeć z modelowego punktu widzenia. Kiedy mówisz, że instancja klasy Ajest również instancją klasy B, sugerujesz, że „obserwowalne zachowanie instancji klasy Amożna również sklasyfikować jako obserwowalne zachowanie instancji klasy B” (Jest to możliwe tylko wtedy, gdy klasa Bjest mniej konkretna niż klasa A.)

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?

Giorgio
źródło
0

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 z Class B. Class Ai Class Budostępniamy kilka właściwości, które Class Azastępują własną implementację. Ustawienie lub uzyskanie Class Awłaściwości ma inny efekt niż ustawienie lub pobranie dokładnie tej samej właściwości Class B.

public Class A
{
    public virtual string Name
    {
        get; set;
    }
}

Class B : A
{
    public override string Name
    {
        get
        {
            return TranslateName(base.Name);
        }
        set
        {
            base.Name = value;
            FunctionWithSideEffects();
        }
    }
}

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 Namejest 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 Aa Class B, więc nie mogę po prostu zrobić Class Aabstrakcyjny, aby zmusić ludzi do używania Class B.

Istnieje kilka bardzo przydatnych funkcji narzędziowych, które działają Class Ai 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ć na Class Ana Class B. Wiele funkcji, które podejmują, Class Bmoże łatwo przyjąć, Class Agdyby 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ć NameTranslatedwłaściwość, która będzie Class Bwersją Namewłaściwości i bardzo, bardzo ostrożnie zmieniać każde odwołanie do Namewłaściwości pochodnej, aby użyć mojej nowej NameTranslatedwł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.

Stephen
źródło
Co by się stało, gdybyś w klasie pochodnej przesłaniał odziedziczoną właściwość innym rodzajem rzeczy o tej samej nazwie [np. Klasa zagnieżdżona] i stworzył nowe identyfikatory BaseNameoraz TranslatedNamemiał dostęp do stylu klasy A Namei znaczenia klasy B? Wtedy każda próba dostępu Namedo zmiennej typu Bzostanie 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.
supercat
Nie pracuję już w tym miejscu. Naprawienie tego byłoby bardzo niewygodne. :-)
Stephen
-4

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.

sgud
źródło
1
To po prostu otwiera więcej pytań niż odpowiedź.
Frank