Słyszałem, że zasada substytucji Liskov (LSP) jest podstawową zasadą projektowania obiektowego. Co to jest i jakie są przykłady jego zastosowania?
Jaki jest przykład zasady substytucji Liskova?
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.
Wszyscy powinniście sprawdzić inne bezcenne Plakaty motywacyjne z zasadami SOLID.
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
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
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
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
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
SetLength
, który może wewnętrznie wywoływać SetWidth
i SetHeight
- person alancalvitti; 29.03.2019
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:
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.
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
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{}
Bird bird
. Musisz rzucić obiekt na FlyingBirds, aby wykorzystać muchę, co nie jest miłe, prawda?
- person Moody; 20.11.2017
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
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
.
Square
?
- person ca9163d9; 24.01.2012
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
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
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
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
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
Shape
lub Parallelogram
czy coś takiego?
- person AustinWBryan; 09.05.2018
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
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
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
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
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
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 klasyShape
i musi być zmieniana za każdym razem, gdy tworzone są nowe pochodne klasyShape
. 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ę zRectangle
. [...]
Square
odziedziczy funkcjeSetWidth
iSetHeight
. Funkcje te są całkowicie nieodpowiednie dlaSquare
, 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
iSetHeight
[...]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, obiektSquare
zostanie uszkodzony, ponieważ wysokość nie zostanie zmieniona. Jest to wyraźne naruszenie LSP. Funkcja nie działa dla pochodnych jej argumentów.[...]
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
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.
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.
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
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.
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/
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.
To znacznie prostsze niż myślisz:
usr->next = group->users;
group->users = usr;
- person Charlie Martin; 04.07.2012
2 + "2"
). Być może mylisz typowanie silnie z typowaniem statycznym?
- person asmeurer; 20.01.2013
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
2 + '2'
, nadal nie obsługuje 2 + 'two'
.
- person asmeurer; 27.12.2013
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:
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.
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.
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
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() { ... }
}
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.
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?”
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.
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.
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.
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ą
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.
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.
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
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.
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).
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.
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.
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();
}
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ą.
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.
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.
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.
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{}
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.
- Kontrawariancja argumentów metody w podtypie.
- Kowariancja typów zwracanych w podtypie.
- Ż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]
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
Earth
jest instancją plant
, dlaczego miałoby się z niej wywodzić?
- person zar; 03.05.2019