Czy wzorzec stanu narusza zasadę substytucji Liskowa?

17

To zdjęcie pochodzi z aplikacji Projektowanie i wzorce oparte na domenie: z przykładami w języku C # i .NET

wprowadź opis zdjęcia tutaj

To jest diagram klasowy dla Wzorca Stanu, w którym a SalesOrdermoże mieć różne stany w czasie swojego życia. Tylko niektóre przejścia są dozwolone między różnymi stanami.

Teraz OrderStateklasa jest abstractklasą, a wszystkie jej metody są dziedziczone w jej podklasach. Jeśli weźmiemy podklasę Cancelledza stan końcowy, który nie zezwala na żadne przejścia do innych stanów, będziemy musieli zastosować overridewszystkie jej metody w tej klasie, aby zgłaszać wyjątki.

Czy nie narusza to zasady substytucji Liskowa, ponieważ podklasa nie powinna zmieniać zachowania rodzica? Czy zmiana klasy abstrakcyjnej na interfejs to rozwiązuje?
Jak to może zostać naprawione?

Songo
źródło
Czy zmienić OrderState na konkretną klasę, która domyślnie generuje wyjątki „Nieobsługiwane” we wszystkich swoich metodach?
James
Nie przeczytałem tej książki, ale wydaje mi się dziwne, że OrderState zawiera Cancel (), a potem jest stan Cancelled, czyli inny podtyp.
Euforia
@Euforia, dlaczego to jest dziwne? wywołanie cancel()zmienia stan na anulowany.
Songo,
W jaki sposób? Jak można wywołać Anuluj w OrderState, kiedy zmiana stanu w SalesOrder ma się zmienić. Myślę, że cały model jest zły. Chciałbym zobaczyć pełne wdrożenie.
Euforia

Odpowiedzi:

3

Ta konkretna implementacja, tak. Jeśli uczynisz stany konkretnymi klasami, a nie abstrakcyjnymi implementatorami, to odejdziesz od tego.

Jednak wzorzec stanu, do którego się odwołujesz, jest w rzeczywistości projektem maszyny stanów, ogólnie rzecz biorąc, nie zgadzam się z tym, jak widzę, jak rośnie. Sądzę, że istnieją wystarczające podstawy, aby potępić to jako naruszenie zasady pojedynczej odpowiedzialności, ponieważ te wzorce zarządzania stanem stają się centralnymi repozytoriami wiedzy o obecnym stanie wielu innych części systemu. Ten element zarządzania stanem, który jest scentralizowany, będzie wymagał reguł biznesowych istotnych dla wielu różnych części systemu w celu ich racjonalnej koordynacji.

Wyobraź sobie, że wszystkie części systemu, które dbają o stan, znajdują się w różnych usługach, różnych procesach na różnych maszynach, centralny menedżer statusu, który szczegółowo określa status każdego z tych miejsc, skutecznie wąskie gardła całego tego rozproszonego systemu i myślę, że wąskie gardło jest znakiem naruszenia SRP, a także ogólnie złego projektu.

W przeciwieństwie do tego sugerowałbym, aby uczynić obiekty bardziej inteligentnymi, ponieważ w obiekcie Model we wzorze MVC, w którym model wie, jak sobie radzić, nie potrzebuje zewnętrznego orkiestratora, aby zarządzać swoimi wewnętrznymi działaniami lub uzasadnieniem.

Nawet umieszczając taki wzór stanu wewnątrz obiektu, aby zarządzał tylko sobą, wydaje się, że zrobiłbyś ten obiekt zbyt dużym. Powiedziałbym, że przepływy pracy powinny odbywać się poprzez komponowanie różnych samowystarczalnych obiektów, a nie za pomocą jednego uporządkowanego stanu, który zarządza przepływem innych obiektów lub przepływem inteligencji w sobie.

Ale w tym momencie jest to więcej sztuki niż inżynierii, a zatem zdecydowanie subiektywne jest twoje podejście do niektórych z tych rzeczy, które mówi, że zasady są dobrym przewodnikiem i tak, implementacja, którą wymieniasz, jest naruszeniem LSP, ale może być poprawiona, aby nie była. Zachowaj ostrożność podczas korzystania z SRP podczas korzystania z tego rodzaju wzorów i prawdopodobnie będziesz bezpieczny.

Jimmy Hoffa
źródło
Zasadniczo dużym problemem jest to, że orientacja obiektowa jest dobra do dodawania nowych klas, ale utrudnia dodawanie nowych metod. A w przypadku stanu, jak powiedziałeś, jest mało prawdopodobne, że będziesz musiał bardzo często rozszerzać kod o nowe stany.
hugomg
3
@Jimmy Hoffa Przykro mi, ale co dokładnie rozumiesz przez „jeśli uczynisz stany konkretnymi klasami, a nie abstrakcyjnymi implementatorami, to odejdziesz”. ?
Songo,
@ Jimmy: Próbowałem zaimplementować stan bez wzorca stanu, ponieważ każdy komponent wiedział tylko o swoim stanie. Skończyło się to wprowadzeniem dużych zmian w przepływie znacznie bardziej skomplikowanym do wdrożenia i utrzymania. To powiedziawszy, myślę, że używanie maszyny stanów (najlepiej czyjejś biblioteki, aby zmusić ją do traktowania jej jako czarnej skrzynki) zamiast wzorca projektowania stanu (w związku z tym stany są tylko elementami w wyliczeniu, a nie pełne klasy, a przejścia są po prostu głupie edge) daje wiele korzyści z wzorca stanu, będąc jednocześnie wrogo nastawionym do prób nadużywania go przez programistów.
Brian,
8

podklasa nie powinna zmieniać zachowania rodzica?

Jest to powszechna błędna interpretacja LSP. Podklasa może zmieniać zachowanie elementu nadrzędnego, o ile pozostaje ono zgodne z typem elementu nadrzędnego.

W Wikipedii istnieje dobre, długie wyjaśnienie , sugerujące rzeczy, które naruszą LSP:

... istnieje wiele warunków behawioralnych, które podtyp musi spełniać. Wyszczególniono je w terminologii przypominającej metodologię projektowania według umowy, co prowadzi do pewnych ograniczeń w zakresie interakcji kontraktów z dziedziczeniem:

  • W podtypie nie można wzmocnić warunków wstępnych.
  • Warunki podrzędne nie mogą zostać osłabione w podtypie.
  • Niezmienniki nadtypu muszą być zachowane w podtypie.
  • Ograniczenie historii („reguła historii”). Obiekty są uważane za modyfikowalne tylko za pomocą ich metod (enkapsulacji). Ponieważ podtypy mogą wprowadzać metody, które nie są obecne w nadtypu, wprowadzenie tych metod może pozwolić na zmiany stanu w podtypie, które nie są dopuszczalne w nadtypie. Ograniczenia historyczne tego zabraniają. Był to nowatorski element wprowadzony przez Liskova i Winga. Naruszenie tego ograniczenia można zilustrować, definiując MutablePoint jako podtyp ImmutablePoint. Jest to naruszenie ograniczenia historii, ponieważ w historii punktu niezmiennego stan jest zawsze taki sam po utworzeniu, więc nie może obejmować historii MutablePoint w ogóle. Pola dodane do podtypu można jednak bezpiecznie modyfikować, ponieważ nie można ich zaobserwować za pomocą metod nadtypu.

Osobiście łatwiej mi to zapamiętać: jeśli patrzę na parametr w metodzie typu A, czy ktoś przekazujący podtyp B sprawiłby mi jakieś niespodzianki? Jeśli tak, to nastąpi naruszenie LSP.

Czy zgłoszenie wyjątku jest niespodzianką? Nie całkiem. Jest to coś, co może się zdarzyć w dowolnym momencie, niezależnie od tego, czy wywołuję metodę wysyłki w OrderState, czy przyznano, czy wysłano. Więc muszę to wyjaśnić i tak naprawdę nie jest to naruszenie LSP.

To powiedziawszy , myślę, że istnieją lepsze sposoby radzenia sobie z tą sytuacją. Gdybym pisał to w C #, użyłbym interfejsów i sprawdziłbym implementację interfejsu przed wywołaniem metody. Na przykład jeśli bieżący stan zamówienia nie implementuje IShippable, nie wywołuj na nim metody wysyłki.

Ale wtedy też nie użyłbym wzoru państwa dla tej konkretnej sytuacji. Wzorzec stanu jest znacznie bardziej odpowiedni do stanu aplikacji niż do stanu obiektu domeny takiego jak ten.

Krótko mówiąc, jest to źle wymyślony przykład Wzorca Stanu i niezbyt dobry sposób radzenia sobie ze stanem zamówienia. Ale prawdopodobnie nie narusza LSP. A wzorzec państwowy sam w sobie na pewno nie.

pdr
źródło
Wydaje mi się, że mylisz LSP z zasadą najmniejszego zaskoczenia / zdziwienia. Wierzę, że LSP ma bardziej wyraźne granice tam, gdzie je narusza, chociaż stosujesz bardziej subiektywną POLA, która opiera się na oczekiwaniach, które są subiektywne ; to znaczy, że każda osoba oczekuje czegoś innego niż następny. LSP opiera się na umownych i dobrze określonych gwarancjach udzielonych przez dostawcę, a nie na subiektywnie ocenianych oczekiwaniach odgadywanych przez konsumenta.
Jimmy Hoffa
@JimmmyHoffa: Masz rację i dlatego wyjaśniłem dokładne znaczenie, zanim podałem prostszy sposób zapamiętywania LSP. Jeśli jednak pojawi się pytanie, co oznacza „niespodzianka”, wrócę i sprawdzę dokładne zasady LSP (czy naprawdę możesz je sobie przypomnieć?). Wyjątki zastępujące funkcjonalność (lub odwrotnie) są trudne, ponieważ nie naruszają żadnej konkretnej klauzuli powyżej, co prawdopodobnie jest wskazówką, że powinno się z nią postępować zupełnie inaczej.
pdr
1
Wyjątek to oczekiwany warunek zdefiniowany w umowie, myślę o NetworkSocket, może mieć oczekiwany wyjątek podczas wysyłania, jeśli nie jest otwarty, jeśli twoja implementacja nie zgłasza tego wyjątku dla zamkniętego gniazda naruszasz LSP , jeśli umowa stanowi inaczej, a Twój podtyp generuje wyjątek, naruszasz LSP. Tak mniej więcej klasyfikuję wyjątki w LSP. Co do pamiętania zasad; Mogę się mylić i mogę to powiedzieć, ale moje rozumienie LSP vs. POLA jest zdefiniowane w ostatnim zdaniu mojego komentarza powyżej.
Jimmy Hoffa
1
@JimmyHoffa: Nigdy nie uważałem, że POLA i LSP są nawet zdalnie połączone (myślę o POLA w kategoriach interfejsu użytkownika), ale teraz, jak już zauważyłeś, są w pewnym sensie. Nie jestem jednak pewien, czy są w konflikcie, czy to jedno jest bardziej subiektywne niż drugie. Czy LSP nie jest wczesnym opisem POLA w kategoriach inżynierii oprogramowania? W ten sam sposób, gdy denerwuję się, gdy Ctrl-C nie jest kopiowaniem (POLA), denerwuję się również, gdy ImmutablePoint jest zmienny, ponieważ w rzeczywistości jest to podklasa MutablePoint (LSP).
pdr
1
Uznałbym za CircleWithFixedCenterButMutableRadiusprawdopodobne naruszenie LSP, jeśli umowa konsumencka ImmutablePointokreśla, że ​​jeśli X.equals(Y), konsumenci mogą swobodnie podstawić X na Y i odwrotnie, a zatem muszą powstrzymać się od używania klasy w taki sposób, że takie zastąpienie spowodowałoby kłopoty. Typ może być w stanie legalnie zdefiniować equalstakie, że wszystkie instancje będą porównywane jako odrębne, ale takie zachowanie prawdopodobnie ograniczy jego użyteczność.
supercat
2

(Jest to napisane z punktu widzenia C #, więc nie zaznaczono wyjątków).

Zgodnie z artykułem Wikipedii o LSP jednym z warunków LSP jest:

Metodami podtypu nie należy zgłaszać żadnych nowych wyjątków, z wyjątkiem przypadków, gdy same wyjątki są podtypami wyjątków zgłaszanych przez metody nadtypu.

Jak powinieneś to zrozumieć? Czym dokładnie są „wyjątki zgłaszane przez metody nadtypu”, gdy metody nadtypu są abstrakcyjne? Myślę, że są to wyjątki udokumentowane jako wyjątki, które mogą być zgłaszane za pomocą metod nadtypu.

Oznacza to, że jeśli OrderState.Ship()jest to udokumentowane jako „wyrzuca, InvalidOperationExceptionjeśli ten stan nie jest obsługiwany przez bieżący stan”, myślę, że ten projekt nie łamie LSP. Z drugiej strony, jeśli metody nadtypu nie zostaną w ten sposób udokumentowane, LSP zostanie naruszone.

Ale to nie znaczy, że jest to dobry projekt, nie należy używać wyjątków dla normalnego przepływu sterowania, co wydaje się bardzo bliskie. Ponadto, w prawdziwej aplikacji, prawdopodobnie będziesz chciał wiedzieć, czy daną operację można wykonać, zanim spróbujesz to zrobić, na przykład, aby wyłączyć przycisk „Wyślij” w interfejsie użytkownika.

svick
źródło
Właściwie to właśnie ten punkt skłonił mnie do myślenia. Jeśli podklasy zgłaszają wyjątki, które nie zostały zdefiniowane w obiekcie nadrzędnym, musi to stanowić naruszenie LSP.
Songo,
Myślę, że w języku, w którym wyjątki nie są sprawdzane, ich rzucanie nie jest naruszeniem LSP. To tylko koncepcja samego języka, który w dowolnym miejscu i czasie może zostać zgłoszony. Więc każdy kod klienta powinien być na to gotowy.
Zapadlo,