Jaki jest przykład Zasady substytucji Liskowa?

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

Author: Steve Chambers, 0000-00-00

26 answers

Świetnym przykładem ilustrującym LSP (podany przez wujka Boba w podcaście, który słyszałem niedawno) było to, że czasami coś, co brzmi dobrze w języku naturalnym, nie działa w kodzie.

W matematyce a Square jest Rectangle. Rzeczywiście jest to specjalizacja prostokąta. "Is a" sprawia, że chcesz modelować to dziedziczeniem. Jeśli jednak w kodzie Square wywodzisz się z Rectangle, to Square powinno być używane wszędzie tam, gdzie oczekujesz Rectangle. To powoduje dziwne zachowanie.

Wyobraź sobie, że masz metody SetWidth i SetHeight na swojej klasie bazowej; wydaje się to całkowicie logiczne. Jeśli jednak Twoje odniesienie Rectangle wskazuje na Square, to SetWidth i SetHeight nie ma sensu, ponieważ ustawienie jednego zmieniłoby drugi, aby pasował do niego. W tym przypadku Square nie spełnia testu podstawienia Liskowa z Rectangle I abstrakcja posiadania Square dziedziczenia z Rectangle jest zła.

Powinniście sprawdzić inne bezcenne solidne zasady Plakaty Motywacyjne .

 673
Author: m-sharp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-04 09:36:26

Zasada podstawienia Liskowa (LSP, lsp ) to koncepcja programowania obiektowego, która mówi:

Funkcje wykorzystujące wskaźniki lub odniesienia do klas bazowych muszą być możliwość używania obiektów klas pochodnych nie wiedząc o tym.

W jego sercu LSP jest o interfejsach i kontraktach, a także Jak zdecydować, kiedy rozszerzyć klasę vs. użyj innej strategii, takiej jak kompozycja, aby osiągnąć swój cel.

Najskuteczniejszy sposób I widziałem, aby zilustrować ten punkt był w Head First OOA & D. Przedstawiają one scenariusz, w którym jesteś deweloperem w projekcie budowy ramy dla gier strategicznych.

Prezentują klasę, która reprezentuje planszę, która wygląda tak:

Diagram Klas

Wszystkie metody przyjmują współrzędne X i Y jako parametry, aby zlokalizować położenie płytki w dwuwymiarowej tablicy Tiles. Pozwoli to twórcy gry zarządzać jednostkami na planszy w trakcie mecz.

Książka idzie dalej, aby zmienić wymagania, aby powiedzieć, że praca ramki gry musi również obsługiwać plansze do gier 3D, aby pomieścić gry, które mają lot. Tak więc wprowadzono klasę ThreeDBoard, która rozszerza Board.

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

Gdzie to się rozpada, gdy spojrzysz na wszystkich pozostałych członków odziedziczonych po Board. Metody dla AddUnit, GetTile, GetUnits i tak dalej, wszystkie przyjmują zarówno parametry X, jak i Y w klasie Board, ale ThreeDBoard również potrzebuje parametru Z.

Więc musisz zaimplementować te metody ponownie z parametrem Z. Parametr Z nie ma kontekstu dla klasy Board, a metody dziedziczone z klasy Board tracą swoje znaczenie. Jednostka kodu próbująca użyć klasy ThreeDBoard jako swojej podstawowej klasy Board byłaby bardzo pechowa.

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

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

 401
Author: NotMyself,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-02-08 14:07:47

LSP dotyczy niezmienników.

Klasyczny przykład jest podany przez następującą pseudokodową deklarację (pominięto 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 getterów i seterów, a Rectangle powinien 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 ta niezmienna musi być naruszona przez poprawną implementacja Square, dlatego nie jest poprawnym substytutem Rectangle.

 113
Author: Konrad Rudolph,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-06 11:30:48

Robert Martin ma znakomitą pracę na temat podstawienia Liskowa. Omawia subtelne i nie tak subtelne sposoby naruszania zasady.

Niektóre istotne części artykułu (zauważ, że drugi przykład jest mocno skondensowany):
Jest to jeden z najbardziej znanych i cenionych na świecie producentów.]}

Jednym z najbardziej rażących naruszeń tej zasady jest użycie C++ Run-Time Type Information (RTTI), aby wybrać funkcję opartą na typ 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żdą możliwą pochodną klasy Shape i musi ona zostać zmieniona ilekroć powstają nowe pochodne Shape. Rzeczywiście, wielu postrzega strukturę tej funkcji jako anathemę do projektowania zorientowanego obiektowo.

Kwadrat i prostokąt, bardziej subtelne naruszenie.

Istnieją jednak inne, znacznie bardziej subtelne sposoby naruszania LSP. Rozważ aplikacja, która używa klasy Rectangle zgodnie z opisem 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 kwadraty oprócz prostokątów. [...]

Oczywiście, kwadrat jest prostokątem dla wszystkich normalnych intencji i celów. Ponieważ relacja ISA utrzymuje się, logiczne jest modelowanie Square class as being derived from Rectangle. [...]

Square dziedziczy funkcje SetWidth i SetHeight. Te funkcje to zupełnie nieodpowiednie dla Square, ponieważ szerokość i wysokość kwadratu są identyczne. To powinna być znacząca wskazówka że jest problem z projektem. Jest jednak sposób na pominąć problem. Możemy obejść SetWidth i SetHeight [...]

Ale rozważmy 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, Square obiekt zostanie uszkodzony, ponieważ wysokość nie zostanie zmieniona. To jest jasne naruszenie LSP. Funkcja nie działa dla pochodne jego argumentów.

[...]

 70
Author: Phillip Wells,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-16 09:35:04

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 (tzn. S dziedziczy, wywodzi lub jest podtypem supertype T).

Na przykład, dzieje się tak, gdy funkcja z parametrem wejściowym typu T jest wywoływana (tzn. wywoływana) z wartością argumentu typu S. Lub, gdy identyfikatorowi typu T przypisana jest 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 metody typu S (np. Square) są wywoływane.

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 nadal ma niezmienniki, np. niezmienne ustawiacze prostokątów oczekują, że wymiary będą niezależnie modyfikowane, ale ustawiacze kwadratowe niezmienne 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 musiała mają sprzeczne parametry wejściowe i kowariantne wyjście.

Kontrawariant oznacza, że wariancja jest sprzeczna z kierunkiem dziedziczenia, tzn. Typ Si każdego parametru wejściowego każdej metody podtypu S musi być taki sam lub supertype typu Ti odpowiedniego parametru wejściowego odpowiedniej metody supertype T.

Kowariancja oznacza, że wariancja jest w tym samym kierunku dziedziczenia, tzn. Typ So Wyjście każdej metody podtypu S musi być tym samym lub Podtyp typu To odpowiedniego wyjścia odpowiedniej metody supertype T.

Dzieje się tak, ponieważ jeśli wywołujący myśli, że ma typ T, myśli, że wywołuje metodę T, to dostarcza argumenty typu Ti i przypisuje wyjście do typu To. Jeśli rzeczywiście 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, jeśli Si nie były sprzeczne w.r.t. do Ti, to podtyp Xi-który nie byłby Podtyp Si-mógłby być przypisany do Ti.

Scala lub Cejlon), które mają adnotacje wariancji na parametrach polimorfizmu typu (tj. generyki), co - lub przeciw - kierunek adnotacji wariancji dla każdego parametru typu typu T musi być naprzeciw lub w tym samym kierunku odpowiednio do każdego parametru wejściowego lub wyjściowego (każdej metody T), która ma typ parametru typu.

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


Podtyp jest odpowiedni , gdzie można wyliczyć niezmienniki.

Trwa wiele badań nad tym, jak modelować niezmienniki, tak, że są one egzekwowane przez kompilator.

Typestate (patrz str. 3) deklaruje i wymusza niezmienniki stanu ortogonalne do type. Alternatywnie, niezmienniki mogą być wymuszone przez konwertowanie twierdzeń na typy . Na przykład, aby upewnić się, że plik jest otwarty przed jego zamknięciem, następnie File.open() może zwrócić Typ OpenFile, który zawiera metodę close (), która nie jest dostępna w pliku. W ten sposób można uzyskać dostęp do wszystkich funkcji, takich jak np. niezmienniki w czasie kompilacji. System typów może być nawet Turing-kompletny, np. Scala. Języki zależne i teorematy formalizują modele typowania wyższego rzędu.

Ze względu na potrzebę semantyki do abstrakcji nad rozszerzeniem , oczekuję, że użycie typowania do modelowania niezmienników, tj. zunifikowanej semantyki denotacyjnej wyższego rzędu, jest lepsze od Typestatu. "Rozszerzenie" oznacza nieograniczoną, permutowaną kompozycję nieskoordynowanych, modułowych rozwój. Ponieważ wydaje mi się, że jest to antyteza unifikacji, a więc stopni swobody, 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 dla rozszerzalnej kompozycji. Na przykład problem wyrażenia podobny do rozszerzenia został ujednolicony w domenach podtypowania, przeciążania funkcji i parametrycznego typowania.

Moje teoretyczne stanowisko jest takie, że dla wiedzy istnieje (zobacz część "centralizacja jest ślepa i nienadająca się"), nie będzie nigdy ogólnego modelu, który może wymusić 100% pokrycie wszystkich możliwych niezmienników w języku komputerowym Turinga. Aby wiedza istniała, istnieje wiele nieoczekiwanych możliwości, tzn. zaburzenia i Entropia muszą zawsze wzrastać. To jest siła entropii. Aby udowodnić wszystkie możliwe obliczenia potencjalnego rozszerzenia, należy obliczyć a priori wszystkie możliwe rozszerzenia.

Dlatego istnieje twierdzenie o zatrzymaniu, tzn. nie da się przewidzieć, czy każdy możliwy program w języku programowania Turing-complete zakończy się. Można udowodnić, że jakiś konkretny program się kończy (taki, w którym wszystkie możliwości zostały zdefiniowane i obliczone). Nie jest jednak możliwe udowodnienie, że wszystkie możliwe rozszerzenia tego programu wygasają, chyba że możliwości rozszerzenia tego programu nie są kompletne (np. poprzez wpisywanie zależne). Ponieważ podstawowym wymogiem Turinga-kompletności jest nieograniczona rekurencja , intuicyjnie można zrozumieć, w jaki sposób twierdzenia Gödla o niekompletności i paradoks Russella mają zastosowanie do rozszerzenia.

W 1990 roku, w wyniku badań przeprowadzonych przez Instytut Fizyki Uniwersytetu Warszawskiego, w 1991 roku, w Instytucie Fizyki Uniwersytetu Warszawskiego, w 1994 roku, w 1999 roku, w 1999 roku, w 1999 roku, w 1999 roku.]}
  • Twierdzenie Gödla 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łonkowska dla zbiór, który może zawierać zbiór, albo wylicza określony typ każdego elementu, albo zawiera siebie. Tak więc zbiory albo nie mogą być rozszerzone, albo są nieograniczoną rekurencją. Na przykład, zestaw wszystkiego, co nie jest czajnikiem, obejmuje siebie, co obejmuje siebie, co obejmuje siebie, itp.... Tak więc reguła jest niespójna, jeśli (może zawierać zbiór i) nie wylicza konkretnych typów (tzn. zezwala na wszystkie nieokreślone typy) i nie zezwala na nieograniczone rozszerzenie. Jest to zbiór zestawów, które są Nie ich członkowie. Ta niezdolność do bycia zarówno spójnym, jak i całkowicie wyliczonym nad wszystkimi możliwymi rozszerzeniami, jest twierdzeniem Gödla o niekompletności.
  • zasada substytucji Liskowa : ogólnie jest problemem nie do podjęcia decyzji, czy dowolny zbiór jest podzbiorem innego, tzn. dziedziczenie jest ogólnie nie do podjęcia decyzji.
  • Linsky odwołując się : nie da się przewidzieć, czym jest obliczenie czegoś, kiedy jest opisane lub postrzegane, tzn. 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 (układ zamknięty, czyli wszystko) zmierza do maksimum nieporządku, czyli do maksimum niezależnych możliwości.
 39
Author: Shelby Moore III,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:26:33

Zastępowalność jest zasadą w programowaniu obiektowym stwierdzającą, ż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 może latać z powodu swojego ptaka, ale co z tym:

public class Ostrich extends Bird{}

Struś jest ptakiem, ale nie potrafi latać, Klasa strusia jest podtypem klasy Ptak, ale nie może używać metody muchy, czyli ż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{} 
 37
Author: Maysara Alhindi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-07-04 19:58:38

LSP jest regułą o kontrakcie klas: jeśli klasa bazowa spełnia Kontrakt, to przez klasy pochodne LSP musi również spełniać ten kontrakt.

W Pseudo-Pythonie

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 wywołanie Foo na obiekcie pochodnym daje dokładnie takie same wyniki jak wywołanie Foo na obiekcie bazowym, o ile arg jest takie samo.

 20
Author: Charlie Martin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-11-08 17:53:32

Funkcje, które używają wskaźników lub odwołań do klas bazowych, muszą być w stanie używać obiektów klas pochodnych, nie wiedząc o tym.

Kiedy po raz pierwszy przeczytałem o LSP, założyłem, że chodziło o to w bardzo ścisłym sensie, zasadniczo zrównując to z implementacją interfejsu i bezpiecznym dla typu odlewaniem. Co oznaczałoby, że LSP jest zapewniony lub nie przez sam język. Na przykład, w tym ścisłym znaczeniu, ThreeDBoard jest z pewnością zastępowalny dla Zarządu, o ile kompilator jest zaniepokojony.

Po przeczytaniu więcej na temat pojęcia choć okazało się, że LSP jest ogólnie interpretowane szerzej niż to.

W skrócie, co to znaczy dla kodu klienta, aby "wiedzieć" , że obiekt za wskaźnikiem jest typu pochodnego, a nie typu wskaźnika, nie jest ograniczone do bezpieczeństwa typu. Przestrzeganie LSP jest również testowane poprzez sondowanie rzeczywistego zachowania obiektów. Czyli zbadanie wpływu stanu obiektu i argumentów metody na wyniki wywołania metod lub typy WYJĄTKÓW wyrzucanych z obiektu.

Wracając ponownie do przykładu, w teorii metody planszowe mogą być wykonane tak, aby działały 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 naruszania funkcjonalności, którą ma dodać ThreeDBoard.

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

 19
Author: Chris Ammerman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-10-07 02:44:33

O dziwo, nikt nie opublikował oryginału papieru opisującego lsp. Nie jest to łatwe do odczytania jako jeden Robert Martin, ale warto.

 16
Author: amit,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-02-25 06:21:45

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, aby przetestować A.

Aby w pełni przetestować podklasę a, prawdopodobnie muszę dodać jeszcze kilka przypadków testowych, ale przynajmniej mogę ponownie wykorzystać wszystkie przypadki testowe superclass B.

Sposobem na uświadomienie sobie tego jest zbudowanie czegoś, co McGregor nazywa "równoległą hierarchią do testowania": moja ATest Klasa odziedziczy po BTest. Pewna forma wtrysku jest następnie potrzebna, aby upewnić się, że przypadek testowy działa z obiektami typu A, a nie Typu B (wystarczy prosty wzór metody szablonowej).

Zauważ, że ponowne użycie pakietu super-test dla wszystkich implementacji podklasy jest w rzeczywistości sposobem na sprawdzenie, czy te implementacje podklasy są zgodne z LSP. Tak więc, można również argumentować, że jeden powinien uruchomić pakiet testów klasy superclass w kontekście dowolnej podklasy.

Zobacz też odpowiedź na Stackoverflow pytanie " Czy Mogę zaimplementować serię testów wielokrotnego użytku w celu przetestowania implementacji interfejsu?"

 15
Author: avandeursen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:55:00

Jest lista kontrolna, aby ustalić, czy naruszasz Liskova.

  • Jeśli naruszasz jeden z poniższych elementów - > naruszasz Liskov.
  • Jeśli nie naruszysz żadnego - > nie możesz niczego zawrzeć.

Lista kontrolna:

  • żadne nowe wyjątki nie powinny być wyrzucane do klasy pochodnej: Jeśli twoja klasa bazowa wyrzuciła ArgumentNullException, wtedy twoje podklasy mogły rzucać wyjątki typu ArgumentNullException lub jakiekolwiek wyjątki wywodzi się z ArgumentNullException. Rzucanie IndexOutOfRangeException jest naruszeniem Liskowa.
  • warunki wstępne nie mogą być wzmocnione : Załóżmy, że twoja klasa bazowa działa 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 działał doskonale wcześniej z ujemnymi intami, jest łamany.
  • Post-conditions cannot be osłabione : Załóżmy, że twoja klasa bazowa wymaga wszystkich połączeń do bazy danych powinna zostać zamknięta przed zwróceniem metody. W swojej podklasie przekroczyłeś tę metodę i zostawiłeś połączenie Otwarte do dalszego ponownego użycia. Osłabiłeś post-Warunki tej metody.
  • niezmienniki muszą być zachowane : najtrudniejsze i bolesne ograniczenie do spełnienia. Niezmienniki są przez pewien czas ukryte w klasie bazowej i jedynym sposobem na ich ujawnienie jest odczytanie kodu klasy bazowej. Zasadniczo musisz być pewien, kiedy nadpisujesz metodę, cokolwiek niezmiennego musi pozostać niezmieniona po wykonaniu nadpisanej metody. Najlepszą rzeczą, jaką mogę wymyślić, jest wymuszenie tych niezmiennych ograniczeń w klasie bazowej, ale nie byłoby to łatwe.
  • Ograniczenie historii: gdy nadpisujesz metodę, nie możesz modyfikować niemodyfikowanej właściwości w klasie bazowej. Spójrz na ten kod i zobaczysz, że nazwa jest zdefiniowana jako niemodyfikowalna (zestaw prywatny), ale Podtyp wprowadza nową metodę, która umożliwia 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);
        }
    }
    

Istnieją 2 inne pozycje: Kontrawarancja argumentów metody i Kowariancja typów zwrotnych. Ale nie jest to możliwe w C# (jestem programistą C#), więc nie dbam o nich.

Odniesienie:

 15
Author: Cù Đức Hiếu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-12 07:31:17

Wydaje mi się, że każdy w pewnym sensie zajmował się tym, czym technicznie jest LSP: w zasadzie chcesz być w stanie abstrahować od szczegółów podtypu i bezpiecznie używać supertypów.

Więc Liskov ma 3 podstawowe zasady:

  1. Reguła podpisu: każda operacja supertype w podtypie powinna mieć poprawną implementację składniowo. Coś, co kompilator będzie w stanie sprawdzić za Ciebie. Jest mała reguła o rzucaniu mniej wyjątków i byciu przynajmniej tak dostępnym jak metody supertype.

  2. Zasada metod: implementacja tych operacji jest semantycznie poprawna.

    • słabsze warunki wstępne: funkcje podtypu powinny przyjmować co najmniej to, co supertyp przyjął jako dane wejściowe, jeśli nie więcej.
    • silniejsze Warunki Postconditions: powinny one wytworzyć podzbiór danych wyjściowych wytworzonych przez metody supertype.
  3. Properties Rule: to wykracza poza Indywidualne wywołania funkcji.

    • niezmienniki: rzeczy, które są zawsze prawdziwe, muszą pozostać prawdziwe. Np. rozmiar zestawu nigdy nie jest ujemny.
    • Właściwości ewolucyjne: zwykle ma to coś wspólnego z niezmiennością lub rodzajem stanów, w których obiekt może być. A może obiekt tylko rośnie i nigdy się nie kurczy, więc metody podtypu nie powinny tego zrobić.

Wszystkie te właściwości muszą być zachowane, a funkcja dodatkowego podtypu nie powinna naruszać właściwości supertype.

Jeśli te trzy rzeczy są załatwione, masz wyabstrahowane od podstawowych rzeczy i piszesz luźno sprzężony kod.

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

 13
Author: snagpaul,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-18 16:54:19

Ta formuła LSP jest o wiele za silna:

Jeśli 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 jest niezmienione, gdy o1 jest podtypem o2, to S jest podtypem T.

Co w zasadzie oznacza, że S jest inną, całkowicie zamkniętą implementacją dokładnie tego samego co T. i mógłbym być odważny i zdecydować, że wydajność jest częścią zachowania P...

Więc, zasadniczo każde użycie opóźnionego wiązania narusza LSP. Chodzi o to, aby uzyskać inne zachowanie, kiedy zamieniamy obiekt jednego rodzaju na inny!

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

 9
Author: Damien Pollet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-04-03 00:11:54

Long krótka historia, zostawmy prostokąty prostokąty i kwadraty kwadraty, praktyczny przykład podczas rozszerzania klasy rodzica, musisz albo zachować dokładne API rodzica, albo je rozszerzyć.

Powiedzmy, że masz bazę ItemsRepository.

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 bazowym API 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ępowanie rodzic klasa z podklasa zrywa 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 :(
    }
}
 7
Author: EnchanterIO,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-28 23:10:45

Jakiś dodatek:
zastanawiam się , dlaczego nikt nie napisał o niezmiennych, wstępnych i postowych warunkach klasy bazowej, które muszą być przestrzegane przez klasy pochodne. Aby pochodna Klasy D była całkowicie odrzucalna przez klasę bazową B, Klasa D musi spełniać pewne warunki:

  • in-warianty klasy bazowej muszą być zachowane przez klasę pochodną
  • warunki wstępne klasy bazowej nie mogą być wzmocnione przez klasę pochodną
  • Post-conditions of the klasa bazowa nie może być osłabiona przez klasę pochodną.

Więc pochodna musi być świadoma powyższych trzech warunków narzuconych przez klasę bazową. Stąd Zasady podtypowania są wstępnie ustalone. Co oznacza, że relacja "jest" jest przestrzegana tylko wtedy, gdy pewne zasady są przestrzegane przez Podtyp. Zasady te, w postaci niezmienników, precodionów i postkodionów, powinny zostać określone w formalnym " wzór umowy ".

Dalsze dyskusje na ten temat dostępne na moim blogu: zasada substytucji Liskowa

 6
Author: aknon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-12-27 05:20:22

W bardzo prostym zdaniu możemy powiedzieć:

Klasa potomna nie może naruszać jej cech klasy bazowej. Musi być do tego zdolny. Możemy powiedzieć, że to to samo co Podtyp.

 6
Author: Alireza Rahmani,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-16 09:09:53

Kwadrat to prostokąt, w którym szerokość jest równa wysokości. Jeśli kwadrat ustawia dwa różne rozmiary dla szerokości i wysokości narusza niezmiennik kwadratu. Działa to poprzez wprowadzenie efektów ubocznych. Ale jeśli prostokąt miał ustawioną wielkość (wysokość, szerokość) z warunkiem 0

Dlatego kwadrat z możliwością zmiany rozmiaru nie jest prostokątem z możliwością zmiany rozmiaru.

 4
Author: Wouter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-07-27 21:56:40

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

Chciałbym pokazać, jak można dostosować LSP do przykładu z prawdziwego świata:

<?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 naruszać LSP w tej konfiguracji wykonując jedną prostą zmianę w ten sposób:

<?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 produkują ten sam wynik.

 4
Author: Steve Chamaillard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-18 19:07:29

Czy implementacja ThreeDBoard w postaci tablicy tablicy byłaby tak przydatna?

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

Jeśli chodzi o interfejs zewnętrzny, możesz rozważyć użycie interfejsu dla dwóch i trzech płyt (chociaż żadna z powyższych metod nie pasuje).

 3
Author: Tom Hawtin - tackline,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-11 15:28:44

Zachęcam do przeczytania artykułu:

Znajdziesz tam wyjaśnienie, czym jest zasada substytucji Liskowa, ogólne wskazówki, które pomogą Ci odgadnąć, czy już ją złamałeś i przykład podejścia, które pomoże Ci uczynić hierarchię klasową bezpieczniejszą.

 2
Author: Ryszard Dżegan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-09 07:32:23

Najbardziej klarowne Wyjaśnienie dla LSP, jakie do tej pory znalazłem, brzmiało:" zasada podstawienia Liskova mówi, że obiekt klasy pochodnej powinien być w stanie zastąpić obiekt klasy bazowej bez wprowadzania błędów w systemie lub modyfikowania zachowania klasy bazowej " z tutaj. Artykuł podaje przykład kodu do łamania LSP i naprawiania go.

 2
Author: Prasa,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-05-03 19:34:38

Zasada substytucji LISKOWA (z książki Marka Seemanna) mówi, że powinniśmy być w stanie zastąpić jedną implementację interfejsu inną bez łamania klienta lub implementation.It ta zasada pozwala na sprostanie wymaganiom, które pojawią się w przyszłości, nawet jeśli nie możemy ich przewidzieć dzisiaj.

Jeśli odłączymy komputer od ściany( implementacja), ani gniazdko (interfejs), ani komputer (Klient) się nie zepsuje (w rzeczywistości, jeśli jest to laptop komputer, może nawet działać na swoich bateriach przez pewien czas). W przypadku oprogramowania klient często oczekuje jednak, że usługa będzie dostępna. Jeśli usługa została usunięta, otrzymujemy NullReferenceException. Aby poradzić sobie z tego typu sytuacjami, możemy stworzyć implementację interfejsu, który "nic nie robi"."Jest to wzorzec projektowy znany jako obiekt Null[4] i odpowiada mniej więcej odłączeniu komputera od ściany. Ponieważ używamy luźnego sprzężenia, możemy zastąpić rzeczywistą implementacja z czymś, co nie robi nic bez powodowania kłopotów.

 2
Author: Raghu Reddy Muttana,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-12 17:16:29

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 typem prostokąta, ponieważ jego szerokość jest taka sama jak jego wysokość. Stwórzmy Square klasę również na podstawie tego info:

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

Jeśli zamienimy Rectangle na Square w naszym pierwszym kodzie, to złamie się:

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

Dzieje się tak dlatego, że Square ma nowy warunek wstępny, którego nie mieliśmy w Rectangle klasie: width == height. Według LSP instancje Rectangle powinna być zastępowalna instancjami Rectangle podklasy. Dzieje się tak, ponieważ te instancje przechodzą kontrolę typu dla instancji Rectangle, co spowoduje nieoczekiwane błędy w kodzie.

To był przykład dla "warunki wstępne nie mogą być wzmocnione w Podtyp" część artykuł wiki. Podsumowując, naruszenie LSP prawdopodobnie spowoduje błędy w kodzie w pewnym momencie.
 2
Author: inf3rno,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-09 04:26:28

Zasada substytucji Liskowa(LSP)

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

Musimy się upewnić, że nowe pochodne klasy rozszerzają się bez zastępowanie funkcjonalności starych klas. W przeciwnym razie nowe klasy może wywoływać niepożądane efekty, gdy są one używane w istniejącym programie Moduły.

Substytucja Liskowa Zasada mówi, że jeśli moduł programu jest używając klasy bazowej, wtedy odniesienie do klasy bazowej może być zastąpiona klasą pochodną bez wpływu na funkcjonalność moduł programu.

Przykład:

Poniżej znajduje się klasyczny przykład, dla którego łamana jest zasada substytucji Liskowa. W przykładzie używane są 2 klasy: prostokąt i kwadrat. Załóżmy, że obiekt Rectangle jest używany gdzieś w aplikacji. Rozszerzamy aplikacji i dodać klasę kwadrat. Klasa kwadratowa 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ąt, ustawiamy szerokość na 5 i wysokość na 10 i otrzymujemy obszar. Dla prostokąta o szerokości 5 i wysokości 10 powierzchnia powinna 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:

Zasada ta jest tylko rozszerzeniem Zasada otwierania i zamykania oznacza, że musimy upewnić się, że nowe pochodne klasy rozszerzają klasy bazowe bez zmiany ich zachowania.

Zobacz: zasada otwierania i zamykania

Niektóre podobne pojęcia dla lepszej struktury: Konwencja nad konfiguracją

 2
Author: Ishu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-21 09:01:48

Zasada podstawienia Likova mówi, że jeśli moduł programu używa klasy bazowej, to odniesienie do klasy bazowej może zostać zastąpione klasą pochodną bez wpływu na funkcjonalność modułu programu.

Typy intencyjne muszą być całkowicie zastępujące typy bazowe.

Example-Co-variant return types in java.

 1
Author: Ishan Aggarwal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-23 19:26:00

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

[..] aby zrozumieć niektóre zasady, ważne jest, aby zdać sobie sprawę, kiedy zostały naruszone. Oto, co teraz zrobię.

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

Rozważ następujące 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 z kontem mówi nam, że konto zostanie wycofane, ale nie zawsze tak jest. Więc, co powinienem zrobić, aby to naprawić? Ja tylko 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ść rozróżniania konkretnych obiektów. Na przykład, biorąc pod uwagę umowę pierwszego konta, może wyglądać jak poniżej:
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ę otwarto-zamkniętą [to jest wymóg wypłaty pieniędzy. Ponieważ nigdy nie wiadomo, co się stanie, jeśli przedmiot naruszający umowę nie będzie miał wystarczająco dużo pieniędzy. Prawdopodobnie po prostu nic nie zwraca, prawdopodobnie wyjątek zostanie wyrzucony. Więc musisz sprawdzić, czy to hasEnoughMoney() -- który nie jest częścią interfejsu. Więc to wymuszone sprawdzenie zależne od klasy betonu jest naruszeniem OCP].

Ten punkt odnosi się również do błędne przekonanie, że często spotykam się z naruszeniem LSP. Jest napisane: "jeśli zachowanie rodzica zmieniło się u dziecka, to narusza LSP."Jednak tak nie jest - tak długo, jak dziecko nie narusza umowy rodzica.

 -1
Author: zapadlo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-12 17:10:59