Jaki jest przykład zasady substytucji Liskowa?


Odpowiedzi:


892

Świetnym przykładem ilustrującym LSP (podanym przez wuja Boba w podcastu, który ostatnio słyszałem) było to, że czasami coś, co brzmi poprawnie w języku naturalnym, nie działa w kodzie.

W matematyce a Squarejest a Rectangle. Rzeczywiście jest to specjalizacja prostokąta. „Jest” powoduje, że chcesz modelować to z dziedziczeniem. Jednak jeśli w kodzie, z którego się Squarewywodzisz Rectangle, a Squarepowinno być użyteczne wszędzie tam, gdzie oczekujesz Rectangle. To powoduje dziwne zachowanie.

Wyobraź sobie, że posiadasz SetWidthi SetHeightmetody w swojej Rectangleklasie podstawowej; wydaje się to całkowicie logiczne. Jeśli jednak twoje Rectangleodniesienie wskazywało na a Square, to SetWidthi SetHeightnie ma sensu, ponieważ ustawienie jednego zmieniłoby drugie, aby je dopasować. W tym przypadku Squaretest Liskowa nie powiedzie się, Rectanglea abstrakcja Squaredziedziczenia Rectanglejest zła.

wprowadź opis zdjęcia tutaj

Wszyscy powinniście sprawdzić inne bezcenne Motywacyjne plakaty SOLIDNE zasady .


19
@ m-sharp Co jeśli jest to niezmienny prostokąt, taki, że zamiast SetWidth i SetHeight mamy zamiast tego metody GetWidth i GetHeight?
Pacerier

139
Morał tej historii: modeluj swoje klasy na podstawie zachowań, a nie właściwości; modeluj swoje dane w oparciu o właściwości, a nie zachowania. Jeśli zachowuje się jak kaczka, to z pewnością ptak.
Sklivvz

193
Cóż, kwadrat wyraźnie jest rodzajem prostokąta w prawdziwym świecie. To, czy możemy to modelować w naszym kodzie, zależy od specyfikacji. LSP wskazuje, że zachowanie podtypu powinno być zgodne z zachowaniem typu podstawowego, jak określono w specyfikacji typu podstawowego. Jeśli specyfikacja typu podstawy prostokąta mówi, że wysokość i szerokość można ustawić niezależnie, wówczas LSP mówi, że kwadrat nie może być podtypem prostokąta. Jeśli specyfikacja prostokąta mówi, że prostokąt jest niezmienny, wówczas kwadrat może być podtypem prostokąta. Chodzi o podtypy utrzymujące zachowanie określone dla typu podstawowego.
SteveT

62
@Pacerier nie ma problemu, jeśli jest niezmienny. Prawdziwy problem polega na tym, że nie modelujemy prostokątów, lecz „prostokąty o zmiennym kształcie”, tj. Prostokąty, których szerokość lub wysokość można modyfikować po utworzeniu (i nadal uważamy to za ten sam obiekt). Jeśli spojrzymy na klasę prostokąta w ten sposób, jasne jest, że kwadrat nie jest „prostokątem przekształcalnym”, ponieważ kwadrat nie może zostać przekształcony i nadal być kwadratem (ogólnie). Matematycznie nie widzimy problemu, ponieważ zmienność nie ma nawet sensu w kontekście matematycznym.
asmeurer,

14
Mam jedno pytanie dotyczące zasady. Dlaczego miałby być problem, gdyby Square.setWidth(int width)został wdrożony w ten sposób this.width = width; this.height = width;:? W takim przypadku gwarantuje się, że szerokość jest równa wysokości.
MC Emperor

488

Zasada substytucji Liskowa (LSP, ) to koncepcja programowania obiektowego, która stwierdza:

Funkcje korzystające ze wskaźników lub referencji do klas podstawowych muszą mieć możliwość korzystania z obiektów klas pochodnych bez ich znajomości.

W jego sercu LSP dotyczy interfejsów i umów, a także tego, jak zdecydować, kiedy rozszerzyć klasę, a nie zastosować innej strategii, takiej jak kompozycja, aby osiągnąć swój cel.

Najskuteczniejszym sposobem Widziałem, aby zilustrować ten punkt był w Head First OOA & D . Przedstawiają scenariusz, w którym jesteś deweloperem projektu, który ma stworzyć ramy dla gier strategicznych.

Prezentują klasę reprezentującą tablicę, która wygląda następująco:

Schemat klasy

Wszystkie metody przyjmują współrzędne X i Y jako parametry w celu zlokalizowania położenia kafelka w dwuwymiarowej tablicy Tiles. Umożliwi to twórcy gry zarządzanie jednostkami na planszy w trakcie gry.

Książka dalej zmienia wymagania, aby powiedzieć, że rama gry musi również obsługiwać plansze 3D, aby pomieścić gry, które mają lot. Tak więc wprowadzono ThreeDBoardklasę, która się rozszerza Board.

Na pierwszy rzut oka wydaje się to dobrą decyzją. Boardzawiera zarówno Heighta Widthwłaściwości i ThreeDBoardzapewnia oś z.

Rozkłada się, gdy spojrzysz na wszystkich odziedziczonych członków Board. Metody AddUnit, GetTile, GetUnitsi tak dalej, ma wszystkie parametry X i Y w Boardklasy, lecz ThreeDBoardwymaga również parametr Z.

Musisz więc ponownie zaimplementować te metody za pomocą parametru Z. Parametr Z nie ma kontekstu dla Boardklasy, a odziedziczone metody z Boardklasy tracą swoje znaczenie. Jednostka kodu próbująca wykorzystać ThreeDBoardklasę jako klasę podstawową Boardbyłaby bardzo pechowa.

Może powinniśmy znaleźć inne podejście. Zamiast powiększenia Board, ThreeDBoardpowinien składać się z Boardobiektów. Jeden Boardobiekt na jednostkę osi Z.

To pozwala nam korzystać z dobrych, obiektowych zasad, takich jak enkapsulacja i ponowne użycie, i nie narusza LSP.


10
Zobacz także problem z elipsą koła na Wikipedii, aby zobaczyć podobny, ale prostszy przykład.
Brian

Requote z @NotMySelf: „Myślę, że przykładem jest po prostu wykazanie, że dziedziczenie z tablicy nie ma sensu w kontekście ThreeDBoard, a wszystkie podpisy metody są bez znaczenia dla osi Z.”.
Contango,

1
Więc jeśli dodamy inną metodę do klasy Child, ale cała funkcjonalność Parent nadal ma sens w klasie Child, czy byłoby to łamanie LSP? Ponieważ z jednej strony zmodyfikowaliśmy interfejs do używania nieco Dziecka, z drugiej strony, jeśli podrzucimy Dziecko na Rodzica, kod, który oczekuje, że Rodzic będzie działał dobrze.
Nickolay Kondratyev

5
To jest przykład antyłiskowski. Liskov zmusza nas do uzyskania prostokąta z kwadratu. Więcej parametrów z klasy mniej parametrów. I dobrze pokazałeś, że jest źle. To naprawdę dobry żart, który został oznaczony jako odpowiedź i został poparty 200 razy odpowiedzią anty-liskovską na pytanie liskovskie. Czy zasada Liskowa jest naprawdę błędem?
Gangnus,

3
Widziałem, że dziedziczenie działa w niewłaściwy sposób. Oto przykład. Klasą podstawową powinna być 3DBoard, a klasa pochodna Board. Tablica nadal ma oś Z Max (Z) = Min (Z) = 1
Paulustrious

169

Podstawialność jest zasadą w programowaniu obiektowym, mówiącą, że w programie komputerowym, jeśli S jest podtypem T, wówczas obiekty typu T można zastąpić 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 może latać, ponieważ jest ptakiem, ale co z tym:

public class Ostrich extends Bird{}

Struś jest ptakiem, ale nie może latać, klasa strusia jest podtypem ptaka Ptak, ale nie może używać metody latania, co oznacza, że ​​łamiemy zasadę LSP.

Dobry przykład

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

3
Dobry przykład, ale co byś zrobił, gdyby klient miał Bird bird. Musisz rzucić obiekt na FlyingBirds, aby użyć muchy, co nie jest miłe, prawda?
Moody,

16
Nie. Jeśli klient ma Bird bird, to znaczy, że nie może użyć fly(). Otóż ​​to. Zdanie a Ducknie zmienia tego faktu. Jeśli klient tak FlyingBirds bird, to nawet jeśli go przejdzie Duck, powinien zawsze działać w ten sam sposób.
Steve Chamaillard

9
Czy nie byłoby to dobrym przykładem segregacji interfejsu?
Saharsh

Doskonały przykład Dzięki Człowieku
Abdelhadi Abdo

6
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.
Trzynasty

132

LSP dotyczy niezmienników.

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

class Rectangle {
    int getHeight()
    void setHeight(int value)
    int getWidth()
    void setWidth(int value)
}

class Square : Rectangle { }

Teraz mamy problem, chociaż interfejs pasuje. Powodem jest to, że naruszyliśmy niezmienniki wynikające z matematycznej definicji kwadratów i prostokątów. Sposób działania pobierających i ustawiających Rectanglepowinien spełniać następujące niezmienniki:

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

Jednak ten niezmiennik musi zostać naruszony przez poprawną implementację Square, dlatego nie jest prawidłowym zamiennikiem Rectangle.


35
Stąd trudność użycia „OO” do modelowania czegokolwiek, co chcielibyśmy modelować.
DrPizza

9
@DrPizza: Oczywiście. Jednak dwie rzeczy. Po pierwsze, takie relacje mogą być nadal modelowane w OOP, aczkolwiek niepełne lub przy użyciu bardziej skomplikowanych objazdów (wybierz dowolną, która odpowiada Twojemu problemowi). Po drugie, nie ma lepszej alternatywy. Inne mapowania / modelowania mają te same lub podobne problemy. ;-)
Konrad Rudolph

7
@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 jest punktem 3D, w którym trzeci wymiar jest pomijany (lub 0 - wszystkie punkty leżą na tej samej płaszczyźnie w Przestrzeń 3D). Ale to oczywiście nie jest praktyczne. Zasadniczo jest to jeden z przypadków, w których dziedziczenie tak naprawdę nie pomaga i nie istnieje naturalny związek między bytami. Modeluj je osobno (przynajmniej nie znam lepszego sposobu).
Konrad Rudolph

7
OOP służy do modelowania zachowań, a nie danych. Twoje klasy naruszają enkapsulację nawet przed naruszeniem LSP.
Sklivvz

2
@AustinWBryan Yep; im dłużej pracuję w tej dziedzinie, tym częściej używam dziedziczenia tylko dla interfejsów i abstrakcyjnych klas bazowych, a także dla pozostałych elementów. Czasami jest to trochę więcej pracy (mądre pisanie na klawiaturze), ale pozwala uniknąć wielu problemów i jest szeroko powtarzana przez innych doświadczonych programistów.
Konrad Rudolph,

77

Robert Martin ma doskonały artykuł na temat zasady substytucji Liskowa . Omawia subtelne i niezbyt subtelne sposoby naruszania zasady.

Niektóre istotne części artykułu (zauważ, że drugi przykład jest mocno skondensowany):

Prosty przykład naruszenia LSP

Jednym z najbardziej rażących naruszeń tej zasady jest wykorzystanie C ++ Run-Time Type Information (RTTI) w celu wybrania funkcji na podstawie typu obiektu. to znaczy:

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 DrawShapefunkcja jest źle sformułowana. Musi wiedzieć o każdej możliwej pochodnej Shapeklasy i musi być zmieniana za każdym razem, gdy Shapetworzone są nowe pochodne . Rzeczywiście, wiele osób uważa strukturę tej funkcji za anatemę dla projektowania obiektowego.

Kwadrat i prostokąt, bardziej subtelne naruszenie.

Istnieją jednak inne, znacznie bardziej subtelne sposoby naruszania LSP. Rozważ aplikację, która korzysta z Rectangleklasy w sposób opisany 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ź 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 celów i celów. Ponieważ utrzymuje się relacja ISA, logiczne jest modelowanie Square klasy jako pochodnej Rectangle. [...]

Squareodziedziczy funkcje SetWidthi SetHeight. Funkcje te są całkowicie nieodpowiednie dla a Square, ponieważ szerokość i wysokość kwadratu są identyczne. To powinna być znacząca wskazówka, że ​​istnieje problem z projektem. Istnieje jednak sposób na uniknięcie problemu. Możemy zastąpić SetWidthi SetHeight[...]

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

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

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

[...]


14
Znacznie późno, ale pomyślałem, że to ciekawy 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 dziecięcej jest silniejszy niż warunek podstawowy klasy rodzicielskiej, nie można zastąpić rodzica dzieckiem bez naruszenia warunku wstępnego. Stąd LSP.
user2023861,

@ user2023861 Masz całkowitą rację. Na tej podstawie napiszę odpowiedź.
inf3rno

40

LSP jest konieczny tam, gdzie jakiś kod uważa, że ​​wywołuje metody typu T, i może nieświadomie wywoływać metody typu S, w którym S extends T(tj. SDziedziczy, wywodzi się z podtypu lub jest jego podtypem T).

Dzieje się tak na przykład wtedy, gdy funkcja z parametrem wejściowym typu Tjest wywoływana (tzn. 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 oczekiwań (tj. Niezmienników) dla metod typu T(np. Rectangle), Nie należy ich naruszać, 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 niezmiennymi polami wciąż ma niezmienniki, np. Niezmienne układy prostokątów oczekują, że wymiary będą niezależnie modyfikowane, ale niezmienne układacze kwadratów naruszają to oczekiwanie.

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 tego podtypu Smiała przeciwwariantny parametr wejściowy i wyjściowy efekt kowariantny.

Kontrawariant oznacza, że ​​wariancja jest sprzeczna z kierunkiem dziedziczenia, tj. Typ Sikażdego parametru wejściowego każdej metody podtypu S, musi być taki sam lub nadtyp typu Tiodpowiedniego parametru wejściowego odpowiedniej metody nadtypu T.

Kowariancja oznacza, że ​​wariancja jest w tym samym kierunku dziedziczenia, tzn. Rodzaj Sowyniku każdej metody podtypu Smusi być taki sam lub podtypu typu Toodpowiedniego wyniku odpowiedniej metody nadtypu T.

Wynika to z faktu, że jeśli program wywołujący myśli, że ma typ T, myśli, że wywołuje metodę T, wówczas dostarcza argumenty typu Tii przypisuje dane wyjściowe do typu To. Gdy faktycznie wywołuje odpowiednią metodę S, każdy Tiargument wejściowy jest przypisywany do Siparametru wejściowego, a dane Sowyjściowe są przypisywane do typu To. Zatem jeśli Sinie byłyby sprzeczne z wrt Ti, to podtyp Xi, który nie byłby podtypem, Simógłby zostać przypisany Ti.

Ponadto w przypadku języków (np. Scala lub Cejlon), które mają adnotacje wariancji w miejscu definicji parametrów polimorfizmu typu (tj. Rodzajowych), ko- lub przeciwny kierunek adnotacji wariancji dla każdego parametru typu Tmusi być przeciwny lub taki sam odpowiednio do każdego parametru wejściowego lub wyjściowego (każdej metody T), który ma typ parametru type.

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


Podpisywanie jest właściwe tam, gdzie niezmienniki można wyliczyć.

Trwa wiele badań nad tym, jak modelować niezmienniki, aby były one wymuszane przez kompilator.

Typestate (patrz strona 3) deklaruje i wymusza niezmienniki stanu ortogonalne do wpisywania. Alternatywnie niezmienniki można wymusić, przekształcając twierdzenia na typy . Na przykład, aby potwierdzić, że plik jest otwarty przed jego zamknięciem, wówczas File.open () może zwrócić typ OpenFile, który zawiera metodę close (), która nie jest dostępna w File. Tic-krzyżyk API może być kolejny przykład stosując typowanie wymusić niezmienników w czasie kompilacji. System typów może być nawet kompletny w Turinga, np . Scala . Języki i dowody twierdzeń o typie zależnym formalizują modele pisania wyższego rzędu.

Ze względu na potrzebę abstrakcji semantyki zamiast rozszerzenia , spodziewam się, że zastosowanie typowania do modelowania niezmienników, tj. Ujednoliconej semantyki denotacyjnej wyższego rzędu, jest lepsze niż typestate. „Rozszerzenie” oznacza nieograniczony, permutowany skład nieskoordynowanego, modułowego rozwoju. Ponieważ wydaje mi się, że jest antytezą zjednoczenia, a tym samym stopni swobody, mieć dwa wzajemnie zależne modele (np. Typy i typowanie) do wyrażania wspólnej semantyki, których nie można zjednoczyć ze sobą w celu rozszerzenia kompozycji . Na przykład rozszerzenie podobne do problemu wyrażenia zostało ujednolicone w dziedzinie podtytułu, przeciążenia funkcji i parametrycznych domen typowania.

Moja teoretyczna pozycja jest taka, że aby istniała wiedza (patrz sekcja „Centralizacja jest ślepa i nieodpowiednia”), nigdy nie będzie ogólnego modelu, który mógłby wymusić 100% pokrycie wszystkich możliwych niezmienników w języku komputerowym kompletnym Turinga. Aby istniała wiedza, istnieje wiele nieoczekiwanych możliwości, tzn. Nieporządek i entropia muszą zawsze rosnąć. To jest siła entropii. Aby udowodnić wszystkie możliwe obliczenia potencjalnego rozszerzenia, należy z góry obliczyć wszystkie możliwe rozszerzenia.

Dlatego istnieje Twierdzenie Haltinga, tzn. Nie można rozstrzygnąć, czy każdy możliwy program w języku programowania Turinga zakończy się. Można udowodnić, że jakiś określony program kończy się (taki, w którym wszystkie możliwości zostały zdefiniowane i obliczone). Nie można jednak udowodnić, że wszelkie możliwe rozszerzenia tego programu kończą się, chyba że możliwości rozszerzenia tego programu nie są kompletne w Turingu (np. Przez wpisywanie zależne). Ponieważ podstawowym wymogiem dla kompletności Turinga jest nieograniczona rekurencja , intuicyjne jest zrozumienie, w jaki sposób twierdzenia Gödela i paradoks Russella odnoszą się do rozszerzenia.

Interpretacja tych twierdzeń uwzględnia je w uogólnionym pojęciowym rozumieniu siły entropicznej:

  • Twierdzenia Gödela o niekompletności : każda teoria formalna, w której można udowodnić wszystkie prawdy arytmetyczne, jest niespójna.
  • Paradoks Russella : każda reguła członkostwa dla zestawu, który może zawierać zestaw, albo wylicza określony typ każdego członka, albo zawiera siebie. Zatem zestawy nie mogą zostać rozszerzone lub są nieograniczoną rekurencją. Na przykład zestaw wszystkiego, co nie jest czajnikiem, obejmuje siebie, który obejmuje siebie, który obejmuje siebie itp. Zatem reguła jest niespójna, jeśli (może zawierać zestaw i) nie wylicza określonych typów (tzn. Zezwala na wszystkie nieokreślone typy) i nie zezwala na nieograniczone rozszerzenie. Jest to zestaw zbiorów, które nie są członkami siebie. Ta niezdolność do bycia spójnym i całkowicie wyliczonym na całym możliwym rozszerzeniu, to twierdzenia Gödela o niekompletności.
  • Zasada podstawienia Liskowa : ogólnie nierozstrzygalnym problemem jest to, czy jakikolwiek zestaw jest podzbiorem innego, tzn. Dziedziczenie jest na ogół nierozstrzygalne.
  • Odwołanie Linsky'ego : nierozstrzygalne jest, czym jest obliczenie czegoś, kiedy jest opisane lub postrzegane, tj. Percepcja (rzeczywistość) nie ma absolutnego punktu odniesienia.
  • Twierdzenie Coase'a : nie ma zewnętrznego punktu odniesienia, dlatego żadna bariera dla nieograniczonych możliwości zewnętrznych zawiedzie.
  • Druga zasada termodynamiki : cały wszechświat (system zamknięty, tj. Wszystko) zmierza do maksymalnego nieuporządkowania, czyli maksymalnych niezależnych możliwości.

17
@Shelyby: Zmieszałeś zbyt wiele rzeczy. Rzeczy nie są tak mylące, jak je określasz. Wiele z twoich teoretycznych twierdzeń opiera się na wątłych podstawach, takich jak: „Aby istniała wiedza, istnieje wiele nieoczekiwanych możliwości, .........” ORAZ „ogólnie rzecz biorąc, nierozstrzygalnym problemem jest to, czy jakikolwiek zbiór jest podzbiorem innego, tj. dziedziczenie jest na ogół nierozstrzygalne ”. Możesz założyć osobny blog dla każdego z tych punktów. W każdym razie twoje twierdzenia i założenia są wysoce wątpliwe. Nie wolno używać rzeczy, których nie jesteśmy świadomi!
aknon

1
@aknon Mam blog, który bardziej szczegółowo 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ą ze związanym nieskończonym końcem lub funkcja koindukcyjna ma nieznaną wartość końcową i znaną granicę początkową. Względność jest problemem po wprowadzeniu rekurencji. Właśnie dlatego Turing complete jest równoważny nieograniczonej rekurencji .
Shelby Moore III,

4
@ShelbyMooreIII Idziesz w zbyt wielu kierunkach. To nie jest odpowiedź.
Soldalma,

1
@Soldalma to odpowiedź. Nie widzisz tego w sekcji Odpowiedź. Twój jest komentarzem, ponieważ znajduje się w sekcji komentarzy.
Shelby Moore III,

1
Podobnie jak mieszanie ze światem Scala!
Ehsan M. Kermani

23

Istnieje lista kontrolna do ustalenia, czy naruszasz Liskov.

  • Jeśli naruszysz jeden z następujących elementów -> naruszysz Liskov.
  • Jeśli nie naruszysz żadnego -> nie mogę niczego zawrzeć.

Lista kontrolna:

  • Żadne nowe wyjątki nie powinny być zgłaszane w klasie pochodnej : Jeśli twoja klasa podstawowa zgłosiła ArgumentNullException, wówczas twoje podklasy mogły jedynie generować wyjątki typu ArgumentNullException lub dowolne wyjątki wyprowadzone z ArgumentNullException. Zgłaszanie IndexOutOfRangeException stanowi naruszenie Liskowa.
  • Warunków wstępnych nie można wzmocnić : Załóżmy, że klasa podstawowa współpracuje z członkiem int. Teraz twój podtyp wymaga, aby int był dodatni. Jest to wzmocnione warunki wstępne, a teraz każdy kod, który wcześniej działał doskonale z ujemnymi intami, został uszkodzony.
  • Nie można osłabić następujących warunków : załóż, że klasa podstawowa wymaga, aby wszystkie połączenia z bazą danych zostały zamknięte przed zwróceniem metody. W swojej podklasie przesłoniłeś tę metodę i pozostawiłeś otwarte połączenie dla dalszego wykorzystania. Osłabiłeś warunki końcowe tej metody.
  • Niezmienniki muszą być zachowane : najtrudniejsze i najbardziej bolesne ograniczenie do spełnienia. Niezmienniki są od pewnego czasu ukryte w klasie podstawowej, a jedynym sposobem ich ujawnienia jest odczytanie kodu klasy podstawowej. Zasadniczo musisz mieć pewność, że po zastąpieniu metody cokolwiek niezmiennego musi pozostać niezmienione po wykonaniu zastąpionej metody. Najlepszą rzeczą, jaką mogę wymyślić, jest wymuszenie tych niezmiennych ograniczeń w klasie podstawowej, ale nie byłoby to łatwe.
  • Ograniczenie historii : Podczas przesłonięcia metody nie wolno modyfikować właściwości niemodyfikowalnych w klasie podstawowej. Spójrz na ten kod i zobaczysz, że Nazwa jest zdefiniowana jako niemodyfikowalna (zestaw prywatny), ale SubType wprowadza nową metodę, która pozwala ją modyfikować (poprzez odbicie):

    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);
        }
    }
    

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

Odniesienie:


Jestem również programistą w języku C # i powiem, że twoje ostatnie stwierdzenie nie jest prawdziwe w Visual Studio 2010, z .NET 4.0. Kowariancja typów zwracanych umożliwia uzyskanie bardziej pochodnego typu zwracanego niż zdefiniowany przez interfejs. Przykład: Przykład: IEnumerable <T> (T jest kowariantem) IEnumerator <T> (T jest kowariantem) IQueryable <T> (T jest kowariantem) IGgrupowanie <TKey, TElement> (TKey i TElement są kowariantem) IComparer <T> (T jest sprzeczne) IEqualityComparer <T> (T jest sprzeczne) IComparable <T> (T jest
sprzeczne

1
Świetna i skoncentrowana odpowiedź (chociaż oryginalne pytania dotyczyły raczej przykładów niż reguł).
Mike

23

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

Chciałbym pokazać, w jaki sposób można dostosować LSP do rzeczywistego przykładu:

<?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ą wybraliśmy.

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

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


6
Ten przykład nie narusza LSP, o ile ograniczamy semantykę Database::selectQueryobsługi tylko podzbioru SQL obsługiwanego przez wszystkie silniki DB. To nie jest praktyczne ... To powiedziawszy, przykład jest nadal łatwiejszy do zrozumienia niż większość innych tutaj używanych.
Palec

5
Uznałem tę odpowiedź za najłatwiejszą do zrozumienia spośród pozostałych.
Malcolm Salvador,

22

LSP jest regułą dotyczącą umowy klauzul: jeśli klasa podstawowa spełnia kontrakt, wówczas klasy pochodne LSP muszą również spełniać tę umowę.

W pseudo-python

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

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

spełnia LSP, jeśli za każdym razem, gdy wywołujesz Foo na obiekcie pochodnym, daje dokładnie takie same wyniki jak wywoływanie Foo na obiekcie bazowym, o ile arg jest taki sam.


9
Ale ... jeśli zawsze otrzymujesz takie samo zachowanie, to po co mieć klasę pochodną?
Leonid

2
Pominąłeś punkt: to samo obserwowane zachowanie. Możesz na przykład zamienić coś na wydajność O (n) na coś funkcjonalnie równoważnego, ale na wydajność O (lg n). Lub możesz zastąpić coś, co uzyskuje dostęp do danych zaimplementowanych za pomocą MySQL i zastąpić je bazą danych w pamięci.
Charlie Martin

@Charlie Martin, kodowanie interfejsu, a nie implementacji - kopie to. Nie jest to unikalne dla OOP; promują to również języki funkcjonalne, takie jak Clojure. Nawet jeśli chodzi o Javę lub C #, myślę, że użycie interfejsu zamiast abstrakcyjnej klasy i hierarchii klas byłoby naturalne dla podanych przykładów. Python nie jest silnie napisany i nie ma interfejsów, a przynajmniej nie jest jawny. Moja trudność polega na tym, że robię OOP od kilku lat bez przestrzegania SOLID. Teraz, kiedy się z tym spotkałem, wydaje się to ograniczające i niemal wewnętrznie sprzeczne.
Hamish Grubijan,

Musisz wrócić i sprawdzić oryginalny artykuł Barbary. report-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 utrzymuje (lub nie) w żadnym język programowania, który ma jakąś formę dziedziczenia.
Charlie Martin,

1
@HamishGrubijan Nie wiem, kto ci powiedział, że Python nie jest mocno napisany na maszynie, ale okłamywali cię (a jeśli mi nie wierzysz, odpal interpretera Pythona i spróbuj 2 + "2"). Być może mylisz „silnie wpisany” z „statycznie wpisany”?
asmeurer,

21

Długa historia krótkiego, zostawmy prostokąty prostokątów i kwadratów kwadratów, praktyczny przykład przy przedłużaniu klasę nadrzędną, trzeba też zachować dokładną nadrzędnego API lub jego przedłużenia.

Załóżmy, że masz podstawową pozycję ItemsRepository.

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

        return $numberOfDeletedRows;
    }
}

I rozszerzająca go podklasa:

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 polegającego na nim.

/**
 * 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 jest uszkodzony, gdy zastępując nadrzędnego klasę z PODKLASA przerw zamówienia 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 na temat pisania oprogramowania, które można konserwować w moim kursie: https://www.udemy.com/enterprise-php/


20

Funkcje wykorzystujące wskaźniki lub odwołania do klas podstawowych muszą mieć możliwość korzystania z obiektów klas pochodnych bez znajomości tego.

Kiedy po raz pierwszy przeczytałem o LSP, założyłem, że miał on na celu bardzo ścisły sens, zasadniczo utożsamiając go z implementacją interfejsu i rzutowaniem bezpiecznym dla typu. Co oznaczałoby, że LSP jest albo zapewniony, albo nie przez sam język. Na przykład, w tym ścisłym znaczeniu, ThreeDBoard jest z pewnością substytutem dla Board, jeśli chodzi o kompilator.

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

Krótko mówiąc, co oznacza, że ​​kod klienta „wie”, że obiekt za wskaźnikiem jest typu pochodnego, a nie typ wskaźnika, nie ogranicza się do bezpieczeństwa typu. Zgodność z LSP można również przetestować poprzez zbadanie rzeczywistego zachowania obiektów. Oznacza to badanie wpływu argumentów stanu i metody obiektu na wyniki wywołań metody lub rodzajów wyjątków zgłaszanych przez obiekt.

Wracając do przykładu, teoretycznie można sprawić , że metody Board będą działać dobrze na ThreeDBoard. W praktyce jednak bardzo trudno będzie zapobiec różnicom w zachowaniu, które klient może nie obsługiwać poprawnie, bez ingerowania w funkcje, które ma dodać ThreeDBoard.

Mając tę ​​wiedzę, ocena przestrzegania LSP może być doskonałym narzędziem w określaniu, kiedy skład jest bardziej odpowiednim mechanizmem rozszerzania istniejącej funkcjonalności, a nie dziedziczeniem.


19

Myślę, że każdy w pewnym sensie opisał, czym technicznie jest LSP: Zasadniczo chcesz być w stanie oderwać się od szczegółów podtypu i bezpiecznie korzystać z nadtypów.

Więc Liskov ma 3 podstawowe zasady:

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

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

    • Słabsze warunki wstępne: Funkcje podtypu powinny przyjmować co najmniej to, co nadtyp wziął jako dane wejściowe, jeśli nie więcej.
    • Silniejsze warunki dodatkowe: Powinny wytworzyć podzbiór danych wyjściowych wytworzonych metodami nadtypu.
  3. Reguła właściwości: Wykracza to poza indywidualne wywołania funkcji.

    • Niezmienniki: Rzeczy, które zawsze są prawdziwe, muszą pozostać prawdziwe. Na przykład. rozmiar zestawu nigdy nie jest ujemny.
    • Właściwości ewolucyjne: zwykle ma to związek z niezmiennością lub stanami, 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ą zostać zachowane, a dodatkowa funkcjonalność podtypu nie powinna naruszać właściwości nadtypu.

Jeśli załatwisz te trzy rzeczy, oderwasz się od podstawowych rzeczy i piszesz luźno powiązany kod.

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


18

Ważnym przykładem zastosowania LSP jest testowanie oprogramowania .

Jeśli mam klasę A, która jest podklasą B zgodną z LSP, mogę ponownie użyć zestawu testów B do przetestowania A.

Aby w pełni przetestować podklasę A, prawdopodobnie muszę dodać jeszcze kilka przypadków testowych, ale przynajmniej mogę ponownie użyć wszystkich przypadków testowych nadklasy B.

Sposobem na osiągnięcie tego jest zbudowanie tego, co McGregor nazywa „równoległą hierarchią testowania”: moja ATestklasa odziedziczy BTest. Potrzebna jest zatem pewna forma iniekcji, aby upewnić się, że przypadek testowy działa z obiektami typu A, a nie typu B (wystarczy prosty wzór szablonu).

Zauważ, że ponowne użycie pakietu super-testów dla wszystkich implementacji podklasy jest w rzeczywistości sposobem na sprawdzenie, czy te implementacje podklasy są zgodne z LSP. Zatem można również argumentować, że należy uruchomić pakiet testów nadklasy w kontekście dowolnej podklasy.

Zobacz także odpowiedź na pytanie Stackoverflow „ Czy mogę zaimplementować serię testów wielokrotnego użytku w celu przetestowania implementacji interfejsu?


13

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() { ... }
}

Tutaj nie ma problemu, prawda? Samochód jest zdecydowanie urządzeniem transportowym i tutaj możemy zobaczyć, że zastępuje on metodę startEngine () swojej nadklasy.

Dodajmy kolejne urządzenie transportowe:

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

Teraz wszystko nie idzie zgodnie z planem! Tak, rower jest urządzeniem transportowym, jednak nie ma silnika i dlatego nie można zaimplementować metody startEngine ().

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

Rozwiązaniem tych problemów jest poprawna hierarchia dziedziczenia, aw naszym przypadku rozwiązalibyśmy problem, różnicując klasy urządzeń transportowych z silnikami i bez. Chociaż rower jest środkiem transportu, nie ma silnika. W tym przykładzie nasza definicja urządzenia transportowego jest błędna. Nie powinien mieć silnika.

Możemy zmienić naszą klasę TransportDevice w następujący sposób:

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

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

Teraz możemy rozszerzyć TransportDevice dla urządzeń niezmotoryzowanych.

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

I rozszerz Urządzenia transportowe dla urządzeń zmotoryzowanych. 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, przy jednoczesnym przestrzeganiu zasady substytucji Liskowa.

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

Nasza klasa rowerów jest również zgodna z zasadą substytucji Liskowa.

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}

9

Takie sformułowanie LSP jest zdecydowanie zbyt silne:

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

Co w zasadzie oznacza, że ​​S to kolejna, całkowicie zamknięta implementacja dokładnie tej samej rzeczy co T. I mógłbym być odważny i zdecydować, że wydajność jest częścią zachowania P ...

Zasadniczo każde użycie późnego wiązania narusza LSP. Chodzi o to, że OO polega na uzyskaniu innego zachowania, gdy zamieniamy jeden obiekt na inny!

Formuła cytowana przez wikipedię jest lepsza, ponieważ właściwość zależy od kontekstu i niekoniecznie obejmuje całe zachowanie programu.


2
Ehm, ta formuła należy do Barbary Liskov. Barbara Liskov, „Data Abstraction and Hierarchy”, SIGPLAN Notices, 23,5 (maj 1988). Nie jest on „zbyt silny”, jest „dokładnie właściwy” i nie ma implikacji, o których myślisz, że ma. Jest silny, ale ma odpowiednią siłę.
DrPizza

Potem jest bardzo niewiele podtypów w prawdziwym życiu :)
Damien Pollet

3
„Zachowanie jest niezmienione” nie oznacza, że ​​podtyp da dokładnie takie same konkretne wartości wyniku. Oznacza to, że zachowanie podtypu odpowiada oczekiwaniom w typie podstawowym. Przykład: Kształt typu podstawowego może mieć metodę draw () i określać, że ta metoda powinna renderować kształt. Dwa podtypy Kształtu (np. Kwadrat i Okrąg) wprowadziłyby metodę draw (), a wyniki wyglądałyby inaczej. Ale dopóki zachowanie (renderowanie kształtu) będzie zgodne z określonym zachowaniem Kształtu, wówczas Kwadrat i Okrąg będą podtypami Kształtu zgodnie z LSP.
SteveT,

9

W bardzo prostym zdaniu możemy powiedzieć:

Klasa potomna nie może naruszać jej charakterystyk klasy podstawowej. Musi sobie z tym poradzić. Można powiedzieć, że jest to tak samo jak podtyp.


9

Liskov's Substitution Principle (LSP)

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

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

Zasada podstawienia Liskowa stwierdza, że ​​jeśli moduł programu korzysta z klasy Base, wówczas odwołanie do klasy Base można zastąpić klasą Derived bez wpływu na funkcjonalność modułu programu.

Przykład:

Poniżej znajduje się klasyczny przykład, w którym naruszono zasadę substytucji Liskowa. W tym przykładzie zastosowano 2 klasy: Prostokąt i Kwadrat. Załóżmy, że obiekt Rectangle jest używany gdzieś w aplikacji. Rozszerzamy aplikację i dodajemy klasę Square. Klasa kwadratowa jest zwracana przez wzorzec fabryczny, oparty na niektórych warunkach i nie wiemy dokładnie, jaki typ obiektu zostanie zwrócony. Ale wiemy, że to prostokąt. Otrzymujemy obiekt prostokąta, ustawiamy szerokość na 5 i wysokość na 10 i otrzymujemy obszar. W przypadku prostokąta o szerokości 5 i wysokości 10 obszar powinien wynosić 50. Zamiast tego wynik wyniesie 100

    // 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 podstawowe bez zmiany ich zachowania.

Zobacz także: Zasada otwartego zamknięcia

Kilka podobnych koncepcji lepszej struktury: Konwencja o konfiguracji


8

Zasada substytucji Liskowa

  • Zastąpiona metoda nie powinna pozostać pusta
  • Zastąpiona metoda nie powinna generować błędu
  • Zachowanie klasy bazowej lub interfejsu nie powinno być modyfikowane (przerabiane), ponieważ zachodzi zachowanie klasy pochodnej.

7

Niektóre uzupełnienia:
Zastanawiam się, dlaczego nikt nie napisał o niezmienniku, warunkach wstępnych i warunkach końcowych klasy podstawowej, które muszą być przestrzegane przez klasy pochodne. Aby pochodna klasa D była całkowicie odporna na działanie klasy podstawowej B, klasa D musi spełniać pewne warunki:

  • Warianty klasy podstawowej muszą być zachowane przez klasę pochodną
  • Warunki wstępne klasy bazowej nie mogą być wzmacniane przez klasę pochodną
  • Klasa pochodna nie może osłabiać następujących warunków klasy podstawowej.

Tak więc pochodna musi być świadoma trzech powyższych warunków narzuconych przez klasę podstawową. Dlatego zasady podtypów są z góry ustalone. Co oznacza, że ​​stosunek „JEST A” będzie przestrzegany tylko wtedy, gdy podtyp przestrzega pewnych zasad. Zasady te, w postaci niezmienników, warunków wstępnych i warunków dodatkowych, powinny zostać określone w formalnej „ umowie projektowej ”.

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


6

LSP w prostych słowach stwierdza, że ​​obiekty tej samej nadklasy powinny mieć możliwość wymiany między sobą bez niszczenia czegokolwiek.

Na przykład, jeśli mamy Catoraz Dogklasę pochodzącą z Animalklasy, wszelkie funkcje wykorzystujące klasę zwierzę powinno mieć możliwość korzystania z Catlub Dogi zachowywać się normalnie.


4

Czy wdrożenie ThreeDBoard pod względem tablicy będzie tak przydatne?

Być może możesz chcieć traktować plastry ThreeDBoard w różnych płaszczyznach jako planszę. W takim przypadku możesz wyodrębnić interfejs (lub klasę abstrakcyjną) dla tablicy, aby umożliwić wiele implementacji.

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


1
Myślę, że przykładem jest po prostu wykazanie, że dziedziczenie z tablicy nie ma sensu w kontekście ThreeDBoard, a wszystkie podpisy metody są bez znaczenia dla osi Z.
NotMyself,

4

Kwadrat to prostokąt, którego szerokość równa się wysokości. Jeśli kwadrat ustawia dwa różne rozmiary dla szerokości i wysokości, narusza to niezmiennik kwadratowy. Można to obejść poprzez wprowadzenie efektów ubocznych. Ale jeśli prostokąt miał setSize (wysokość, szerokość) z warunkiem wstępnym 0 <wysokość i 0 <szerokość. Pochodna metoda podtypu wymaga wysokość == szerokość; silniejszy warunek wstępny (i to narusza lsp). To pokazuje, że chociaż kwadrat jest prostokątem, nie jest prawidłowym podtypem, ponieważ warunek wstępny jest wzmocniony. Obejście (ogólnie rzecz biorąc, zła rzecz) powoduje efekt uboczny, co osłabia stan postu (co narusza lsp). setWidth na podstawie ma warunek słupka 0 <szerokość. Wyprowadzony osłabia go o wysokości == szerokości.

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


4

Zasada ta została wprowadzona przez Barbarę Liskov w 1987 roku i rozszerza zasadę otwartego zamknięcia, koncentrując się na zachowaniu nadklasy i jej podtypów.

Jego znaczenie staje się oczywiste, gdy weźmiemy pod uwagę konsekwencje jego naruszenia. Rozważ aplikację korzystającą z następującej klasy.

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 oprócz prostokątów wymaga także manipulowania kwadratami. Ponieważ kwadrat jest prostokątem, klasę kwadratu należy wyprowadzić z klasy Prostokąt.

public class Square : Rectangle
{
} 

W ten sposób napotkamy jednak dwa problemy:

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

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 do przodu i rozważmy tę inną funkcję:

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

Gdybyśmy przekazali odwołanie do obiektu kwadratowego do tej funkcji, naruszylibyśmy LSP, ponieważ funkcja nie działa dla pochodnych jej argumentów. Szerokość i wysokość właściwości nie są polimorficzne, ponieważ nie zostały zadeklarowane jako wirtualne w prostokącie (kwadratowy obiekt zostanie uszkodzony, ponieważ wysokość nie zostanie zmieniona).

Jednak deklarując, że właściwości setera są wirtualne, napotkamy kolejne naruszenie, OCP. W rzeczywistości utworzenie kwadratu klasy pochodnej powoduje zmiany w prostokącie klasy podstawowej.


3

Najczystszym wyjaśnieniem LSP, które do tej pory znalazłem, jest „Zasada podstawienia Liskowa mówi, że obiekt klasy pochodnej powinien być w stanie zastąpić obiekt klasy podstawowej bez powodowania błędów w systemie lub modyfikowania zachowania klasy podstawowej „ stąd . W artykule podano przykładowy kod naruszenia LSP i jego naprawienia.


1
Podaj przykłady kodu dotyczące przepływu stosu.
sebenalern

3

Powiedzmy, że używamy prostokąta w naszym kodzie

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

W naszej klasie geometrii dowiedzieliśmy się, że kwadrat jest specjalnym rodzajem prostokąta, ponieważ jego szerokość jest taka sama jak jego wysokość. Stwórzmy również Squareklasę na podstawie tych informacji:

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

Gdybyśmy wymienić Rectanglez Squarenaszego pierwszego kodu, a potem będzie przerwa:

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

To dlatego, że Squarema nowy warunek nie mieliśmy w Rectangleklasie: width == height. Według LSP Rectangleinstancje powinny być zastępowalne Rectangleinstancjami podklasy. Jest tak, ponieważ te instancje przechodzą sprawdzanie typu dlaRectangle instancji i dlatego powodują nieoczekiwane błędy w kodzie.

To był przykład części „warunków wstępnych, których nie można wzmocnić w podtypie” w artykule wiki . Podsumowując, naruszenie LSP prawdopodobnie spowoduje błędy w kodzie w pewnym momencie.


3

LSP mówi, że „Obiekty powinny być zastępowalne według ich podtypów”. Z drugiej strony zasada ta wskazuje

Klasy potomne nigdy nie powinny łamać definicji typów klas nadrzędnych.

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();
}

Naprawianie 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();
}

2

Zachęcam do zapoznania się z artykułem: Naruszenie zasady substytucji Liskowa (LSP) .

Możesz znaleźć wyjaśnienie, czym jest Zasada Zastępstwa Liskowa, ogólne wskazówki pomagające odgadnąć, czy już ją naruszyłeś, oraz przykład podejścia, które pomoże ci zwiększyć bezpieczeństwo w hierarchii klas.


2

ZASADA SUBSTYTUCJI LISKOWA (z książki Marka Seemanna) stwierdza, że ​​powinniśmy być w stanie zastąpić jedną implementację interfejsu inną bez przerywania ani klienta, ani implementacji. Ta zasada pozwala sprostać wymaganiom, które pojawią się w przyszłości, nawet jeśli możemy ” przewidzieć je dzisiaj.

Jeśli odłączymy komputer od ściany (implementacja), ani gniazdko sieciowe (interfejs), ani komputer (klient) nie ulegną awarii (w rzeczywistości, jeśli jest to laptop, może nawet działać na baterie przez pewien czas) . Jednak w przypadku oprogramowania klient często oczekuje, że usługa będzie dostępna. Jeśli usługa została usunięta, otrzymujemy wyjątek NullReferenceException. Aby poradzić sobie z tego typu sytuacją, możemy stworzyć implementację interfejsu, który „nic nie robi”. Jest to wzorzec projektowy znany jako Null Object [4] i odpowiada w przybliżeniu odłączeniu komputera od ściany. Ponieważ używamy luźnego sprzężenia, możemy zastąpić prawdziwą implementację czymś, co nic nie robi bez powodowania problemów.


2

Zasada podstawienia Likowa stwierdza, że jeśli moduł programu korzysta z klasy Base, wówczas odwołanie do klasy Base można zastąpić klasą Derived bez wpływu na funkcjonalność modułu programu.

Cel - typy pochodne muszą całkowicie zastępować typy podstawowe.

Przykład - typy zwracanych wariantów w java.


1

Oto fragment tego postu który ładnie wyjaśnia rzeczy:

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

Co oznacza naruszenie tej zasady? Oznacza to, że obiekt nie spełnia umowy narzuconej przez abstrakcję wyrażoną za pomocą interfejsu. Innymi słowy, oznacza to, że źle zidentyfikowałeś swoje abstrakcje.

Rozważ następujący przykład:

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. Wynika to z faktu, że umowa konta mówi nam, że konto zostanie wycofane, ale nie zawsze tak jest. Co powinienem zrobić, aby to naprawić? Właśnie modyfikuję umowę:

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 zdolność do odróżnienia zastosowanych konkretnych obiektów. Na przykład biorąc pod uwagę pierwszą umowę Konta, może wyglądać następująco:

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

I to automatycznie narusza zasadę otwartego zamknięcia [to znaczy wymogu wypłaty pieniędzy. Ponieważ nigdy nie wiadomo, co się stanie, jeśli obiekt naruszający umowę nie ma wystarczającej ilości pieniędzy. Prawdopodobnie nic nie zwraca, prawdopodobnie zostanie zgłoszony wyjątek. Musisz więc sprawdzić, czy to hasEnoughMoney()nie jest częścią interfejsu. Zatem ta wymuszona kontrola zależna od konkretnej klasy stanowi naruszenie OCP].

Ten punkt dotyczy także błędnego przekonania, które dość często spotykam na temat naruszenia LSP. Mówi: „jeśli zachowanie rodzica zmieniło się u dziecka, narusza to LSP”. Nie robi to jednak - o ile dziecko nie naruszy umowy rodzica.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.