Jaki jest przykład zasady substytucji Liskova?

Słyszałem, że zasada substytucji Liskov (LSP) jest podstawową zasadą projektowania obiektowego. Co to jest i jakie są przykłady jego zastosowania?


person NotMyself    schedule 11.09.2008    source źródło
comment
Więcej przykładów przestrzegania i naruszeń LSP tutaj   -  person StuartLC    schedule 15.05.2015
comment
To pytanie ma nieskończenie wiele dobrych odpowiedzi, więc jest zbyt szerokie.   -  person Raedwald    schedule 16.12.2018
comment
12 lat później byłoby wspaniale, gdybyś mógł odrzucić własną odpowiedź i zaakceptować inną, ponieważ Twoja własna odpowiedź jest niestety całkowicie błędna i nie opisuje LSP, ale zamiast tego niedopasowanie interfejsu. A ponieważ jest zaakceptowany, wprowadza w błąd wiele osób (na co wskazuje liczba głosów za).   -  person Konrad Rudolph    schedule 13.10.2020
comment
To działa dla mnie. Przełączono na najpopularniejszą odpowiedź.   -  person NotMyself    schedule 13.10.2020
comment
liczba tysięcy głosów!   -  person Jonas Grønbek    schedule 06.06.2021


Odpowiedzi (33)


Świetnym przykładem ilustrującym LSP (podany przez wujka Boba w podcastie, który ostatnio słyszałem) jest to, że czasami coś, co brzmi dobrze w języku naturalnym, nie do końca działa w kodzie.

W matematyce Square to Rectangle. Rzeczywiście jest to specjalizacja prostokąta. „Jest a” sprawia, że ​​chcesz modelować to za pomocą dziedziczenia. Jeśli jednak w kodzie Square pochodzi od Rectangle, to Square powinno być użyteczne wszędzie tam, gdzie oczekujesz Rectangle. To powoduje dziwne zachowanie.

Wyobraź sobie, że masz metody SetWidth i SetHeight w swojej klasie bazowej Rectangle; wydaje się to całkowicie logiczne. Jeśli jednak odwołanie Rectangle wskazywało na Square, to SetWidth i SetHeight nie mają sensu, ponieważ ustawienie jednego zmieniłoby drugie, aby je dopasować. W tym przypadku Square nie zaliczy testu podstawienia Liskov z Rectangle, a abstrakcja posiadania Square dziedziczenia z Rectangle jest zła.

tu wpisz opis obrazu

Wszyscy powinniście sprawdzić inne bezcenne Plakaty motywacyjne z zasadami SOLID.

person m-sharp    schedule 25.02.2009
comment
@m-sharp A co, jeśli jest to niezmienny prostokąt taki, że zamiast SetWidth i SetHeight mamy metody GetWidth i GetHeight? - person Pacerier; 26.04.2012
comment
Morał z tej historii: modeluj swoje klasy w oparciu o zachowania, a nie właściwości; modelować swoje dane na podstawie właściwości, a nie zachowań. Jeśli zachowuje się jak kaczka, to z pewnością jest ptakiem. - person Sklivvz; 20.05.2012
comment
Cóż, kwadrat wyraźnie JEST rodzajem prostokąta w prawdziwym świecie. To, czy możemy to zamodelować w naszym kodzie, zależy od specyfikacji. LSP wskazuje, że zachowanie podtypu powinno być zgodne z zachowaniem typu podstawowego, zgodnie z definicją w specyfikacji typu podstawowego. Jeśli specyfikacja podstawowego typu prostokąta mówi, że wysokość i szerokość można ustawić niezależnie, to LSP mówi, że kwadrat nie może być podtypem prostokąta. Jeśli specyfikacja prostokąta mówi, że prostokąt jest niezmienny, to kwadrat może być podtypem prostokąta. Chodzi o podtypy zachowujące zachowanie określone dla typu podstawowego. - person SteveT; 24.09.2012
comment
@Pacerier nie ma problemu, jeśli jest niezmienny. Rzeczywisty problem polega na tym, że nie modelujemy prostokątów, ale prostokąty, które można modyfikować, czyli takie, których szerokość lub wysokość można zmienić po utworzeniu (i nadal uważamy, że jest to ten sam obiekt). Jeśli spojrzymy na klasę prostokąta w ten sposób, jasne jest, że kwadrat nie jest prostokątem, który można zmienić, ponieważ kwadrat nie może zostać zmieniony i nadal jest kwadratem (ogólnie). Matematycznie nie widzimy problemu, ponieważ zmienność nie ma nawet sensu w kontekście matematycznym. - person asmeurer; 20.01.2013
comment
Z wykładu prof. Barbary Liskov: Obiekty podtypów powinny zachowywać się jak te z nadtypów, jeśli są używane przez metody nadtypowe. - person ruhong; 04.02.2015
comment
Jeśli szerokość i wysokość mogą być zmieniane przez ustawiacze, powinna istnieć tylko klasa prostokąta, a nie specjalna klasa kwadratowa. Zamiast tego klasa prostokąta powinna mieć getter o nazwie IsSquare. Ilekroć szerokość i wysokość mają te same wartości, IsSquare zwróci true, w przeciwnym razie false. Typy nie zawsze są statyczne, ale czasami – jak w tym przypadku – mogą się zmieniać. - person brighty; 10.02.2015
comment
dlaczego więc potrzebuję podtypowania, jeśli zachowanie mojej odziedziczonej klasy nie różni się od zachowania rodzica (z wyjątkiem przypadków, gdy robi to samo, ale inaczej)? po co wszystkie te metody override, skoro muszą zachowywać się zupełnie podobnie do bazy? mówiąc o figurach geometrycznych: IDrawable ma Draw(). jak możesz nalegać, aby Draw() w Circle:IDrawable dawał taki sam wynik jak w przypadku Square:IDrawable? lub powiedzmy Rotated90DegreesSquare:Square rysować tak samo jak kwadrat? - person jungle_mole; 11.09.2015
comment
Mam jedno pytanie dotyczące zasady. Dlaczego miałby być problem, gdyby Square.setWidth(int width) został zaimplementowany w następujący sposób: this.width = width; this.height = width;? W tym przypadku gwarantuje się, że szerokość jest równa wysokości. - person MC Emperor; 28.10.2015
comment
Ten plakat motywacyjny nie ma sensu. Powinno być Jeśli wygląda jak kaczka, szarlata się jak kaczka, ale potrzebuje baterii ... dlaczego znowu interesujesz się bateriami? Bo w końcu oznacza to, że jedynym zamiennikiem kaczki jest inna kaczka (najlepiej dokładnie ta sama). Taki radykalizm jest autodestrukcyjny. - person David Tonhofer; 05.01.2016
comment
Problem kwadrat-prostokąt jest również znany jako Problem koło-elipsy. - person mbx; 02.03.2016
comment
Dobra czy zła, dobra czy zła, zależy od czyjejś perspektywy - czy to od strony konsumenta kodu, czy od strony autora kodu. LSP, IMO, dotyczy strony konsumenckiej. Patrząc z właściwej perspektywy, wiele zła wygląda dobrze. - person Sudhir; 23.05.2016
comment
Innym rozwiązaniem jest zdefiniowanie prostokątów jako zachowujących proporcje, a nie jako posiadających niezależne szerokości i wysokości. - person Ed L; 16.07.2016
comment
Wydaje się nie odpowiadać na pytanie. Po przeczytaniu nadal nie wiem, czym jest LSP (chyba, że ​​plakat zawiera definicję, choć biorąc pod uwagę kontekst takich plakatów, nie jest to jasne). - person iheanyi; 12.08.2016
comment
Nie rozumiem też kwadratowego przykładu. Jak setWidth i setHeight nie mają sensu na kwadracie. Jak łatwo wytłumaczyłeś, robienie jednego implikuje drugie - to tylko definicja kwadratu. Nie wyjaśnia, dlaczego jest coś złego lub niespójnego w zastępowaniu prostokąta kwadratem. - person iheanyi; 12.08.2016
comment
Czy nie można po prostu nadpisać setWidth lub setHeight iw definicji wywołać drugą metodę z tym samym parametrem i pufem, wszystko działa, a zachowanie jest spójne? - person Ungeheuer; 22.11.2016
comment
@MCemperor jeśli zmienisz implementację setHeight() i setWidth() w Square, więc miejsca w twoim kodzie, w których używasz Rectangule nie będą już działać, jeśli przekażesz Square i to jest główny punkt dotyczący LSP; - person sdlins; 26.12.2016
comment
Dziękuję za odpowiedź - ale co z tego, jeśli LSP jest zepsute? - person BKSpurgeon; 02.01.2017
comment
Jednak obraz NIE jest prawidłowym przykładem zepsutego LSP (właściwie to zapach Reddita, a to nie jest coś, co chcemy powąchać tutaj na SO). Podklasy zawsze będą bardziej szczegółowe niż nadklasy. Jednak ich specyfika niekoniecznie łamie LSP. Chodzi o to, czy te specyfiki wpływają (i łamią) wspólną umowę. To, czy kaczka zasilana bateryjnie złamie wspólny kontrakt abstrakcyjnej kaczki, zależy od konkretnych szczegółów projektu. - person AnT; 28.01.2017
comment
Każdy używa tego kwadratu vs prostokąta, co jest tak okropnym przykładem. Zastępujesz właściwość Width, aby ustawić właściwość Height, co wydaje się bardziej przypominać efekt uboczny. To wszystko zostałoby rozwiązane, gdybyś kazał ludziom wyraźnie ustawić szerokość i wysokość. - person johnny 5; 01.03.2017
comment
Zarówno obraz, jak i link na końcu są zepsute. Czy masz coś przeciwko temu, aby to naprawić? - person PaulB; 05.06.2017
comment
Dzięki @PaulB. Naprawione. - person m-sharp; 06.06.2017
comment
Los techies teraz pracują dla mnie, @m-sharp. Czy to tylko chwilowa awaria, a może nawet regionalna? Wersja 8 wydaje się teraz przynajmniej niepotrzebna. - person Palec; 07.06.2017
comment
Chłodny. Cofnąłem zmianę. SO ma naprawdę fajną historię zmian! - person m-sharp; 07.06.2017
comment
Cholera, cała strona Lot Techies zwraca teraz tę samą stronę zatytułowaną Błąd bazy danych i zawierającą tylko h1 z błędem nawiązania połączenia z bazą danych. Znalazłem najnowsza migawka WayBack Machine i wyraźnie mówi, że obrazy są objęte licencją CC BY-SA. Proponuję umieścić link do migawki WayBack Machine, a także do oryginalnego adresu URL i przesłać obraz na serwery Stack Overflow (za pośrednictwem edytora). W ten sposób przerwy nie wpłyną na pocztę. - person Palec; 10.06.2017
comment
o.setDimensions(width, height) rozwiązałoby ten problem, ale LSP nadal byłoby naruszone, ponieważ kwadrat ma silniejsze warunki wstępne (width == height) niż prostokąt. Nie sądzę, aby Twój post zawierał cokolwiek na temat LSP. - person inf3rno; 09.10.2017
comment
Tutaj znajdziesz dobre informacje uzupełniające na temat nadpisania metody / umów softwareengineering.stackexchange.com/a/244783/1451 - person BrunoLM; 21.10.2017
comment
Czy prostokąt jest rodzajem kwadratu, czy kwadrat jest rodzajem prostokąta, czy na odwrót? Prostokąt może być zdefiniowany jako kwadrat o nierównych bokach, a kwadrat może być zdefiniowany jako prostokąt o równych bokach. Kwadrat o nierównych bokach nie jest kwadratem, ale prostokąt o równych bokach nadal jest prostokątem i kwadratem. Czyli kwadrat to prostokąt z dodatkową umową. Dlatego interfejs dla kwadratu powinien być taki sam jak dla prostokąta. Ustawienie jednego wymiaru powinno wyznaczać drugi ma sens. - person ATL_DEV; 15.12.2017
comment
Nie powiedziałbym, że nie ma problemu, jeśli są niezmienne. Może to zniechęcić czytelnika do zobaczenia GetHeight i GetWidth, czy zamierzają zrobić dokładnie to samo. Może to drobny problem, ale wciąż jest to powód, aby zrobić to w inny sposób. - person AustinWBryan; 09.05.2018
comment
Czy w niektórych książkach nazywa się regułą %100? - person anilkay; 17.06.2018
comment
@AustinWBryan nie, to są dobrze określone ilości. Kwadrat ma zarówno wysokość, jak i szerokość; zdarzają się, że są równe. Jeśli czytelnik jest tym zbity z tropu, to albo dowiaduje się czegoś nowego i ważnego o kodzie, albo do niego nie pasuje :) - person Ben Kushigian; 19.06.2018
comment
Myślę, że mylimy podtyp z konkretnym przypadkiem. Kwadrat nie różni się zachowaniem od prostokąta. Obwód, pole powierzchni można obliczyć przy użyciu tych samych wzorów. Kwadrat nie jest ulepszeniem ani udoskonaleniem prostokąta, to tylko szczególny przypadek. Kwadrat nie powinien być podklasą prostokąta, nie powinien nawet być oddzielną klasą. - person Mircea Ion; 05.02.2019
comment
@MirceaIon, Square może być podklasą Rectangle, aby uprościć interfejs, np. z SetLength, który może wewnętrznie wywoływać SetWidth i SetHeight - person alancalvitti; 29.03.2019
comment
Można wyprowadzić Square z Rectangle, ale nie powinieneś mieć w tym przypadku setWidth i setHeight. Ale nikt nie mógł przeszkodzić ci w posiadaniu ustawiaczy dla strefy, w której umieścisz prostokąty. W takim przypadku Square wypełni tylko część strefy, podczas gdy Rectangle wypełni ją całkowicie. :) - person Alex; 17.04.2019
comment
Czy ktoś może, z miłości do kodu, powiedzieć mi, jak inaczej modelować ten sam przykład, aby był zgodny z LSP? - person Saurabh Goyal; 11.06.2019
comment
Kiedyś oblałem wywiad na dokładnie to pytanie o kwadraty. Prowadzący wywiad odrzucił moje bezpośrednie pytanie: Co zrobimy z tymi prostokątami i kwadratami? Dziś cieszę się, że nie dostałem tej pracy. :) - person Aleksei Guzev; 15.12.2019
comment
Który wujek Bob? :-? - person Yousha Aleayoub; 05.02.2020
comment
@sdlins Nie zgadzam się. Czy możesz wymienić jedną sytuację, w której zachowanie funkcji setHeight i setWidth zostało nadpisane w klasie Square, np. MCEmperor, że kod nie będzie już działał dla Reactangle? - person Murilo; 09.04.2020
comment
O wiele lepiej byłoby podać przykładowy przypadek użycia i fragment kodu, który zademonstrowałby kod. - person Giorgi Tsiklauri; 06.08.2020
comment
@Murilo Musi wypełnić nasze pudełko 640x480, więc r.SetWidth (640); r.SetHeight(480) lub odwrotnie, nie ma znaczenia. Każda podklasa, która oferuje możliwość oddzielnego ustawiania szerokości i wysokości, ale w rzeczywistości tego nie robi, psuje rzeczy. - person prosfilaes; 13.01.2021
comment
Problem z metodami SetWidth i SetHeight polega na tym, że nie tylko łamią LSP, ale także ISP (zasada segregacji interfejsów), więc powinny być segregowane na dwa różne interfejsy, być może SetSize(int, int) dla Rectangle i SetSize(int) dla Square, ponieważ są inni, Getters mogą pozostać tam, gdzie są… otwarci na dyskusję - person Eugenio Miró; 28.01.2021
comment
Jeśli chodzi o kaczki, baterie i dlaczego łamie LSV -› ponieważ kaczki z definicji nie potrzebują baterii. Dla mnie jest to dość oczywiste i jest to metafora w formie żartu, którego nie należy traktować poważniej. - person Ric Jafe; 17.03.2021
comment
@SaurabhGoyal - chodzi o to, aby poprawna implementacja nie miała skutków ubocznych jednego gettera do innej właściwości, ponieważ rodzic nie miał ich w pierwszej kolejności. Na przykład, nie możesz mieć getterów dla indywidualnej wysokości i szerokości, a następnie mieć je jako parę i pojedynczy setDimentions(x,y) i w ten sposób możesz uniknąć tego specyficznego efektu ubocznego zmiany wysokości podczas ustawiania szerokości w przykładzie kwadratu . Z pewnością istnieje wiele sposobów na osiągnięcie tego. Najważniejsze jest to, że zachowanie dziecka różniłoby się od zachowania rodzica. To właśnie łamie LSP. - person Ric Jafe; 17.03.2021
comment
To pomogło mi zrozumieć więcej niż jakikolwiek inny przykład - person Tom; 24.05.2021

Liskov Substitution Principle (LSP, lsp) jest koncepcja programowania zorientowanego obiektowo, która stwierdza:

Funkcje korzystające ze wskaźników lub odwołań do klas bazowych muszą mieć możliwość korzystania z obiektów klas pochodnych bez wiedzy o tym.

W swoim sercu LSP dotyczy interfejsów i kontraktów, a także tego, jak decydować, kiedy rozszerzyć klasę, czy użyć innej strategii, takiej jak kompozycja, aby osiągnąć swój cel.

Najskuteczniejszy sposób, jaki widziałem, aby zilustrować ten punkt, to Najpierw OOA&D. Przedstawiają scenariusz, w którym jesteś deweloperem projektu, którego celem jest zbudowanie frameworka do gier strategicznych.

Przedstawiają klasę reprezentującą tablicę, która wygląda tak:

Diagram klas

Wszystkie metody przyjmują współrzędne X i Y jako parametry, aby zlokalizować pozycję kafelka w dwuwymiarowej tablicy Tiles. Umożliwi to twórcy gry zarządzanie jednostkami na planszy w trakcie gry.

Książka dalej zmienia wymagania, mówiąc, że ramy gry muszą również obsługiwać plansze do gier 3D, aby pomieścić gry, które mają lot. Wprowadzono więc klasę ThreeDBoard, która rozszerza Board.

Na pierwszy rzut oka wydaje się to dobrą decyzją. Board udostępnia zarówno właściwości Height, jak i Width, a ThreeDBoard udostępnia oś Z.

Załamuje się, gdy spojrzysz na wszystkie inne elementy odziedziczone po Board. Wszystkie metody dla AddUnit, GetTile, GetUnits itd. przyjmują zarówno parametry X, jak i Y w klasie Board, ale ThreeDBoard również wymaga parametru Z.

Musisz więc ponownie zaimplementować te metody z parametrem Z. Parametr Z nie ma kontekstu dla klasy Board, a metody odziedziczone z klasy Board tracą znaczenie. Jednostka kodu próbująca użyć klasy ThreeDBoard jako swojej klasy bazowej Board miałaby pecha.

Może powinniśmy znaleźć inne podejście. Zamiast rozszerzać Board, ThreeDBoard powinno składać się z Board obiektów. Jeden obiekt Board na jednostkę osi Z.

Pozwala nam to na stosowanie dobrych zasad obiektowych, takich jak enkapsulacja i ponowne użycie, i nie narusza LSP.

person NotMyself    schedule 11.09.2008
comment
Zobacz także Problem koło-elipsy w Wikipedii, aby zapoznać się z podobnym, ale prostszym przykładem. - person Brian; 21.10.2011
comment
Zacytuj @NotMySelf: Myślę, że przykład ma po prostu zademonstrować, że dziedziczenie z tablicy nie ma sensu w kontekście ThreeDBoard, a wszystkie sygnatury metod są bez znaczenia w przypadku osi Z.. - person Contango; 05.06.2013
comment
Cytat z @Chris Ammerman: Ocena przestrzegania LSP może być świetnym narzędziem do określenia, kiedy kompozycja jest bardziej odpowiednim mechanizmem rozszerzania istniejącej funkcjonalności, a nie dziedziczenia. - person Contango; 05.06.2013
comment
Więc jeśli dodamy kolejną metodę do klasy Child, ale cała funkcjonalność Parent nadal ma sens w klasie Child, czy nie będzie to łamanie LSP? Ponieważ z jednej strony zmodyfikowaliśmy interfejs, aby nieco używać Dziecka, z drugiej strony, jeśli rzucimy Dziecko na Rodzica, kod, który oczekuje Rodzica, będzie działał dobrze. - person Nickolay Kondratyev; 18.06.2013
comment
To jest przykład anty-Liskov. Liskov każe nam wyprowadzić Prostokąt z Kwadratu. More-parameters-class od less-parameters-class. I ładnie pokazałeś, że jest źle. To naprawdę dobry żart, że oznaczono jako odpowiedź i otrzymaliśmy 200-krotną przewagę nad odpowiedzią anty-liskov na pytanie liskov. Czy zasada Liskowa jest naprawdę błędem? - person Gangnus; 18.10.2015
comment
@Contango A czy możesz podać przykład, w którym dziedziczenie jest dobre, zgodnie z cytatem w odpowiedzi tutaj? - person Gangnus; 18.10.2015
comment
Widziałem, jak dziedziczenie działa w niewłaściwy sposób. Oto przykład. Klasą bazową powinna być 3DBoard, a klasa pochodna Board. Płytka nadal ma oś Z o wartości Max(Z) = Min(Z) = 1 - person Paulustrious; 05.08.2017
comment
@Nickolay Kondratyev, tak, nie łamie LSP, sam to rozwiązałeś. - person brgs; 12.07.2018
comment
Czy możemy rozwiązać problem bez użycia kompozycji? Myślę, że można to zrobić zmieniając podpis z GetUnits(int x, int y) na GetUnits(Position pos) i to samo dotyczy innych funkcji. W ten sposób nie naruszy LSP. Popraw mnie, jeśli się mylę. - person du369; 22.04.2020
comment
@Paulustrious to hamuje zasadę otwarte-zamknięte. Co jeśli chcesz FourDBBoard? Musiałbyś uczynić to klasą bazową ThreeDBoard, zmieniając implementację ThreeDBoard (możesz nawet nie mieć dostępu). - person Jupiter; 19.07.2020
comment
Numpy rozwiązał dokładnie ten problem za pomocą ndarray. Tablica 3-wymiarowa, 2-wymiarowa i 1-wymiarowa to wszystkie specjalizacje uogólnionej tablicy n-wymiarowej. Matematycy odkryli to wieki temu. Programiści stopniowo wymyślają algebrę na nowo. - person Adam Acosta; 22.12.2020

Substytucyjność to zasada programowania obiektowego mówiąca, że ​​w programie komputerowym, jeśli S jest podtypem T, to obiekty typu T mogą być zastąpione obiektami typu S

Zróbmy prosty przykład w Javie:

Zły przykład

public class Bird{
    public void fly(){}
}
public class Duck extends Bird{}

Kaczka potrafi latać, bo jest ptakiem, ale co z tym:

public class Ostrich extends Bird{}

Struś to ptak, ale nie umie latać, klasa Strusia to podtyp klasy Bird, ale nie powinien umieć używać metody muchowej, czyli łamiemy zasadę LSP.

Dobry przykład

public class Bird{}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{} 
person Maysara Alhindi    schedule 04.07.2017
comment
Ładny przykład, ale co byś zrobił, gdyby klient miał Bird bird. Musisz rzucić obiekt na FlyingBirds, aby wykorzystać muchę, co nie jest miłe, prawda? - person Moody; 20.11.2017
comment
Nie. Jeśli klient ma Bird bird, oznacza to, że nie może użyć fly(). Otóż ​​to. Przekazanie Duck nie zmienia tego faktu. Jeśli klient ma FlyingBirds bird, to nawet jeśli zostanie przekazany Duck, powinien zawsze działać w ten sam sposób. - person Steve Chamaillard; 18.02.2018
comment
Czy nie byłoby to również dobrym przykładem dla Segregacji Interfejsów? - person Saharsh; 05.03.2019
comment
Doskonały przykład Dzięki Man - person Abdelhadi Abdo; 28.05.2019
comment
Co powiesz na użycie interfejsu „Flyable” (nie mogę wymyślić lepszej nazwy). W ten sposób nie angażujemy się w tę sztywną hierarchię.. Chyba że wiemy, że naprawdę tego potrzebujemy. - person Thirdy; 28.05.2019
comment
Weterynarz, który leczy złamane skrzydła, zobaczy Zły przykład jako lepszy. Niektóre nielotne gatunki ptaków potrafią latać po swojemu (kurczak jest tutaj hybrydą). Również zdolność latania zmienia się w ewolucji. Domyślna metoda lotu obsługująca NULL jest dobrą decyzją, którą należy później zmienić. - person Sławomir Lenart; 31.01.2020
comment
Tak więc klasa rodzicielska powinna zawierać tylko zachowania, które mają całe jej dzieci, czyli innymi słowy, nie ograniczać zachowań dzieci. - person Ari; 25.02.2020
comment
Wreszcie zrozumiany po godzinie poszukiwań! - person Boshra N; 17.11.2020

LSP dotyczy niezmienników.

Klasyczny przykład podaje następująca deklaracja pseudokodu (pominięto implementacje):

class Rectangle {
    int getHeight()
    void setHeight(int value) {
        postcondition: width didn’t change
    }
    int getWidth()
    void setWidth(int value) {
        postcondition: height didn’t change
    }
}

class Square extends Rectangle { }

Teraz mamy problem, chociaż interfejs się zgadza. Powodem jest to, że naruszyliśmy niezmienniki wynikające z matematycznej definicji kwadratów i prostokątów. Sposób działania getterów i setterów Rectangle powinien spełniać następujący niezmiennik:

void invariant(Rectangle r) {
    r.setHeight(200)
    r.setWidth(100)
    assert(r.getHeight() == 200 and r.getWidth() == 100)
}

Jednak ten niezmiennik (jak również wyraźne warunki końcowe) muszą zostać naruszone przez poprawną implementację Square, dlatego nie jest prawidłowym substytutem Rectangle.

person Konrad Rudolph    schedule 12.09.2008
comment
I stąd trudności w używaniu OO do modelowania czegokolwiek, co chcemy faktycznie modelować. - person DrPizza; 30.11.2009
comment
@DrPizza: Absolutnie. Jednak dwie rzeczy. Po pierwsze, takie relacje mogą nadal być modelowane w OOP, aczkolwiek niecałkowicie lub przy użyciu bardziej złożonych objazdów (wybierz to, co odpowiada Twojemu problemowi). Po drugie, nie ma lepszej alternatywy. Inne odwzorowania/modelowanie mają te same lub podobne problemy. ;-) - person Konrad Rudolph; 30.11.2009
comment
@KonradRudolph, To dobry przykład. Jaki jest zatem właściwy sposób zadeklarowania Square? - person ca9163d9; 24.01.2012
comment
@NickW W niektórych przypadkach (ale nie w powyższym) można po prostu odwrócić łańcuch dziedziczenia – logicznie rzecz biorąc, punkt 2D to punkt 3D, w którym trzeci wymiar jest pomijany (lub 0 – wszystkie punkty leżą na tej samej płaszczyźnie przestrzeń 3D). Ale to oczywiście nie jest praktyczne. Ogólnie rzecz biorąc, jest to jeden z przypadków, w których dziedziczenie tak naprawdę nie pomaga, a między podmiotami nie istnieje żaden naturalny związek. Modeluj je osobno (przynajmniej nie znam lepszego sposobu). - person Konrad Rudolph; 24.01.2012
comment
OOP ma na celu modelowanie zachowań, a nie danych. Twoje zajęcia naruszają enkapsulację jeszcze przed naruszeniem LSP. - person Sklivvz; 20.05.2012
comment
@Sklivvz Zgadzam się. Trudno było jednak wymyślić zwięzły przykład, a ten jest standardowym przykładem LSP (znajdziesz go w kilku książkach). Ale tak, chociaż jest to powszechne, jest to dalekie od dobrego OOP. - person Konrad Rudolph; 20.05.2012
comment
Metody setHeight() i setWidth() prawdopodobnie nie powinny istnieć, ponieważ powoduje to zmienną klasę. Gdyby jednak istniały, można by po prostu throw NotSupportedException() / NotImplementedException() lub coś podobnego. - person Leonid; 03.07.2012
comment
@Leonid W pewnym sensie to również zrywa umowę, ponieważ zmienia „podpis” (czyli możliwe sposoby, w jakie funkcja może wyjść). Teraz niektóre frameworki (ahem .NET…) rutynowo używają tej trasy, ale uważam, że jest to bardzo zły projekt interfejsu. - person Konrad Rudolph; 03.07.2012
comment
Nie rozumiem, jak naruszono tutaj LSP. Niezmienny oznacza, że ​​getheight i getwidth powinny zwracać ustaloną wysokość i ustaloną szerokość, co jest zawsze prawdziwe, niezależnie od tego, czy prostokąt jest kwadratem czy prostokątem. Jeśli klient ustawi inną wysokość i szerokość, na pewno wie, że przedmiot nie jest kwadratem. Podobnie, jeśli klient wiedział, że obiekt jest kwadratowy, ustawi równą wysokość i szerokość. Zakładając, że klasa prostokąta ma dwie funkcje pole i obwód, funkcje będą działać poprawnie bez względu na to, czy prostokąt jest kwadratem, czy nie. Więc jak to narusza LSP? - person nurabha; 05.06.2013
comment
@nurabha „co zawsze jest prawdą, niezależnie od pogody, prostokąt jest kwadratem lub prostokątem” – fałsz. Z prawidłowo zakodowanym square to nie zawsze będzie prawdziwe, obaj ustawiający muszą zresetować zarówno szerokość, jak i wysokość, w przeciwnym razie nie zachowają (domniemanych) niezmienników kwadratu. Innymi słowy: masz niespójny system typów i nigdy tego nie chcesz. - person Konrad Rudolph; 06.06.2013
comment
@nurabha Założenie, że prostokąt ma dwie funkcje pole i obwód, doprowadziłoby do tego samego problemu. SetArea(4); SetPerimeter(10); uczyniłoby zwykły model prostokąta prostokątem 1x4. Wywołania sprawią, że model klasy Square najpierw będzie 2x2, a następnie albo zmieni się na kwadrat sqrt(10)xsqrt(10), albo wyrzuci InvalidOperationException lub coś takiego. - person Wolfzoon; 07.08.2016
comment
Jeśli nie ma rzeczywistych niezmienników, jak możesz je naruszyć? Skąd wiadomo, jakie warianty istnieją, aby je naruszyć (lub dostosować)? - person iheanyi; 12.08.2016
comment
@iheanyi Musisz albo jasno określić niezmienniki (czyli asercje, albo użyć systemu typów), albo musisz znać domenę, w której pracujesz. Wiemy, jak definiuje się prostokąty i kwadraty, więc wiemy, jakie niezmienniki nakładają. - person Konrad Rudolph; 13.08.2016
comment
Zgadzam się - albo ograniczenia lub niezmienniki powinny być jawne, albo powinny być narzucone przez system. Ale na podstawie twojego przykładu nic nie zmusza mnie do napisania klasy kwadratowej z domniemanym niezmiennikiem, o którym wspomniałeś. Przypuszczam, że sugerowałbym zaktualizowanie odpowiedzi, aby usunąć sugerowane podczas omawiania niezmienników, są to rzeczywiste ograniczenia, które powodują problem. - person iheanyi; 14.08.2016
comment
LSP nie powinien wiedzieć, co robi obiekt pochodny, tylko że obiekty pochodne honorują kontrakt klasy bazowej. Na przykład Square może mieć SetDepth(int d) => throw exception. Ale zastanawiam się... czy prostokąt jest specjalnym rodzajem kwadratu, który nie wymaga H=W? Alternatywnie..czworokąt ⇒ trapez ⇒ równoległobok. - person Paulustrious; 05.08.2017
comment
Alternatywnie..Quadrilateral ⇒ Trapezoid ⇒ Parallelogram. Prostokąt i Kwadrat zostaną ostatecznie wyprowadzone. od Równoległoboku, chociaż byłoby Równoległobok ⇒ Romb ⇒ Kwadrat. Prawidłowe wyprowadzenie jest jak projektowanie tabel bazy danych. Zrób to źle na początku i zmierzasz do wpadki - person Paulustrious; 05.08.2017
comment
Czy bardziej sensowne byłoby dziedziczenie Rectangle po typie Square, czy też posiadanie Square i Rectangle po prostu klasami rodzeństwa? - person AustinWBryan; 09.05.2018
comment
@AustinWBryan Unikałbyś tego konkretnego problemu, ale relacja nie ma sensu: modele dziedziczenia relacji „jest-a” i „każdy prostokąt jest-kwadrat” są błędne (i w konsekwencji natkniesz się na inne problemy, w których nie można po prostu wymienić jednego na drugie). Odwrotnie jest poprawne („każdy kwadrat jest prostokątem”), dlatego tak kuszące jest pisanie kodu, który narusza LSP. - person Konrad Rudolph; 09.05.2018
comment
@KonradRudolph Czy zatem lepiej byłoby zachować je jako rodzeństwo, które dziedziczy po Shape lub Parallelogram czy coś takiego? - person AustinWBryan; 09.05.2018
comment
@AustinWBryan Tak; im dłużej pracuję w tej dziedzinie, tym częściej używam dziedziczenia tylko dla interfejsów i abstrakcyjnych klas bazowych, a kompozycji dla reszty. Czasami wymaga to trochę więcej pracy (mądre pisanie), ale pozwala uniknąć całej masy problemów i jest szeroko powtarzane przez innych doświadczonych programistów. - person Konrad Rudolph; 09.05.2018
comment
@KonradRudolph Wow, nie miałem pojęcia, chociaż przy ostatnim projekcie miałem problemy z używaniem zbyt dużej ilości dziedziczenia. Zdałem sobie sprawę, że chcę nadać niektórym klasom pochodnym właściwości innych klas pochodnych, a nie było na to sposobu. Czy uważasz, że tylko klasy abstrakcyjne powinny mieć dzieci w większości? - person AustinWBryan; 09.05.2018
comment
@AustinWBryan W większości przypadków tak. Jest to słynna jedna z zasad wpływowych książek o Efektywnym C++, a bardziej ogólna zasada, Kompozycja nad dziedziczeniem, ma swoją własną strona Wikipedii. - person Konrad Rudolph; 09.05.2018
comment
@KonradRudolph Wow, dzięki, sprawdzę to. Czy ta książka nadal jest warta przeczytania dla rozwoju C#, aby poznać struktury i wzorce projektowe? - person AustinWBryan; 09.05.2018
comment
@AustinWBryan Prawdopodobnie nie, niestety; jest bardzo specyficzny dla C++. Jednak Efektywne C# Billa Wagnera podąża za podobnym schematem. - person Konrad Rudolph; 10.05.2018
comment
@ca9163d9 Do modelowania Square z Rectangle, jeśli podejście polimorficzne nie jest konieczne, zalecałbym SquareDecorator, który przyjmuje obiekt Rectangle. Co ty lub ktokolwiek inny myśli o tym? - person Reuel Ribeiro; 11.01.2019
comment
@ReuelRibeiro To idzie w kierunku nadmiernej inżynierii; w większości przypadków lepiej byłoby po prostu zapewnić klasy ortogonalne, które mogą dziedziczyć po Shape. To powiedziawszy, twoje podejście jest z pewnością właściwym rozwiązaniem w wielu rzeczywistych przypadkach użycia. - person Konrad Rudolph; 11.01.2019
comment
Nie jestem przekonany, że to dobry przykład naruszenia LSP. Rozumiem, że jest to poniekąd kanoniczny przykład użyty, ale twierdzenie użyte do udowodnienia naruszenia nie jest otwarte i zamknięte. Tak, kiedy czytasz kod, wydaje się to oczywiste. Po prostu ustawiłem szerokość i wysokość, dlaczego wysokość nie jest taka, na jaką ją ustawiłem? Ale ustawienie szerokości i wysokości to dwie oddzielne operacje, a w końcu powodem, dla którego mamy metody ustawiania właściwości, a nie ujawniania samych właściwości, jest to, że możemy zachować nasze niezmienniki, co może skutkować efektami ubocznymi. - person wired_in; 13.10.2020
comment
W istocie mówię, że chociaż może to być naruszeniem, nie jest to oczywiste, że tak jest, a zatem nie powinno być używane jako przykład dla osób próbujących nauczyć się tego pojęcia. - person wired_in; 13.10.2020
comment
@wired_in Widzę, skąd pochodzisz, ale Twój komentarz ignoruje konkretny kontekst przykładu. Mianowicie, nie wywołujemy tylko starego settera na dowolnym starym typie obiektu. Konkretnie przypisujemy szerokość i wysokość prostokąta. A to wiąże się z ukrytymi oczekiwaniami (niejawnym kontraktem). Oczekiwanie (niejawne) jest takie, że zmiana szerokości nie zmienia wysokości i na odwrót. Są powody, aby zmienić tę umowę (w końcu tak robi kwadrat!). Ale ogólnie są to naturalne oczekiwania. Podstawienie (kwadratem) narusza tę ogólność. - person Konrad Rudolph; 13.10.2020
comment
@KonradRudolph Nie ignorowałem faktu, że twierdzenie było w kontekście prostokąta i nigdy nie powiedziałem, że nie jest to naruszenie LSP. Wcale nie jest oczywiste, że istnieje niejawna umowa, która mówi, że zmiana szerokości prostokąta nie może zmienić wysokości i vice versa. Ponieważ zmiana szerokości jest operacją odrębną od zmiany wysokości, nie widzę niejawnego kontraktu, który mówi, że kwadrat nie może wymusić swoich niezmienników, gdy szerokość lub wysokość jest zmieniana oddzielnie, jako prostokąt. - person wired_in; 13.10.2020
comment
@KonradRudolph Twoja przykładowa asercja wymusiła ustawienie szerokości i wysokości w tym samym czasie przez połączenie dwóch oddzielnych operacji, ale rzeczywisty kontrakt prostokąta ma te operacje jako oddzielne, co sprawia, że ​​cały ten przykład jest raczej ćwiczeniem interpretacji niż konkretne naruszenie LSP - person wired_in; 13.10.2020
comment
@wired_in „Nie widzę niejawnej umowy” — nie widzisz jej, ponieważ jest niejawna. Możesz to uczynić jawnie, dodając asercje do ustawiających klas bazowych Rectangle, które zapewniają, że inna wartość nie zostanie zmodyfikowana. Ale fakt, że kwadraty mają różne niejawne kontrakty, jest właśnie powodem, dla którego jest to naruszenie LSP: oba są ważne w izolacji, ale kwadrat nie jest prawidłowym podtypem prostokąta, ponieważ ich niejawne kontrakty nie są kompatybilny. - person Konrad Rudolph; 13.10.2020
comment
@wired_in Dodałem wyraźne warunki końcowe do mojej deklaracji klasy Rectangle. Ale uczciwe ostrzeżenie: może w końcu usunę je ponownie, ponieważ są sztuczne i w każdym razie zostały już przechwycone przez metodę przykładową invariant. Co więcej, LSP w swojej pierwotnej definicji dotyczy również niezmienników niejawnych, a nie tylko jawnych. Oryginalny przykład bez warunków końcowych był bardzo zgodny z pierwotną, formalną definicją LSP. - person Konrad Rudolph; 13.10.2020
comment
@KonradRudolph Problem polega na tym, że niezmienniki niejawne są dyskusyjne. Niekoniecznie są osadzone w kamieniu. Twoja interpretacja tego, co stanowi niejawny niezmiennik w tym przypadku, jest bardzo uparty i oparta na kontekście aplikacji. - person wired_in; 19.10.2020
comment
@KonradRudolph Mogę argumentować, że skoro setHeight() i setWidth() są niezależnymi operacjami, to każda sama w sobie nie jest naruszeniem żadnego niezmiennika. Tylko wtedy, gdy próbujesz zmusić niezależne operacje do bycia jedną operacją, możesz zobaczyć swój pogląd na to, co uważasz za niejawny niezmiennik. W najlepszym razie jest to słaby argument za naruszeniem LSP - person wired_in; 19.10.2020
comment
@wired_in „Tylko wtedy, gdy próbujesz wymusić, aby niezależne operacje były jedną operacją” — Nie, nie zmuszamy ich do tego, aby były jedną operacją — są one wyraźnie nie jedną operacją. To, co wymuszamy, to niezmiennik, czyli twierdzenia o stanie obiektu. I znowu, to jest cały sens LSP: niezmienniki. Chodzi o niezmienniki, a nie o operacje. - person Konrad Rudolph; 19.10.2020

Robert Martin ma doskonały na zasada substytucji Liskov. Omawia subtelne i niezbyt subtelne sposoby, w jakie zasada może zostać naruszona.

Kilka istotnych części artykułu (zauważ, że drugi przykład jest mocno skondensowany):

Prosty przykład naruszenia LSP

Jednym z najbardziej rażących naruszeń tej zasady jest użycie C++ Run-Time Type Information (RTTI) do wyboru funkcji na podstawie typu obiektu. tj.:

void DrawShape(const Shape& s)
{
  if (typeid(s) == typeid(Square))
    DrawSquare(static_cast<Square&>(s)); 
  else if (typeid(s) == typeid(Circle))
    DrawCircle(static_cast<Circle&>(s));
}

Najwyraźniej funkcja DrawShape jest źle uformowana. Musi wiedzieć o każdej możliwej pochodnej klasy Shape i musi być zmieniana za każdym razem, gdy tworzone są nowe pochodne klasy Shape. Rzeczywiście, wielu uważa strukturę tej funkcji za przekleństwo dla projektowania zorientowanego obiektowo.

Kwadrat i prostokąt, bardziej subtelne naruszenie.

Istnieją jednak inne, znacznie bardziej subtelne sposoby naruszania LSP. Rozważ aplikację, która używa klasy Rectangle, jak opisano poniżej:

class Rectangle
{
  public:
    void SetWidth(double w) {itsWidth=w;}
    void SetHeight(double h) {itsHeight=w;}
    double GetHeight() const {return itsHeight;}
    double GetWidth() const {return itsWidth;}
  private:
    double itsWidth;
    double itsHeight;
};

[...] Wyobraźmy sobie, że pewnego dnia użytkownicy domagają się możliwości manipulowania kwadratami oprócz prostokątów. [...]

Oczywiście kwadrat jest prostokątem dla wszystkich normalnych intencji i celów. Ponieważ relacja ISA jest zachowana, logiczne jest modelowanie klasy Square jako wywodzącej się z Rectangle. [...]

Square odziedziczy funkcje SetWidth i SetHeight. Funkcje te są całkowicie nieodpowiednie dla Square, ponieważ szerokość i wysokość kwadratu są identyczne. Powinno to być istotną wskazówką, że jest problem z projektem. Istnieje jednak sposób na obejście problemu. Moglibyśmy nadpisać SetWidth i SetHeight [...]

Ale rozważ następującą funkcję:

void f(Rectangle& r)
{
  r.SetWidth(32); // calls Rectangle::SetWidth
}

Jeśli przekażemy odwołanie do obiektu Square do tej funkcji, obiekt Square zostanie uszkodzony, ponieważ wysokość nie zostanie zmieniona. Jest to wyraźne naruszenie LSP. Funkcja nie działa dla pochodnych jej argumentów.

[...]

person Phillip Wells    schedule 12.09.2008
comment
Bardzo późno, ale pomyślałem, że to interesujący cytat w tym artykule: 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 dziecka jest silniejszy niż warunek wstępny klasy rodzica, nie można zastąpić dziecka rodzica bez naruszenia tego warunku . Stąd LSP. - person user2023861; 11.02.2015
comment
@user2023861 Masz całkowitą rację. Napiszę odpowiedź na tej podstawie. - person inf3rno; 09.10.2017

LSP jest konieczne, gdy jakiś kod myśli, że wywołuje metody typu T i może nieświadomie wywoływać metody typu S, gdzie S extends T (tj. S dziedziczy, wywodzi się lub jest podtypem nadtypu T).

Na przykład dzieje się tak, gdy funkcja z parametrem wejściowym typu T jest wywoływana (tj. wywoływana) z wartością argumentu typu S. Lub, gdy identyfikator typu T ma przypisaną wartość typu S.

val id : T = new S() // id thinks it's a T, but is a S

LSP wymaga, aby oczekiwania (tj. niezmienniki) dla metod typu T (np. Rectangle) nie były naruszane, gdy zamiast tego wywoływane są metody typu S (np. Square).

val rect : Rectangle = new Square(5) // thinks it's a Rectangle, but is a Square
val rect2 : Rectangle = rect.setWidth(10) // height is 10, LSP violation

Nawet typ z polami niezmiennymi nadal ma niezmienniki, np. niezmienne ustawiające prostokąt oczekują, że wymiary będą modyfikowane niezależnie, ale niezmienne ustawiające kwadraty naruszają to oczekiwanie.

class Rectangle( val width : Int, val height : Int )
{
   def setWidth( w : Int ) = new Rectangle(w, height)
   def setHeight( h : Int ) = new Rectangle(width, h)
}

class Square( val side : Int ) extends Rectangle(side, side)
{
   override def setWidth( s : Int ) = new Square(s)
   override def setHeight( s : Int ) = new Square(s)
}

LSP wymaga, aby każda metoda podtypu S miała kontrawariantne parametry wejściowe i kowariantne dane wyjściowe.

Kontrawariant oznacza, że ​​wariancja jest przeciwna do kierunku dziedziczenia, tj. typ Si każdego parametru wejściowego każdej metody podtypu S musi być taki sam lub musi być nadtypem typu Ti odpowiedni parametr wejściowy odpowiedniej metody nadtypu T.

Kowariancja oznacza, że ​​wariancja jest w tym samym kierunku co dziedziczenie, tj. typ So danych wyjściowych każdej metody podtypu S musi być taki sam lub podtyp typu To odpowiedniego wyjście odpowiedniej metody nadtypu T.

Dzieje się tak, ponieważ jeśli osoba wywołująca myśli, że ma typ T, myśli, że wywołuje metodę T, to dostarcza argument(y) typu Ti i przypisuje dane wyjściowe do typu To. Kiedy faktycznie wywołuje odpowiednią metodę S, to każdy argument wejściowy Ti jest przypisany do parametru wejściowego Si, a wyjście So jest przypisane do typu To. Tak więc, gdyby Si nie były kontrawariantne w.r.t. do Ti, następnie podtyp Xi — który nie byłby podtypem Si — można by przypisać do Ti.

Dodatkowo, dla języków (np. Scala lub Ceylon), które mają adnotacje wariancji definicja-miejsce na parametrach polimorfizmu typu (tj. generykach), ko- lub przeciwny kierunek adnotacji wariancji dla każdego parametru typu typu T musi wynosić przeciwnie lub w tym samym kierunku odpowiednio do każdego parametru wejściowego lub wyjściowego (każdej metody z T), który ma typ parametru type.

Dodatkowo dla każdego parametru wejściowego lub wyjścia, które ma typ funkcji, wymagany kierunek wariancji jest odwracany. Ta reguła jest stosowana rekurencyjnie.


Podtypy są odpowiednie, gdy można wyliczyć niezmienniki.

Trwa wiele badań nad modelowaniem niezmienników, aby były one egzekwowane przez kompilator.

Typestate (patrz strona 3) deklaruje i wymusza niezmienniki stanu prostopadłe do typu. Alternatywnie niezmienniki można wymusić przez konwertowanie asercji na typy. Na przykład, aby potwierdzić, że plik jest otwarty przed jego zamknięciem, File.open() może zwrócić typ OpenFile, który zawiera metodę close(), która nie jest dostępna w File. Innym przykładem może być interfejs API kółko i krzyżyk stosowania typowania w celu wymuszenia niezmienników w czasie kompilacji. System czcionek może być nawet kompletny według Turinga, np. Scala. Języki z typowaniem zależnym i dowodzenia twierdzeń formalizują modele typowania wyższego rzędu.

Ze względu na potrzebę abstrahowania nad rozszerzeniem, spodziewam się, że zastosowanie typowania do niezmienników modelu, tj. zunifikowanej semantyki denotacyjnej wyższego rzędu, jest lepsze niż Typestate. „Rozszerzenie” oznacza nieograniczoną, permutowaną kompozycję nieskoordynowanego, modułowego rozwoju. Ponieważ wydaje mi się, że antytezą unifikacji, a więc stopni swobody, jest posiadanie dwóch wzajemnie zależnych modeli (np. typów i Typestate) do wyrażania wspólnej semantyki, które nie mogą być ze sobą zunifikowane w celu rozszerzalności kompozycji . Na przykład rozszerzenie podobne do Problem z wyrażeniem zostało ujednolicone w podtypach, przeciążaniu funkcji i typowaniu parametrycznym domeny.

Moje teoretyczne stanowisko jest takie, że dla wiedza istnieje ( patrz rozdział „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 Turing-complete. Aby wiedza istniała, istnieje wiele nieoczekiwanych możliwości, tj. nieporządek i entropia muszą zawsze rosnąć. To jest siła entropii. Udowodnienie wszystkich możliwych obliczeń potencjalnego rozszerzenia to obliczenie a priori wszystkich możliwych rozszerzeń.

Dlatego istnieje twierdzenie o zatrzymaniu, tj. Nie można rozstrzygnąć, czy każdy możliwy program w języku programowania Turinga kończy się. Można udowodnić, że kończy się jakiś konkretny program (ten, w którym wszystkie możliwości zostały zdefiniowane i obliczone). Ale nie można udowodnić, że wszystkie możliwe rozszerzenia tego programu się kończą, chyba że możliwości rozszerzenia tego programu nie są kompletne w trybie Turinga (np. poprzez pisanie zależne). Ponieważ podstawowym wymogiem dla zupełności Turinga jest nieograniczona rekurencja, intuicyjnie można zrozumieć, w jaki sposób twierdzenia Gödla o niezupełności i paradoks Russella odnoszą się do rozszerzenia.

Interpretacja tych twierdzeń włącza je do uogólnionego pojęciowego rozumienia siły entropicznej:

  • Twierdzenie o niezupełności Gödla: każda formalna teoria, w której można udowodnić wszystkie prawdy arytmetyczne, jest niespójna.
  • Paradoks Russella: każda zasada członkostwa dla zestaw, który może zawierać zestaw, wylicza określony typ każdego elementu członkowskiego lub zawiera sam siebie. Tak więc zbiory albo nie mogą być rozszerzane, albo są nieograniczoną rekurencją. Na przykład zbiór wszystkiego, co nie jest czajniczkiem, zawiera siebie, co zawiera siebie, co zawiera siebie itd.…. W związku z tym reguła jest niespójna, jeśli (może zawierać zestaw i) nie wylicza określonych typów (tj. zezwala na wszystkie nieokreślone typy) i nie zezwala na nieograniczone rozszerzenie. Jest to zbiór zestawów, które same w sobie nie są członkami. Ta niezdolność do bycia zarówno konsekwentnym, jak i całkowicie wyliczonym we wszystkich możliwych rozszerzeniach, jest twierdzeniem Gödla o niezupełności.
  • Zasada substytucji Liskov: ogólnie nierozstrzygalnym problemem jest to, czy jeden zbiór jest podzbiorem innego, tj. dziedziczenie jest generalnie nierozstrzygalne.
  • Odniesienie Linsky'ego: nie można rozstrzygnąć, czym jest obliczenie czegoś, kiedy jest to opisane lub postrzegane, tj. percepcja (rzeczywistość) nie ma absolutnego punktu odniesienia.
  • Twierdzenie Coase'a: nie ma zewnętrznego punktu odniesienia, więc każda bariera dla nieograniczonych możliwości zewnętrznych zawiedzie.
  • Drugie prawo termodynamiki: cały wszechświat (zamknięty system, czyli wszystko) dąży do maksymalnego nieładu, czyli maksymalnych niezależnych możliwości.
person Shelby Moore III    schedule 26.11.2011
comment
@Shelyby: Wymieszałeś zbyt wiele rzeczy. Rzeczy nie są tak zagmatwane, jak je określasz. Wiele z twoich teoretycznych twierdzeń opiera się na kruchych podstawach, takich jak „Aby wiedza istniała, istnieje wiele nieoczekiwanych możliwości…” ORAZ „ogólnie jest nierozstrzygalnym problemem, czy jeden zbiór jest podzbiorem innego, tj. dziedziczenie jest generalnie nierozstrzygalne”. Dla każdego z tych punktów możesz założyć osobny blog. W każdym razie twoje twierdzenia i założenia są bardzo wątpliwe. Nie wolno używać rzeczy, o których nie jest się świadomym! - person aknon; 27.12.2013
comment
@aknon prowadzę blog, który dokładniej wyjaśnia te kwestie. Mój model TOE nieskończonej czasoprzestrzeni to nieograniczone częstotliwości. Nie jest dla mnie mylące, że rekurencyjna funkcja indukcyjna ma znaną wartość początkową z nieskończoną granicą końcową lub funkcja koindukcyjna ma nieznaną wartość końcową i znaną granicę początkową. Względność jest problemem po wprowadzeniu rekurencji. Dlatego Ukończenie Turinga jest równoznaczne z nieograniczoną rekurencją. - person Shelby Moore III; 16.03.2014
comment
@ShelbyMooreIII Idziesz w zbyt wielu kierunkach. To nie jest odpowiedź. - person Soldalma; 09.12.2016
comment
@Soldalma to odpowiedź. Nie widzisz tego w sekcji odpowiedzi. Twój jest komentarz, ponieważ znajduje się w sekcji komentarzy. - person Shelby Moore III; 23.12.2016
comment
@aknon co do twoich zarzutów o marność, na blogu i napisane od mojej odpowiedzi w 2014 r. - person Shelby Moore III; 23.12.2016
comment
Jak twoje mieszanie ze światem scala! - person Ehsan M. Kermani; 28.06.2017

Widzę prostokąty i kwadraty w każdej odpowiedzi i jak naruszyć LSP.

Chciałbym pokazać, jak można dostosować LSP na przykładzie ze świata rzeczywistego:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return $result; 
    }
}

Ten projekt jest zgodny z LSP, ponieważ zachowanie pozostaje niezmienione niezależnie od implementacji, którą zdecydujemy się użyć.

I tak, możesz naruszyć LSP w tej konfiguracji, wykonując jedną prostą zmianę, taką jak:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return ['result' => $result]; // This violates LSP !
    }
}

Teraz podtypy nie mogą być używane w ten sam sposób, ponieważ nie dają już tego samego wyniku.

person Steve Chamaillard    schedule 18.02.2018
comment
Przykład nie narusza LSP tylko tak długo, jak ograniczamy semantykę Database::selectQuery do obsługi tylko podzbioru SQL obsługiwanego przez wszystkie silniki DB. To mało praktyczne... To powiedziawszy, przykład jest nadal łatwiejszy do uchwycenia niż większość innych użytych tutaj. - person Palec; 25.02.2018
comment
Uznałem tę odpowiedź za najłatwiejszą do zrozumienia spośród pozostałych. - person Malcolm Salvador; 11.02.2019
comment
czy praktyczne jest stosowanie LSP w bazach danych? Widzę, że większość, jeśli nie wszystkie, operacje na bazach danych będą musiały być opakowane i są podatne na błędy. Chociaż dobrą stroną jest to, że API pozostaje takie samo, nawet jeśli jest to SQL vs NoSQL. - person Sean W; 23.08.2020

Istnieje lista kontrolna, która pozwala ustalić, czy naruszasz Liskov.

  • Jeśli naruszasz jeden z następujących elementów -› naruszasz Liskov.
  • Jeśli nie naruszysz żadnych -› nie możesz niczego zawrzeć.

Lista kontrolna:

  • Żadne nowe wyjątki nie powinny być zgłaszane w klasie pochodnej: jeśli Twoja klasa bazowa zgłosiła ArgumentNullException, Twoje klasy podrzędne mogły zgłaszać tylko wyjątki typu ArgumentNullException lub wszelkie wyjątki pochodzące od ArgumentNullException. Zgłaszanie wyjątku IndexOutOfRangeException jest naruszeniem Liskov.

  • Warunki wstępne nie mogą zostać wzmocnione: załóż, że twoja klasa bazowa działa z elementem int. Teraz twój podtyp wymaga, aby int był pozytywny. Jest to wzmocnione warunki wstępne, a teraz każdy kod, który wcześniej działał bez zarzutu z ujemnymi wartościami int, jest zepsuty.

  • Warunki końcowe nie mogą być osłabione: Załóżmy, że klasa bazowa wymagała, aby wszystkie połączenia z bazą danych zostały zamknięte przed zwróceniem metody. W swojej podklasie zastąpiłeś tę metodę i pozostawiłeś otwarte połączenie do dalszego wykorzystania. Osłabiłeś warunki końcowe tej metody.

  • Niezmienniki muszą być zachowane: najtrudniejsze i najbardziej bolesne ograniczenie do spełnienia. Niezmienniki są czasami ukryte w klasie bazowej i jedynym sposobem ich ujawnienia jest odczytanie kodu klasy bazowej. Zasadniczo musisz być pewien, że kiedy nadpisujesz metodę, wszystko, co niezmienne, musi pozostać niezmienione po wykonaniu nadpisanej metody. Najlepszą rzeczą, o jakiej mogę pomyśleć, jest wymuszenie tych niezmiennych ograniczeń w klasie bazowej, ale nie byłoby to łatwe.

  • Ograniczenie historii: podczas zastępowania metody nie możesz modyfikować niemodyfikowalnej właściwości w klasie bazowej. Spójrz na ten kod i zobaczysz, że nazwa jest zdefiniowana jako niemodyfikowalna (zestaw prywatny), ale SubType wprowadza nową metodę, która pozwala na jej modyfikację (poprzez odbicie):

     public class SuperType
     {
         public string Name { get; private set; }
         public SuperType(string name, int age)
         {
             Name = name;
             Age = age;
         }
     }
     public class SubType : SuperType
     {
         public void ChangeName(string newName)
         {
             var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName);
         }
     }
    

Są jeszcze 2 inne elementy: Kowariancja argumentów metody i Kowariancja typów zwracanych. Ale nie jest to możliwe w C# (jestem programistą C#), więc nie przejmuję się nimi.

person Cù Đức Hiếu    schedule 13.08.2016
comment
Jestem również programistą C# i powiem, że twoje ostatnie stwierdzenie nie jest prawdziwe w Visual Studio 2010 z frameworkiem .Net 4.0. Kowariancja typów zwracanych pozwala na bardziej pochodny typ zwracany niż ten, który został zdefiniowany przez interfejs. Przykład: Przykład: IEnumerable‹T› (T jest kowariantną) IEnumerator‹T› (T jest kowariantną) IQueryable‹T› (T jest kowariantną) IGrouping‹TKey, TElement› (TKey i TElement są kowariantne) IComparer‹T› (T jest kontrawariantne) IEqualityComparer‹T› (T jest kontrawariantne) IComparable‹T› (T jest kontrawariantne) msdn.microsoft.com/en-us/library/dd233059(v=vs.100).aspx - person LCarter; 05.09.2017
comment
Świetna i skoncentrowana odpowiedź (chociaż oryginalne pytania dotyczyły bardziej przykładów niż zasad). - person Mike; 12.06.2018

Długie w skrócie, zostawmy prostokąty, prostokąty, prostokąty i kwadraty, kwadraty, praktyczny przykład podczas rozszerzania klasy nadrzędnej, musisz albo ZACHOWAĆ dokładnie nadrzędny interfejs API, albo go ROZSZERZYĆ.

Załóżmy, że masz podstawowe repozytorium przedmiotów.

class ItemsRepository
{
    /**
    * @return int Returns number of deleted rows
    */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        return $numberOfDeletedRows;
    }
}

I podklasa go rozszerzająca:

class BadlyExtendedItemsRepository extends ItemsRepository
{
    /**
     * @return void Was suppose to return an INT like parent, but did not, breaks LSP
     */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        // we broke the behaviour of the parent class
        return;
    }
}

Wtedy możesz mieć klienta pracującego z API Base ItemsRepository i na nim polegającego.

/**
 * Class ItemsService is a client for public ItemsRepository "API" (the public delete method).
 *
 * Technically, I am able to pass into a constructor a sub-class of the ItemsRepository
 * but if the sub-class won't abide the base class API, the client will get broken.
 */
class ItemsService
{
    /**
     * @var ItemsRepository
     */
    private $itemsRepository;

    /**
     * @param ItemsRepository $itemsRepository
     */
    public function __construct(ItemsRepository $itemsRepository)
    {
        $this->itemsRepository = $itemsRepository;
    }

    /**
     * !!! Notice how this is suppose to return an int. My clients expect it based on the
     * ItemsRepository API in the constructor !!!
     *
     * @return int
     */
    public function delete()
    {
        return $this->itemsRepository->delete();
    }
} 

LSP zostaje przerwane, gdy zastąpienie klasy nadrzędnej klasą podrzędną przerywa kontrakt API.

class ItemsController
{
    /**
     * Valid delete action when using the base class.
     */
    public function validDeleteAction()
    {
        $itemsService = new ItemsService(new ItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is an INT :)
    }

    /**
     * Invalid delete action when using a subclass.
     */
    public function brokenDeleteAction()
    {
        $itemsService = new ItemsService(new BadlyExtendedItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is a NULL :(
    }
}

Możesz dowiedzieć się więcej o pisaniu oprogramowania, które można konserwować na moim kursie: https://www.udemy.com/enterprise-php/

person Lukas Lukac    schedule 28.10.2017
comment
To znacznie lepszy przykład. Dziękuję Ci! - person Kurt Campher; 14.02.2021

LSP jest regułą dotyczącą kontraktu klas: jeśli klasa bazowa spełnia kontrakt, to klasy pochodne LSP muszą również spełniać ten kontrakt.

W pseudo-pytonie

class Base:
   def Foo(self, arg): 
       # *... do stuff*

class Derived(Base):
   def Foo(self, arg):
       # *... do stuff*

spełnia LSP, jeśli każde wywołanie Foo na obiekcie pochodnym daje dokładnie takie same wyniki, jak wywołanie Foo na obiekcie podstawowym, o ile arg jest takie samo.

person Charlie Martin    schedule 08.11.2008
comment
Ale… jeśli zawsze zachowujesz się tak samo, to jaki jest sens posiadania klasy pochodnej? - person Leonid; 03.07.2012
comment

To znacznie prostsze niż myślisz:

usr->next = group->users;
group->users = usr;
- person Charlie Martin; 04.07.2012
comment
@Charlie Martin, kodowanie do interfejsu, a nie implementacji - wierzę w to. Nie jest to wyjątkowe dla obiektów OOP; promują to również języki funkcjonalne, takie jak Clojure. Nawet jeśli chodzi o Javę lub C#, myślę, że używanie interfejsu zamiast używania abstrakcyjnej klasy i hierarchii klas byłoby naturalne dla podanych przykładów. Python nie jest silnie typizowany i tak naprawdę nie ma interfejsów, przynajmniej nie jawnie. Moją trudnością jest to, że od kilku lat robię OOP bez przestrzegania SOLID. Teraz, kiedy natknąłem się na to, wydaje się to ograniczające i prawie sprzeczne z samym sobą. - person Hamish Grubijan; 06.07.2012
comment
Cóż, musisz wrócić i sprawdzić oryginalny artykuł Barbary. reports-archive.adm.cs. cmu.edu/anon/1999/CMU-CS-99-156.ps Tak naprawdę nie jest to określone w kategoriach interfejsów i jest to logiczna relacja, która obowiązuje (lub nie) w każdym języku programowania, który ma jakaś forma dziedziczenia. - person Charlie Martin; 07.07.2012
comment
@HamishGrubijan Nie wiem kto Ci powiedział, że Python nie jest mocno wpisany, ale okłamywali Cię (a jeśli mi nie wierzysz, odpal interpreter Pythona i spróbuj 2 + "2"). Być może mylisz typowanie silnie z typowaniem statycznym? - person asmeurer; 20.01.2013
comment
@asmeurer Hamish nie myli się. Silnie wpisane nie jest dobrze zdefiniowanym terminem; czasami oznacza to, że ma jakiś rodzaj statycznego sprawdzania typu (w rozumieniu Hamisha), czasami oznacza, że ​​nie wykonuje przymusu typu (twojego znaczenia i jak zwykle tylko w odniesieniu do konkretnego przypadku 'koercji typu ciąg na numer ' - nigdy nie widziałem nikogo, kto by twierdził, że C jest słabo typowane, ponieważ można pomnożyć wartości całkowite przez liczby zmiennoprzecinkowe, lub że Python jest, ponieważ można łączyć u'hello' z 'world'), a czasami oznacza jedną z wielu innych rzeczy. en.wikipedia.org/wiki/Strong_and_weak_typing - person Mark Amery; 27.12.2013
comment
Cóż, z punktu widzenia Liskova te są inne, bo zawsze działają. Jeśli język obsługuje 2 + '2', nadal nie obsługuje 2 + 'two'. - person asmeurer; 27.12.2013
comment
Co dziwne, @MarkAmery, artykuł wiki, do którego linkujesz, obsługuje moje użycie „silnie wpisane”. Tylko dlatego, że ludzie używali go niewłaściwie, nie sprawia to, że definicja jest błędna. W 1974 roku Liskov i Zilles opisali język o silnych typach jako taki, w którym za każdym razem, gdy obiekt jest przekazywany z funkcji wywołującej do funkcji wywoływanej, jego typ musi być zgodny z typem zadeklarowanym w wywoływanej funkcji. - person Charlie Martin; 27.12.2013
comment
@CharlieMartin, jeśli dobrze rozumiem twoją odpowiedź, to LSP to możliwość zastąpienia dowolnej instancji klasy nadrzędnej instancją jednej z jej klas podrzędnych bez negatywnych skutków ubocznych, prawda? - person Daniel; 27.10.2018

Myślę, że każdy w pewnym sensie omówił, czym jest LSP technicznie: zasadniczo chcesz być w stanie abstrahować od szczegółów podtypów i bezpiecznie używać nadtypów.

Tak więc Liskov ma 3 podstawowe zasady:

  1. Reguła podpisu : Powinna istnieć poprawna implementacja składniowo każdej operacji nadtypu w podtypie. Coś, co kompilator będzie mógł dla Ciebie sprawdzić. Istnieje mała zasada dotycząca zgłaszania mniejszej liczby wyjątków i bycia przynajmniej tak samo dostępnym, jak metody nadtypów.

  2. Metody Reguła: Implementacja tych operacji jest semantycznie poprawna.

    • Weaker Preconditions : The subtype functions should take at least what the supertype took as input, if not more.
    • Silniejsze warunki końcowe: Powinny generować podzbiór wyników uzyskanych przez metody nadtypów.
  3. Reguła właściwości : wykracza poza pojedyncze wywołania funkcji.

    • Invariants : Things that are always true must remain true. Eg. a Set's size is never negative.
    • Właściwości ewolucyjne : zwykle coś związanego z niezmiennością lub rodzajem stanów, w których może znajdować się obiekt. A może obiekt tylko rośnie i nigdy się nie kurczy, więc metody podtypów nie powinny tego robić.

Wszystkie te właściwości muszą być zachowane, a dodatkowa funkcjonalność podtypu nie powinna naruszać właściwości nadtypu.

Jeśli zajmiemy się tymi trzema rzeczami, oderwałeś się od podstawowych rzeczy i piszesz luźno powiązany kod.

Źródło: Programowanie w Javie - Barbara Liskov

person snagpaul    schedule 18.12.2016

Zilustrujmy w Javie:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }

   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

class Car extends TransportationDevice
{
   @Override
   void startEngine() { ... }
}

Tu nie ma problemu, prawda? Samochód jest zdecydowanie środkiem transportu, a tutaj widzimy, że zastępuje on metodę startEngine() swojej nadklasy.

Dodajmy kolejne urządzenie transportowe:

class Bicycle extends TransportationDevice
{
   @Override
   void startEngine() /*problem!*/
}

Teraz wszystko nie idzie zgodnie z planem! Tak, rower jest środkiem transportu, jednak nie posiada silnika i stąd metoda startEngine() nie może być zaimplementowana.

Są to rodzaje problemów, do których prowadzi naruszenie zasady substytucji Liskov i najczęściej można je rozpoznać za pomocą metody, która nic nie robi lub nawet nie może zostać wdrożona.

Rozwiązaniem tych problemów jest prawidłowa hierarchia dziedziczenia, a w 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 dokonać refaktoryzacji naszej klasy TransportationDevice w następujący sposób:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }
}

Teraz możemy rozszerzyć TransportationDevice o urządzenia niezmotoryzowane.

class DevicesWithoutEngines extends TransportationDevice
{  
   void startMoving() { ... }
}

I rozszerz TransportationDevice o urządzenia zmotoryzowane. Tutaj bardziej odpowiednie jest dodanie obiektu Engine.

class DevicesWithEngines extends TransportationDevice
{  
   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

W ten sposób nasza klasa samochodów staje się bardziej wyspecjalizowana, przestrzegając zasady substytucji Liskov.

class Car extends DevicesWithEngines
{
   @Override
   void startEngine() { ... }
}

Nasza klasa rowerów jest również zgodna z zasadą zastępstwa Liskov.

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}
person Khaled Qasem    schedule 10.02.2019

Funkcje korzystające ze wskaźników lub odwołań do klas bazowych muszą mieć możliwość korzystania z obiektów klas pochodnych bez wiedzy o tym.

Kiedy po raz pierwszy przeczytałem o LSP, założyłem, że chodziło o to w bardzo ścisłym znaczeniu, zasadniczo utożsamiając go z implementacją interfejsu i bezpiecznym rzutowaniem typu. Co oznaczałoby, że LSP jest albo zapewniane przez sam język, albo nie. Na przykład, w ścisłym tego słowa znaczeniu, ThreeDBoard z pewnością zastępuje Board, jeśli chodzi o kompilator.

Po przeczytaniu więcej na temat koncepcji odkryłem, że LSP jest ogólnie interpretowane szerzej.

Krótko mówiąc, co oznacza, że ​​kod klienta "wiedzie", że obiekt za wskaźnikiem jest typu pochodnego, a nie typu wskaźnika, nie jest ograniczone do bezpieczeństwa typu. Przestrzeganie LSP można również sprawdzić, badając rzeczywiste zachowanie obiektów. Oznacza to zbadanie wpływu argumentów stanu i metody obiektu na wyniki wywołań metod lub typy wyjątków zgłoszonych z obiektu.

Wracając jeszcze raz do przykładu, teoretycznie metody tablicy mogą 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ć prawidłowo, bez ograniczania funkcjonalności, którą ma dodać ThreeDBoard.

Mając tę ​​wiedzę w ręku, ocena przestrzegania LSP może być doskonałym narzędziem do określenia, kiedy kompozycja jest bardziej odpowiednim mechanizmem rozszerzania istniejącej funkcjonalności, a nie dziedziczenia.

person Chris Ammerman    schedule 11.09.2008

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 wykorzystać wszystkie przypadki testowe z nadklasy B.

Sposobem na zrealizowanie tego jest zbudowanie tego, co McGregor nazywa „hierarchią równoległą do testowania”: Moja klasa ATest będzie dziedziczyć po BTest. Następnie potrzebna jest pewna forma wstrzykiwania, aby upewnić się, że przypadek testowy działa z obiektami typu A, a nie typu B (wystarczy prosty wzorzec metody szablonu).

Należy zauważyć, że ponowne użycie zestawu supertestów dla wszystkich implementacji podklas jest w rzeczywistości sposobem sprawdzenia, czy te implementacje podklas są zgodne z LSP. Można zatem argumentować, że powinno uruchomić zestaw 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?

person avandeursen    schedule 25.03.2013

W bardzo prostym zdaniu możemy powiedzieć:

Klasa podrzędna nie może naruszać swoich cech klasy bazowej. Musi być do tego zdolny. Można powiedzieć, że to to samo, co podtypowanie.

person Alireza Rahmani khalili    schedule 16.08.2017

Zasada substytucji Liskov

  • Nadpisana metoda nie powinna pozostać pusta
  • Zastąpiona metoda nie powinna powodować błędu
  • Zachowanie klasy bazowej lub interfejsu nie powinno podlegać modyfikacji (przeróbce), ponieważ jest to spowodowane zachowaniem klasy pochodnej.
person Rahamath    schedule 19.08.2019

To sformułowanie LSP jest zbyt mocne:

Jeżeli dla każdego obiektu o1 typu S istnieje obiekt o2 typu T taki, że dla wszystkich programów P zdefiniowanych w kategoriach T, zachowanie P pozostaje niezmienione, gdy o1 jest zastąpione przez o2, to S jest podtypem T.

Co w zasadzie oznacza, że ​​S jest kolejną, całkowicie hermetyzowaną implementacją dokładnie tego samego co T. I mogę być odważny i zdecydować, że wydajność jest częścią zachowania P...

Tak więc w zasadzie każde użycie późnego wiązania narusza LSP. Cały sens OO polega na uzyskaniu innego zachowania, gdy zastępujemy obiekt jednego rodzaju innym rodzajem!

Sformułowanie cytowane przez wikipedię jest lepsze, ponieważ właściwość zależy od kontekstu i niekoniecznie obejmuje całość zachowanie programu.

person Damien Pollet    schedule 03.04.2009
comment
Eee, to sformułowanie należy do Barbary Liskov. Barbara Liskov, „Data Abstraction and Hierarchy”, SIGPLAN Notices, 23,5 (maj 1988). Nie jest zbyt silny, jest dokładnie słuszny i nie ma takiego wpływu, jaki myślisz, że ma. Jest mocny, ale ma odpowiednią ilość siły. - person DrPizza; 30.11.2009
comment
W rzeczywistości istnieje bardzo mało podtypów :) - person Damien Pollet; 09.12.2009
comment
Zachowanie niezmienione nie oznacza, że ​​podtyp da ci dokładnie te same konkretne wartości wyników. Oznacza to, że zachowanie podtypu jest zgodne z oczekiwaniami w typie podstawowym. Przykład: typ podstawowy Shape może mieć metodę draw() i określać, że ta metoda powinna renderować kształt. Dwa podtypy Shape (np. Square i Circle) implementują metodę draw(), a wyniki będą wyglądać inaczej. Ale dopóki zachowanie (renderowanie kształtu) odpowiadało określonemu zachowaniu Shape, to Square i Circle będą podtypami Shape zgodnie z LSP. - person SteveT; 11.10.2012

Zasada substytucji Liskov (LSP)

Cały czas projektujemy moduł programu i tworzymy hierarchie klas. Następnie rozszerzamy niektóre klasy tworząc klasy pochodne.

Musimy upewnić się, że nowe klasy pochodne po prostu rozszerzają się bez zastępowania funkcjonalności starych klas. W przeciwnym razie nowe klasy mogą powodować niepożądane efekty, gdy są używane w istniejących modułach programu.

Zasada substytucji Liskova stwierdza, że ​​jeśli moduł programu używa klasy Base, to odwołanie do klasy Base może zostać zastąpione klasą Derived bez wpływu na funkcjonalność modułu programu.

Przykład:

Poniżej znajduje się klasyczny przykład, w którym naruszona jest zasada substytucji Liskova. W przykładzie używane są 2 klasy: Prostokąt i Kwadrat. Załóżmy, że obiekt Rectangle jest używany gdzieś w aplikacji. Rozszerzamy aplikację i dodajemy klasę Square. Klasa square jest zwracana przez wzorzec fabryczny, oparty na pewnych 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 będzie wynosił 100

    // Violation of Likov's Substitution Principle
class Rectangle {
    protected int m_width;
    protected int m_height;

    public void setWidth(int width) {
        m_width = width;
    }

    public void setHeight(int height) {
        m_height = height;
    }

    public int getWidth() {
        return m_width;
    }

    public int getHeight() {
        return m_height;
    }

    public int getArea() {
        return m_width * m_height;
    }
}

class Square extends Rectangle {
    public void setWidth(int width) {
        m_width = width;
        m_height = width;
    }

    public void setHeight(int height) {
        m_width = height;
        m_height = height;
    }

}

class LspTest {
    private static Rectangle getNewRectangle() {
        // it can be an object returned by some factory ...
        return new Square();
    }

    public static void main(String args[]) {
        Rectangle r = LspTest.getNewRectangle();

        r.setWidth(5);
        r.setHeight(10);
        // user knows that r it's a rectangle.
        // It assumes that he's able to set the width and height as for the base
        // class

        System.out.println(r.getArea());
        // now he's surprised to see that the area is 100 instead of 50.
    }
}

Wniosek:

Ta zasada jest tylko rozszerzeniem zasady Open Close i oznacza, że ​​musimy upewnić się, że nowe klasy pochodne rozszerzają klasy bazowe bez zmiany ich zachowania.

Zobacz też: Zasada otwierania i zamykania

Kilka podobnych koncepcji dla lepszej struktury: Konwencja nad konfiguracją

person GauRang Omar    schedule 21.05.2018

LSP w prostych słowach stwierdza, że ​​obiekty tego samego superklasa powinna być w stanie zamieniać się między sobą bez uszkadzania czegokolwiek.

Na przykład, jeśli mamy klasy Cat i Dog wywodzące się z klasy Animal, wszystkie funkcje korzystające z klasy Animal powinny móc używać Cat lub Dog i zachowywać się normalnie.

person johannesMatevosyan    schedule 24.01.2020

Zasada ta została wprowadzona przez Barbarę Liskov w 1987 roku i rozszerza zasadę otwarte-zamknięte, skupiając się na zachowaniu superklasy i jej podtypów.

Jego znaczenie staje się oczywiste, gdy zastanowimy się nad konsekwencjami jego naruszenia. Rozważ aplikację, która używa następującej klasy.

public class Rectangle 
{ 
  private double width;

  private double height; 

  public double Width 
  { 
    get 
    { 
      return width; 
    } 
    set 
    { 
      width = value; 
    }
  } 

  public double Height 
  { 
    get 
    { 
      return height; 
    } 
    set 
    { 
      height = value; 
    } 
  } 
}

Wyobraź sobie, że pewnego dnia klient zażąda umiejętności manipulowania kwadratami oprócz prostokątów. Ponieważ kwadrat jest prostokątem, klasa kwadrat powinna pochodzić z klasy Rectangle.

public class Square : Rectangle
{
} 

Jednak robiąc to, napotkamy dwa problemy:

Kwadrat nie potrzebuje zarówno zmiennych wysokości, jak i szerokości odziedziczonych z prostokąta, co może spowodować znaczne marnotrawstwo pamięci, jeśli będziemy musieli tworzyć setki tysięcy obiektów kwadratowych. Właściwości ustawiające szerokość i wysokość odziedziczone z prostokąta są nieodpowiednie dla kwadratu, ponieważ szerokość i wysokość kwadratu są identyczne. Aby ustawić wysokość i szerokość na tę samą wartość, możemy utworzyć dwie nowe właściwości w następujący sposób:

public class Square : Rectangle
{
  public double SetWidth 
  { 
    set 
    { 
      base.Width = value; 
      base.Height = value; 
    } 
  } 

  public double SetHeight 
  { 
    set 
    { 
      base.Height = value; 
      base.Width = value; 
    } 
  } 
}

Teraz, gdy ktoś ustawi szerokość kwadratowego obiektu, jego wysokość odpowiednio się zmieni i na odwrót.

Square s = new Square(); 
s.SetWidth(1); // Sets width and height to 1. 
s.SetHeight(2); // sets width and height to 2. 

Przejdźmy dalej i rozważmy tę inną funkcję:

public void A(Rectangle r) 
{ 
  r.SetWidth(32); // calls Rectangle.SetWidth 
} 

Jeśli przekażemy do tej funkcji odwołanie do obiektu kwadratowego, naruszylibyśmy LSP, ponieważ funkcja nie działa dla pochodnych jej argumentów. Właściwości width i height nie są polimorficzne, ponieważ nie są zadeklarowane jako wirtualne w prostokącie (obiekt kwadratowy zostanie uszkodzony, ponieważ wysokość nie zostanie zmieniona).

Jednak deklarując, że właściwości ustawiające są wirtualne, spotkamy się z innym naruszeniem, OCP. W rzeczywistości utworzenie kwadratu klasy pochodnej powoduje zmiany w prostokącie klasy bazowej.

person Ivan Porta    schedule 02.03.2020
comment
Wydaje mi się, że w metodzie A() miałeś na myśli r.Width = 32;, ponieważ metoda Rectangle nie ma metody SetWidth(). - person wired_in; 13.10.2020

Trochę dodatku:
Zastanawiam się, dlaczego nikt nie napisał o Invariant , warunkach wstępnych i post-warunkach klasy bazowej, które muszą być przestrzegane przez klasy pochodne. Aby pochodna klasa D była całkowicie zastępowana przez klasę bazową B, klasa D musi spełniać pewne warunki:

  • Warianty klasy bazowej muszą być zachowywane przez klasę pochodną
  • Warunki wstępne klasy bazowej nie mogą być wzmacniane przez klasę pochodną
  • Warunki końcowe klasy bazowej nie mogą być osłabiane przez klasę pochodną.

Tak więc pochodna musi być świadoma powyższych trzech warunków nałożonych przez klasę bazową. W związku z tym zasady tworzenia podtypów są z góry ustalone. Co oznacza, że ​​relacja „IS A” powinna być przestrzegana tylko wtedy, gdy podtyp przestrzega pewnych zasad. Zasady te, w postaci niezmienników, warunków wstępnych i warunków końcowych, powinny być określone w formalnej „umowie projektowej”.

Dalsze dyskusje na ten temat dostępne na moim blogu: Zasada substytucji Liskov

person aknon    schedule 27.12.2013

Kwadrat to prostokąt, którego szerokość jest równa wysokości. Jeśli kwadrat ustawia dwa różne rozmiary dla szerokości i wysokości, narusza niezmiennik kwadratu. 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ści == szerokości; silniejszy warunek wstępny (a 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 zła) powoduje efekt uboczny, a to osłabia stan postu (co narusza lsp). setWidth na podstawie ma post-warunek 0 ‹ szerokość. Pochodna osłabia ją o wysokość == szerokość.

Dlatego kwadrat o zmiennym rozmiarze nie jest prostokątem o zmiennym rozmiarze.

person Wouter    schedule 27.07.2015

Czy wdrożenie ThreeDBoard w zakresie tablicy Board byłoby aż tak przydatne?

Być może zechcesz potraktować plasterki ThreeDBBoard na różnych płaszczyznach jako tablicę. W takim przypadku możesz chcieć wyabstrahować interfejs (lub klasę abstrakcyjną) dla Board, aby umożliwić wiele implementacji.

Jeśli chodzi o interfejs zewnętrzny, możesz chcieć wyłączyć interfejs Board zarówno dla TwoDBoard, jak i ThreeDBoard (chociaż żadna z powyższych metod nie pasuje).

person Tom Hawtin - tackline    schedule 11.09.2008
comment
Myślę, że ten przykład ma po prostu pokazać, że dziedziczenie z tablicy nie ma sensu w kontekście ThreeDBoard, a wszystkie sygnatury metod są bez znaczenia w przypadku osi Z. - person NotMyself; 11.09.2008

Najjaśniejszym wyjaśnieniem dla LSP jakie do tej pory znalazłem było „Zasada podstawienia Liskova mówi, że obiekt klasy pochodnej powinien być w stanie zastąpić obiekt klasy bazowej bez powodowaniabłędów w systemie lub modyfikowania zachowania klasy bazowej ” z tutaj. Artykuł zawiera przykładowy kod do naruszenia LSP i naprawiania go.

person Prasa    schedule 03.05.2016
comment
Proszę podać przykłady kodu na stackoverflow. - person sebenalern; 03.05.2016

Załóżmy, że w naszym kodzie używamy prostokąta

r = new Rectangle();
// ...
r.setDimensions(1,2);
r.fill(colors.red());
canvas.draw(r);

Na naszych zajęciach z geometrii dowiedzieliśmy się, że kwadrat jest szczególnym rodzajem prostokąta, ponieważ jego szerokość jest taka sama jak jego wysokość. Stwórzmy również klasę Square na podstawie tych informacji:

class Square extends Rectangle {
    setDimensions(width, height){
        assert(width == height);
        super.setDimensions(width, height);
    }
} 

Jeśli zastąpimy Rectangle Square w naszym pierwszym kodzie, to się zepsuje:

r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);

Dzieje się tak, ponieważ Square ma nowy warunek wstępny, którego nie mieliśmy w klasie Rectangle: width == height. Według LSP Rectangle instancji powinno być zastępowalne przez Rectangle instancje podklas. Dzieje się tak, ponieważ te wystąpienia przechodzą kontrolę typu dla Rectangle wystąpień, a więc spowodują nieoczekiwane błędy w kodzie.

To był przykład części „warunki wstępne nie mogą być wzmocnione w podtypie” w artykuł wiki. Podsumowując, naruszenie LSP prawdopodobnie spowoduje w pewnym momencie błędy w kodzie.

person inf3rno    schedule 09.10.2017

LSP mówi, że „Obiekty powinny być zastępowalne przez ich podtypy”. Z drugiej strony zasada ta wskazuje na:

Klasy potomne nigdy nie powinny łamać definicji typu klasy nadrzędnej.

a poniższy przykład pomaga lepiej zrozumieć LSP.

Bez LSP:

public interface CustomerLayout{

    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            return; //it isn`t rendered in this case
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

Naprawa przez LSP:

public interface CustomerLayout{
    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            showAd();//it has a specific behavior based on its requirement
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}
person Zahra.HY    schedule 06.04.2019

Zachęcam do przeczytania artykułu: Naruszenie zasady substytucji Liskov (LSP).

Znajdziesz tam wyjaśnienie, czym jest Zasada Podstawiania Liskova, ogólne wskazówki, które pomogą Ci odgadnąć, czy już ją naruszyłeś oraz przykład podejścia, które pomoże Ci uczynić Twoją hierarchię klasową bezpieczniejszą.

person Ryszard Dżegan    schedule 09.09.2013

ZASADA SUBSTYTUCJI LISKOWA (z książki Marka Seemanna) mówi, że powinniśmy być w stanie zastąpić jedną implementację interfejsu inną bez naruszania klienta ani implementacji. Przewiduję je dzisiaj.

Jeśli odłączymy komputer od ściany (wdrożenie), ani gniazdko ścienne (interfejs) ani komputer (klient) nie psują się (w rzeczywistości, jeśli jest to laptop, może nawet przez pewien czas działać na bateriach) . Jednak w przypadku oprogramowania klient często oczekuje, że usługa będzie dostępna. Jeśli usługa została usunięta, otrzymujemy NullReferenceException. Aby poradzić sobie z tego typu sytuacją, możemy stworzyć implementację interfejsu, który „nic nie robi”. Jest to wzorzec projektowy znany jako obiekt zerowy[4], który w przybliżeniu odpowiada 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.

person Raghu Reddy Muttana    schedule 12.08.2016

Zasada podstawienia Likova stwierdza, że ​​jeśli moduł programu używa klasy Base, to odwołanie do klasy Base może zostać zastąpione klasą pochodną bez wpływu na funkcjonalność modułu programu.

Intencja — typy pochodne muszą być całkowicie zastępowalne dla ich typów podstawowych.

Przykład — współwariantowe typy zwracane w języku Java.

person Ishan Aggarwal    schedule 23.09.2017

Oto fragment tego posta, który ładnie wyjaśnia:

[..] aby zrozumieć niektóre zasady, ważne jest, aby zdać sobie sprawę, kiedy zostały naruszone. To właśnie zrobię teraz.

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żmy następujący przykład:

interface Account
{
    /**
     * Withdraw $money amount from this account.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}
class DefaultAccount implements Account
{
    private $balance;
    public function withdraw(Money $money)
    {
        if (!$this->enoughMoney($money)) {
            return;
        }
        $this->balance->subtract($money);
    }
}

Czy to naruszenie LSP? TAk. Dzieje się tak, ponieważ umowa konta mówi nam, że konto zostanie wycofane, ale nie zawsze tak jest. Co więc powinienem zrobić, aby to naprawić? Po prostu modyfikuję umowę:

interface Account
{
    /**
     * Withdraw $money amount from this account if its balance is enough.
     * Otherwise do nothing.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}

Voilà, teraz umowa jest spełniona.

To subtelne naruszenie często narzuca klientowi umiejętność odróżnienia użytych konkretnych przedmiotów. Na przykład, biorąc pod uwagę umowę pierwszego Konta, może to wyglądać następująco:

class Client
{
    public function go(Account $account, Money $money)
    {
        if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
            return;
        }
        $account->withdraw($money);
    }
}

A to automatycznie narusza zasadę open-closed [to znaczy wymóg 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 po prostu nic nie zwraca, prawdopodobnie zostanie wyrzucony wyjątek. Musisz więc sprawdzić, czy to hasEnoughMoney() -- co nie jest częścią interfejsu. Więc to wymuszone sprawdzenie zależne od konkretnej klasy jest naruszeniem OCP].

Ten punkt odnosi się również do błędnego przekonania, które dość często spotykam na temat naruszenia LSP. Mówi, że „jeśli zachowanie rodzica zmieniło się u dziecka, to narusza to LSP”. Jednak tak się nie dzieje — o ile dziecko nie narusza umowy rodzica.

person Vadim Samokhin    schedule 12.08.2018

Stwierdza, że ​​jeśli C jest podtypem E, to E można zastąpić obiektami typu C bez zmiany lub zerwania zachowania programu. Mówiąc prościej, klasy pochodne powinny być substytucyjne dla swoich klas nadrzędnych. Na przykład, jeśli syn rolnika jest rolnikiem, może pracować zamiast swojego ojca, ale jeśli syn rolnika jest krykiecistą, nie może pracować zamiast swojego ojca. ojciec.

Przykład naruszenia:

public class Plane{

  public void startEngine(){}      

}        
public class FighterJet extends Plane{}
    
public class PaperPlane extends Plane{}

W podanym przykładzie klasy FighterPlane i PaperPlane rozszerzają klasę Plane zawierającą metodę startEngine(). Jasne jest więc, że FighterPlane może uruchomić silnik, ale PaperPlane nie, więc psuje się LSP.

Klasa PaperPlane chociaż rozszerza klasę Plane i powinna być zastępowana w jej miejsce, ale nie jest odpowiednią jednostką, którą można zastąpić instancję Plane, ponieważ papierowy samolot nie może uruchomić silnika, ponieważ go nie ma. Dobrym przykładem będzie więc:

Szanowany przykład:

public class Plane{ 
} 
public class RealPlane{

  public void startEngine(){} 

}
public class FighterJet extends RealPlane{} 
public class PaperPlane extends Plane{}
person Sarmad Sohail    schedule 22.11.2020

[SOLID]

Zasada substytucji Wiki Liskov (LSP)

Warunków wstępnych nie można wzmocnić w podtypie.
Warunków końcowych nie można osłabić w podtypie.
Niezmienniki supertypu muszą być zachowane w podtypie.

*Warunek wstępny i końcowy to function (method) types[typ funkcji Swift. Funkcja Swift a metoda]

//C1 <- C2 <- C3

class C1 {}
class C2: C1 {}
class C3: C2 {}
  • Warunki wstępne (np. funkcja parameter type) mogą być takie same lub słabsze. (dąży do -› C1)

  • Warunki końcowe (np. funkcja returned type) mogą być takie same lub silniejsze (dążenie do -› C3)

  • Niezmienna zmienna[Informacje] supertypu powinna pozostać niezmienna

Szybki

class A {
    func foo(a: C2) -> C2 {
        return C2()
    }
}

class B: A {
    override func foo(a: C1) -> C3 {
        return C3()
    }
}

Jawa

class A {
    public C2 foo(C2 a) {
        return new C2();
    }
}

class B extends A {

    @Override
    public C3 foo(C2 a) { //You are available pass only C2 as parameter
        return new C3();
    }
}

Podpisywanie

  • Sybtype nie powinien wymagać od dzwoniącego więcej niż supertyp
  • Sybtype nie powinien ujawniać dla wywołującego mniej niż supertyp

Kontrawariancja typów argumentów i kowariancja typu zwracanego.

  1. Kontrawariancja argumentów metody w podtypie.
  2. Kowariancja typów zwracanych w podtypie.
  3. Żadne nowe wyjątki nie powinny być zgłaszane przez metody podtypu, z wyjątkiem sytuacji, gdy te wyjątki same w sobie są podtypami wyjątków zgłaszanych przez metody nadtypu.

[Wariancja, kowariancja, kontrawariancja]

person yoAlex5    schedule 11.11.2020

Spróbuję, rozważmy interfejs:

interface Planet{
}

Jest to realizowane według klasy:

class Earth implements Planet {
    public $radius;
    public function construct($radius) {
        $this->radius = $radius;
    }
}

Użyjesz Earth jako:

$planet = new Earth(6371);
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

Rozważmy teraz jeszcze jedną klasę, która rozszerza Ziemię:

class LiveablePlanet extends Earth{
   public function color(){
   }
}

Teraz według LSP powinieneś być w stanie używać LiveablePlanet zamiast Earth i nie powinno to zepsuć twojego systemu. Tak jak:

$planet = new LiveablePlanet(6371);  // Earlier we were using Earth here
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

Przykłady zaczerpnięte z tutaj

person prady00    schedule 26.04.2019
comment
Słaby przykład. Earth jest instancją plant, dlaczego miałoby się z niej wywodzić? - person zar; 03.05.2019