Próbuję zrozumieć SOLIDNE zasady OOP i doszedłem do wniosku, że LSP i OCP mają pewne podobieństwa (jeśli nie powiedzieć więcej).
zasada otwarta / zamknięta stwierdza: „jednostki oprogramowania (klasy, moduły, funkcje itp.) powinny być otwarte na rozszerzenie, ale zamknięte na modyfikację”.
LSP w prostych słowach stwierdza, że dowolną instancję Foo
można zastąpić dowolną instancją, z Bar
której pochodzi, Foo
a program będzie działał w ten sam sposób.
Nie jestem pro programistą OOP, ale wydaje mi się, że LSP jest możliwe tylko wtedy Bar
, gdy wywodzi się z Foo
niczego w nim nie zmienia, a jedynie go rozszerza. Oznacza to, że w szczególności program LSP jest prawdziwy tylko wtedy, gdy OCP jest prawdziwy, a OCP jest prawdziwy tylko wtedy, gdy LSP jest prawdziwy. Oznacza to, że są równi.
Popraw mnie, jeśli się mylę. Naprawdę chcę zrozumieć te pomysły. Wielkie dzięki za odpowiedź.
Square
zRectangle
nie narusza LSP. (Ale prawdopodobnie nadal jest to zły projekt w niezmiennej sprawie, ponieważ możesz mieć kwadrat,Rectangle
który nie jest taki,Square
który nie pasuje do matematyki)Odpowiedzi:
Rany, istnieją dziwne nieporozumienia na temat tego, co OCP i LSP, a niektóre są spowodowane niedopasowaniem niektórych terminologii i mylącymi przykładami. Obie zasady są tylko „tym samym”, jeśli zastosujesz je w ten sam sposób. Wzory zwykle są zgodne z tymi zasadami w ten czy inny sposób, z kilkoma wyjątkami.
Różnice zostaną wyjaśnione w dalszej części, ale najpierw przyjrzyjmy się samym zasadom:
Zasada otwartego i zamkniętego (OCP)
Według wujka Boba :
Zauważ, że słowo „ rozszerzenie” w tym przypadku niekoniecznie oznacza, że należy podklasować klasę, która potrzebuje nowego zachowania. Zobacz, jak wspomniałem przy pierwszym niedopasowaniu terminologii? Słowo kluczowe
extend
oznacza tylko podklasę w Javie, ale zasady są starsze niż Java.Oryginał pochodzi z Bertrand Meyer w 1988 roku:
Tutaj jest o wiele wyraźniej, że zasada jest stosowana do bytów oprogramowania . Zły przykład to zastąpienie encji programowej, ponieważ modyfikujesz kod całkowicie zamiast dostarczać jakiegoś punktu rozszerzenia. Zachowanie samej jednostki programowej powinno być rozszerzalne, a dobrym przykładem jest implementacja wzorca strategii (ponieważ najłatwiej jest pokazać grupę wzorców GoF IMHO):
W powyższym przykładzie
Context
jest zablokowany dla dalszych modyfikacji. Większość programistów prawdopodobnie chciałaby podklasować klasę, aby ją rozszerzyć, ale tutaj nie robimy tego, ponieważ zakłada ona, że jej zachowanie można zmienić za pomocą wszystkiego, co implementujeIBehavior
interfejs.Tj. Klasa kontekstu jest zamknięta do modyfikacji, ale otwarta do rozszerzenia . W rzeczywistości jest zgodny z inną podstawową zasadą, ponieważ przypisujemy zachowanie kompozycji obiektu zamiast dziedziczenia:
Pozwolę czytelnikowi przeczytać tę zasadę, ponieważ jest to poza zakresem tego pytania. Kontynuując przykład, powiedzmy, że mamy następujące implementacje interfejsu IBehavior:
Za pomocą tego wzorca możemy zmodyfikować zachowanie kontekstu w czasie wykonywania, za pomocą
setBehavior
metody jako punktu rozszerzenia.Dlatego za każdym razem, gdy chcesz rozszerzyć „zamkniętą” klasę kontekstową, zrób to przez podklasowanie jej „otwartej” zależności współpracy. To oczywiście nie jest to samo, co podklasowanie samego kontekstu, ale jest to OCP. LSP również nie wspomina o tym.
Rozszerzanie za pomocą Mixin zamiast dziedziczenia
Istnieją inne sposoby wykonywania OCP inne niż podklasowanie. Jednym ze sposobów jest utrzymanie otwartych klas na rozszerzenie poprzez zastosowanie mixin . Jest to przydatne np. W językach opartych na prototypach, a nie klasach. Chodzi o to, aby zmienić obiekt dynamiczny za pomocą większej liczby metod lub atrybutów, zgodnie z potrzebami, innymi słowy obiekty, które łączą się lub „łączą” z innymi obiektami.
Oto przykładowy mixin, który renderuje prosty szablon HTML dla kotwic:
Chodzi o to, aby dynamicznie rozszerzać obiekty, a zaletą tego jest to, że obiekty mogą dzielić metody, nawet jeśli znajdują się w zupełnie innych domenach. W powyższym przypadku możesz łatwo tworzyć inne rodzaje kotwic HTML, rozszerzając swoją konkretną implementację za pomocą
LinkMixin
.Pod względem OCP „mixiny” są rozszerzeniami. W powyższym przykładzie
YoutubeLink
jest to nasza jednostka oprogramowania, która jest zamknięta dla modyfikacji, ale otwarta na rozszerzenia poprzez użycie mixin. Hierarchia obiektów jest spłaszczona, co uniemożliwia sprawdzenie typów. Jednak nie jest to naprawdę zła rzecz, i wyjaśnię dalej, że sprawdzanie typów jest ogólnie złym pomysłem i łamie pomysł z polimorfizmem.Zauważ, że za pomocą tej metody można wykonać wielokrotne dziedziczenie, ponieważ większość
extend
implementacji może mieszać wiele obiektów:Jedyną rzeczą, o której musisz pamiętać, to nie kolidować nazw, tzn. Mixiny definiują tę samą nazwę niektórych atrybutów lub metod, które zostaną zastąpione. Z mojego skromnego doświadczenia wynika, że nie stanowi to problemu, a jeśli tak się stanie, jest to oznaką wadliwego projektu.
Liskov's Substitution Principle (LSP)
Wujek Bob definiuje to po prostu przez:
Ta zasada jest stara, w rzeczywistości definicja wuja Boba nie rozróżnia zasad, ponieważ sprawia, że LSP jest nadal blisko spokrewniona z OCP przez fakt, że w powyższym przykładzie Strategii zastosowano ten sam nadtyp (
IBehavior
). Spójrzmy więc na oryginalną definicję Barbary Liskov i zobaczmy, czy możemy dowiedzieć się czegoś więcej o tej zasadzie, która wygląda jak twierdzenie matematyczne:Przez chwilę wzruszamy ramionami, zauważ, że w ogóle nie wspomina o klasach. W JavaScript możesz śledzić LSP, nawet jeśli nie jest on wyraźnie oparty na klasach. Jeśli Twój program ma listę co najmniej kilku obiektów JavaScript, które:
... wtedy obiekty są uważane za mające ten sam „typ” i nie ma to tak naprawdę znaczenia dla programu. Jest to zasadniczo polimorfizm . W sensie ogólnym; nie powinieneś znać faktycznego podtypu, jeśli używasz jego interfejsu. OCP nie mówi nic na ten temat. Wskazuje także na błąd projektowy, który popełniają większość początkujących programistów:
Ilekroć odczuwasz potrzebę sprawdzenia podtypu obiektu, najprawdopodobniej robisz to NIEPRAWIDŁOWO.
Okej, więc może nie być źle przez cały czas, ale jeśli masz ochotę sprawdzić jakieś typy za pomocą
instanceof
lub wyliczeń, być może program jest dla ciebie bardziej skomplikowany niż powinien. Lecz nie zawsze tak jest; szybkie i brudne włamania, aby wszystko działało, są w porządku ustępstwem, które można sobie wyobrazić, jeśli rozwiązanie jest wystarczająco małe, a jeśli ćwiczysz bezlitosne refaktoryzowanie , może ulec poprawie, gdy wymagają tego zmiany.Istnieją różne sposoby rozwiązania tego „błędu projektowego”, w zależności od rzeczywistego problemu:
Oba są typowymi „błędami” przy projektowaniu kodu. Istnieje kilka różnych refaktoryzacji, takich jak metoda pull-up lub refaktoryzacja do wzorca takiego jak wzorzec gościa .
W rzeczywistości bardzo podoba mi się wzorzec Visitor, ponieważ może on zająć się dużym spaghetti z instrukcją if, a jego implementacja jest łatwiejsza niż myślisz o istniejącym kodzie. Powiedzmy, że mamy następujący kontekst:
Wyniki instrukcji if mogą zostać przetłumaczone na ich odwiedzających, ponieważ każda z nich zależy od decyzji i kodu do uruchomienia. Możemy wyodrębnić je w następujący sposób:
W tym momencie, jeśli programista nie wiedział o wzorcu gościa, zamiast tego zaimplementował klasę Context, aby sprawdzić, czy jest ona pewnego rodzaju. Ponieważ klasy Visitor mają
canDo
metodę logiczną , implementator może użyć tego wywołania metody, aby ustalić, czy jest to właściwy obiekt do wykonania zadania. Klasa kontekstowa może wykorzystywać wszystkich odwiedzających (i dodawać nowych) w następujący sposób:Oba wzorce są zgodne z OCP i LSP, jednak oba wskazują różne rzeczy na ich temat. Jak więc wygląda kod, jeśli narusza jedną z zasad?
Naruszenie jednej zasady, ale przestrzeganie drugiej
Istnieją sposoby na złamanie jednej z zasad, ale nadal należy przestrzegać drugiej. Poniższe przykłady wydają się wymyślone, nie bez powodu, ale w rzeczywistości zauważyłem, że pojawiają się one w kodzie produkcyjnym (a nawet gorzej):
Obserwuje OCP, ale nie LSP
Powiedzmy, że mamy podany kod:
Ten fragment kodu działa zgodnie z zasadą otwartego i zamkniętego. Jeśli wywołamy
GetPersons
metodę kontekstu , otrzymamy grupę osób z własnymi implementacjami. Oznacza to, że IPerson jest zamknięty dla modyfikacji, ale otwarty dla rozszerzenia. Gdy jednak musimy go użyć, sytuacja zmienia się w mroczny sposób:Musisz wykonać sprawdzanie typów i konwersję typów! Pamiętasz, jak wspomniałem powyżej, że sprawdzanie typu jest złe ? O nie! Ale nie bój się, jak wspomniano powyżej, albo dokonaj refaktoryzacji typu pull-up, albo zaimplementuj wzorzec Visitor. W takim przypadku możemy po prostu zrobić refaktoryzację po dodaniu ogólnej metody:
Korzyścią jest teraz to, że nie musisz już znać dokładnego typu, zgodnie z LSP:
Podąża za LSP, ale nie OCP
Spójrzmy na kod, który następuje po LSP, ale nie OCP, jest trochę wymyślony, ale proszę o cierpliwość w tym, to bardzo subtelny błąd:
Kod wykonuje LSP, ponieważ kontekst może korzystać z LiskovBase bez znajomości faktycznego typu. Można by pomyśleć, że ten kod jest zgodny z OCP, ale spójrz dokładnie, czy klasa jest naprawdę zamknięta ? Co jeśli
doStuff
metoda nie tylko wydrukowała wiersz?Odpowiedź, jeśli wynika z OCP, brzmi po prostu: NIE , nie dlatego, że w tym projekcie obiektu musimy całkowicie zastąpić kod czymś innym. Otwiera to puszkę robaków typu „wklej i wklej”, ponieważ musisz skopiować kod z klasy podstawowej, aby wszystko działało.
doStuff
Metoda na pewno jest otwarta do rozszerzenia, ale nie było całkowicie zamknięte dla modyfikacji.Możemy zastosować do tego wzorzec metody Szablon . Wzorzec metody szablonów jest tak powszechny w frameworkach, że mógłbyś go używać nie wiedząc o tym (np. Komponenty Java Swing, formularze i komponenty C # itp.). Oto jeden ze sposobów zamknięcia
doStuff
metody modyfikacji i upewnienia się, że pozostanie ona zamknięta poprzez oznaczenie jejfinal
słowem kluczowym java . To słowo kluczowe uniemożliwia dalszą podklasę klasy (w języku C # można użyćsealed
tego samego).Ten przykład jest zgodny z OCP i wydaje się głupiutki, ale wyobraź sobie, że to przeskalowane z większą ilością kodu do obsługi. Wciąż widzę kod wdrożony w środowisku produkcyjnym, w którym podklasy całkowicie przesłaniają wszystko, a przesłonięty kod jest w większości wycinany i wklejany między implementacjami. Działa, ale podobnie jak w przypadku całego powielania kodu, jest to również konfiguracja do koszmarów związanych z konserwacją.
Wniosek
Mam nadzieję, że to wszystko wyjaśnia niektóre pytania dotyczące OCP i LSP oraz różnic / podobieństw między nimi. Łatwo je odrzucić jako takie same, ale powyższe przykłady powinny pokazać, że nie są.
Zauważ, że zbierając z powyższego przykładowego kodu:
OCP polega na zablokowaniu działającego kodu, ale nadal utrzymuje go w jakiś sposób za pomocą pewnego rodzaju punktów rozszerzeń.
Ma to na celu uniknięcie powielania kodu poprzez enkapsulację kodu, który zmienia się jak w przykładzie wzorca metody szablonu. Pozwala to również szybko zawieść, ponieważ przełamywanie zmian jest bolesne (tj. Zmiana jednego miejsca, złamanie go wszędzie indziej). Ze względu na utrzymanie koncepcji kapsułkowanie zmian jest dobrą rzeczą, ponieważ zmiany zawsze się zdarzają.
LSP polega na umożliwieniu użytkownikowi obsługi różnych obiektów, które implementują nadtyp, bez sprawdzania, jaki jest rzeczywisty typ. Na tym właśnie polega polimorfizm .
Ta zasada stanowi alternatywę dla sprawdzania i konwersji typów, które mogą wymknąć się spod kontroli wraz ze wzrostem liczby typów, i można to osiągnąć poprzez refaktoryzację typu pull-up lub zastosowanie wzorców, takich jak Visitor.
źródło
Jest to coś, co powoduje wiele zamieszania. Wolę rozważać te zasady nieco filozoficznie, ponieważ istnieje wiele różnych przykładów, a czasem konkretne przykłady nie oddają w istocie całej ich istoty.
Co OCP próbuje naprawić
Powiedzmy, że musimy dodać funkcjonalność do danego programu. Najłatwiejszym sposobem, aby to zrobić, szczególnie dla osób, które zostały przeszkolone do myślenia proceduralnego, jest dodanie klauzuli if wszędzie tam, gdzie jest to potrzebne, lub czegoś podobnego.
Problemy z tym są
Możesz to zrobić, dodając dodatkowe pole do wszystkich książek o nazwie „is_on_sale”, a następnie możesz sprawdzić to pole, drukując cenę dowolnej książki, lub alternatywnie możesz utworzyć wystąpienie książek o wyprzedażach z bazy danych przy użyciu innego typu, który drukuje „(W SPRZEDAŻY)” w przedziale cenowym (nie jest to idealny projekt, ale zapewnia sens domu).
Problem z pierwszym rozwiązaniem proceduralnym jest dodatkowym polem dla każdej książki i w wielu przypadkach dodatkową zbędną złożonością. Drugie rozwiązanie wymusza logikę tylko tam, gdzie jest rzeczywiście wymagana.
Teraz rozważ fakt, że może istnieć wiele przypadków, w których wymagane są różne dane i logika, a zobaczysz, dlaczego warto pamiętać o OCP podczas projektowania klas lub reagowania na zmiany wymagań, jest dobrym pomysłem.
Do tej pory powinieneś wpaść na główny pomysł: postaraj się znaleźć w sytuacji, w której nowy kod może zostać zaimplementowany jako rozszerzenia polimorficzne, a nie modyfikacje proceduralne.
Ale nigdy nie bój się analizować kontekstu i sprawdź, czy wady przeważają nad korzyściami, ponieważ nawet taka zasada, jak OCP, może sprawić, że 20-klasowy bałagan z programu 20-liniowego, jeśli nie zostanie potraktowany ostrożnie .
Co LSP próbuje naprawić
Wszyscy lubimy używać kodu ponownie. Następująca choroba polega na tym, że wiele programów nie rozumie tego całkowicie, do tego stopnia, że ślepo fakturują wspólne wiersze kodu tylko po to, aby stworzyć nieczytelne złożoności i nadmiarowe ścisłe sprzężenie między modułami, które oprócz kilku wierszy kodu, nie mają ze sobą nic wspólnego, jeśli chodzi o koncepcyjne prace do wykonania.
Największym tego przykładem jest ponowne użycie interfejsu . Prawdopodobnie sam to widziałeś; klasa implementuje interfejs nie dlatego, że jest jego logiczną implementacją (lub rozszerzeniem w przypadku konkretnych klas bazowych), ale dlatego, że metody, które deklaruje w tym momencie, mają odpowiednie podpisy, jeśli chodzi o to.
Ale wtedy napotykasz problem. Jeśli klasy implementują interfejsy tylko biorąc pod uwagę podpisy metod, które deklarują, wówczas jesteś w stanie przekazać instancje klas z jednej funkcjonalności pojęciowej do miejsc, które wymagają zupełnie innej funkcjonalności, które tylko zależą od podobnych podpisów.
To nie jest takie okropne, ale powoduje wiele zamieszania, a my mamy technologię, która pozwala nam uniknąć takich błędów. Musimy traktować interfejsy jako API + protokół . Interfejs API jest widoczny w deklaracjach, a protokół jest widoczny w istniejących zastosowaniach interfejsu. Jeśli mamy 2 protokoły koncepcyjne, które korzystają z tego samego interfejsu API, powinny być reprezentowane jako 2 różne interfejsy. W przeciwnym razie wpadniemy w DRY dogmatyzm i, jak na ironię, trudniej jest utrzymać kod.
Teraz powinieneś być w stanie doskonale zrozumieć definicję. LSP mówi: Nie dziedzicz po klasie podstawowej i nie implementuj funkcji w tych podklasach, z którymi inne miejsca, które zależą od klasy podstawowej, nie będą się dogadać.
źródło
Z mojego zrozumienia:
OCP mówi: „Jeśli dodasz nową funkcjonalność, utwórz nową klasę, rozszerzając istniejącą, zamiast ją zmieniać”.
LSP mówi: „Jeśli tworzysz nową klasę rozszerzającą istniejącą klasę, upewnij się, że jest ona całkowicie wymienna z jej bazą”.
Myślę więc, że się uzupełniają, ale nie są równe.
źródło
Chociaż prawdą jest, że zarówno OCP, jak i LSP mają do czynienia z modyfikacją, rodzaj modyfikacji, o której mówi OCP, nie jest tą, o której mówi LSP.
Modyfikacja w odniesieniu do OCP to fizyczne działanie programisty piszącego kod w istniejącej klasie.
LSP zajmuje się modyfikacją zachowania wprowadzoną przez klasę pochodną w porównaniu z klasą podstawową oraz zmianą czasu wykonywania programu, która może być spowodowana użyciem podklasy zamiast nadklasy.
Więc chociaż mogą wyglądać podobnie z odległości OCP! = LSP. W rzeczywistości myślę, że mogą to być jedyne 2 SOLIDNE zasady, których nie można zrozumieć w kategoriach siebie.
źródło
To jest źle. LSP stwierdza, że klasa Bar nie powinna wprowadzać zachowania, którego nie należy oczekiwać, gdy kod korzysta z Foo, gdy Bar pochodzi z Foo. Nie ma to nic wspólnego z utratą funkcjonalności. Możesz usunąć funkcjonalność, ale tylko wtedy, gdy kod korzystający z Foo nie zależy od tej funkcjonalności.
Ale ostatecznie jest to zwykle trudne do osiągnięcia, ponieważ przez większość czasu kod używający Foo zależy od całego jego zachowania. Więc usunięcie go narusza LSP. Ale uproszczenie w ten sposób jest tylko częścią LSP.
źródło
O obiektach, które mogą naruszać
Aby zrozumieć różnicę, powinieneś zrozumieć tematy obu zasad. To nie jest jakaś abstrakcyjna część kodu lub sytuacja, która może naruszać jakąś zasadę. Zawsze jest jakiś określony komponent - funkcja, klasa lub moduł - który może naruszać OCP lub LSP.
Kto może naruszać LSP
Można sprawdzić, czy LSP jest zepsuty tylko wtedy, gdy istnieje interfejs z jakąś umową i implementacją tego interfejsu. Jeśli implementacja nie jest zgodna z interfejsem lub, ogólnie rzecz biorąc, z umową, LSP jest zepsuty.
Najprostszy przykład:
Umowa wyraźnie stanowi, że
addObject
należy dołączyć swój argument do kontenera. ICustomContainer
wyraźnie łamie ten kontrakt. ZatemCustomContainer.addObject
funkcja narusza LSP. ZatemCustomContainer
klasa narusza LSP. Najważniejszą konsekwencją jest to, żeCustomContainer
nie można tego przekazaćfillWithRandomNumbers()
.Container
nie można go zastąpićCustomContainer
.Pamiętaj o bardzo ważnym punkcie. To nie cały kod łamie LSP, to konkretnie
CustomContainer.addObject
i ogólnieCustomContainer
łamie LSP. Kiedy stwierdzisz, że LSP jest naruszony, zawsze powinieneś podać dwie rzeczy:Otóż to. Tylko umowa i jej realizacja. Spuszczony kod nie mówi nic o naruszeniu LSP.
Kto może naruszać OCP
Można sprawdzić, czy OCP jest naruszony tylko wtedy, gdy istnieje ograniczony zestaw danych i składnik, który obsługuje wartości z tego zestawu danych. Jeśli limity zestawu danych mogą się zmieniać w czasie, co wymaga zmiany kodu źródłowego komponentu, wówczas komponent narusza OCP.
Brzmi skomplikowanie. Spróbujmy prostego przykładu:
Zestaw danych to zestaw obsługiwanych platform.
PlatformDescriber
jest komponentem, który obsługuje wartości z tego zestawu danych. Dodanie nowej platformy wymaga zaktualizowania kodu źródłowegoPlatformDescriber
. ZatemPlatformDescriber
klasa narusza OCP.Inny przykład:
„Zestaw danych” to zestaw kanałów, do których należy dodać wpis do dziennika.
Logger
to komponent odpowiedzialny za dodawanie wpisów do wszystkich kanałów. Dodanie obsługi innego sposobu rejestrowania wymaga zaktualizowania kodu źródłowegoLogger
. ZatemLogger
klasa narusza OCP.Zauważ, że w obu przykładach zestaw danych nie jest czymś semantycznie ustalonym. Z czasem może się zmieniać. Może pojawić się nowa platforma. Może pojawić się nowy kanał logowania. Jeśli twój komponent powinien zostać zaktualizowany, kiedy to się stanie, narusza OCP.
Przekraczanie granic
Teraz trudna część. Porównaj powyższe przykłady z następującymi:
Możesz myśleć, że
translateToRussian
narusza OCP. Ale tak naprawdę nie jest.GregorianWeekDay
ma określony limit dokładnie 7 dni tygodnia z dokładnymi nazwami. Ważne jest to, że ograniczenia te semantycznie nie mogą się zmieniać w czasie. W gregoriańskim tygodniu zawsze będzie 7 dni. Zawsze będzie poniedziałek, wtorek itp. Ten zestaw danych jest semantycznie ustalony. Nie jest możliwe, abytranslateToRussian
kod źródłowy wymagał modyfikacji. W ten sposób OCP nie jest naruszane.Teraz powinno być jasne, że wyczerpujące
switch
stwierdzenie nie zawsze oznacza złamanie OCP.Różnica
Teraz poczuj różnicę:
Warunki te są całkowicie ortogonalne.
Przykłady
W @ odpowiedź Spoike za Naruszenie jedną zasadę, ale po drugiej strony jest całkowicie błędne.
W pierwszym przykładzie
for
część-Loop wyraźnie narusza OCP, ponieważ nie można go rozszerzać bez modyfikacji. Ale nic nie wskazuje na naruszenie LSP. I nawet nie jest jasne, czyContext
umowa zezwala na zwrot przez PayPersons cokolwiek opróczBoss
lubPeon
. Nawet przy założeniu kontraktu, który pozwalaIPerson
na zwrócenie dowolnej podklasy, nie ma klasy, która nadpisuje ten warunek końcowy i narusza go. Co więcej, jeśli getPersons zwróci instancję jakiejś trzeciej klasy,for
-loop wykona swoją pracę bez żadnych awarii. Ale ten fakt nie ma nic wspólnego z LSP.Kolejny. W drugim przykładzie ani LSP, ani OCP nie są naruszone. Ponownie,
Context
część po prostu nie ma nic wspólnego z LSP - brak zdefiniowanej umowy, brak podklasy, brak nadpisywania łamania. To nie jest ten,Context
kto powinien być posłuszny LSP, nieLiskovSub
powinien zerwać kontraktu jego bazy. Jeśli chodzi o OCP, czy klasa jest naprawdę zamknięta? - Tak to jest. Nie jest wymagana modyfikacja, aby go rozszerzyć. Oczywiście nazwa punktu rozszerzenia mówi: rób co chcesz, bez ograniczeń . Ten przykład nie jest zbyt przydatny w prawdziwym życiu, ale wyraźnie nie narusza OCP.Spróbujmy podać kilka poprawnych przykładów z prawdziwym naruszeniem OCP lub LSP.
Postępuj zgodnie z OCP, ale nie LSP
Tutaj
HumanReadablePlatformSerializer
nie wymaga żadnych modyfikacji po dodaniu nowej platformy. Tak więc następuje OCP.Ale umowa wymaga,
toJson
aby zwracał poprawnie sformatowany JSON. Klasa tego nie robi. Z tego powodu nie można go przekazać komponentowi, który używaPlatformSerializer
do sformatowania treści żądania sieciowego. W ten sposóbHumanReadablePlatformSerializer
narusza LSP.Postępuj zgodnie z LSP, ale nie OCP
Niektóre modyfikacje poprzedniego przykładu:
Serializator zwraca poprawnie sformatowany ciąg JSON. Więc nie ma tu naruszenia LSP.
Istnieje jednak wymóg, aby jeśli platforma była w większości wykorzystywana, wówczas w JSON powinno znajdować się odpowiednie wskazanie. W tym przykładzie
HumanReadablePlatformSerializer.isMostPopular
funkcja OCP została naruszona przez funkcję, ponieważ kiedyś iOS stanie się najpopularniejszą platformą. Formalnie oznacza to, że zestaw najczęściej używanych platform jest na razie zdefiniowany jako „Android” iisMostPopular
nieodpowiednio obsługuje ten zestaw danych. Zestaw danych nie jest semantycznie ustalony i z czasem może się swobodnie zmieniać.HumanReadablePlatformSerializer
kod źródłowy wymaga aktualizacji w przypadku zmiany.W tym przykładzie możesz również zauważyć naruszenie zasady pojedynczej odpowiedzialności. Uczyniłem to celowo, aby móc zademonstrować obie zasady na tym samym podmiocie. Aby naprawić SRP, możesz wyodrębnić
isMostPopular
funkcję do niektórych zewnętrznychHelper
i dodać parametr doPlatformSerializer.toJson
. Ale to inna historia.źródło
LSP i OCP nie są takie same.
LSP mówi o poprawności programu , gdyż stoi . Jeśli wystąpienie podtypu złamałoby poprawność programu, gdy zostanie podstawione w kodzie typów przodków, oznacza to naruszenie LSP. Być może będziesz musiał wykonać próbny test, aby to pokazać, ale nie będziesz musiał zmieniać podstawowej bazy kodu. Sprawdzasz poprawność samego programu, aby sprawdzić, czy spełnia on LSP.
OCP mówi o poprawności zmian w kodzie programu, delcie z jednej wersji źródłowej do drugiej. Zachowanie nie powinno być modyfikowane. Należy go tylko rozszerzyć. Klasycznym przykładem jest dodawanie pól. Wszystkie istniejące pola nadal działają jak poprzednio. Nowe pole tylko dodaje funkcjonalność. Usunięcie pola jest jednak zwykle naruszeniem OCP. Tutaj sprawdzasz poprawność wersji programu, aby sprawdzić, czy spełnia on OCP.
To jest kluczowa różnica między LSP a OCP. Pierwszy sprawdza poprawność tylko podstawy kodu w obecnej formie , drugi sprawdza poprawność tylko delty podstawy kodu z jednej wersji do następnej . Jako takie nie mogą być tym samym, są zdefiniowane jako sprawdzanie różnych rzeczy.
Dam ci bardziej formalny dowód: powiedzenie „LSP implikuje OCP” oznaczałoby deltę (ponieważ OCP wymaga jednego innego niż w trywialnym przypadku), ale LSP nie wymaga takiego. To jest oczywiście nieprawda. I odwrotnie, możemy obalić „OCP implikuje LSP”, po prostu mówiąc, że OCP jest instrukcją o delcie, a zatem nie mówi nic o instrukcji nad programem w miejscu. Wynika to z faktu, że możesz utworzyć DOWOLNĄ deltę zaczynając od DOWOLNEGO programu. Są całkowicie niezależni.
źródło
Spojrzałbym na to z punktu widzenia klienta. jeśli klient korzysta z funkcji interfejsu i wewnętrznie ta funkcja została zaimplementowana przez klasę A. Załóżmy, że istnieje klasa B, która rozszerza klasę A, to jutro jeśli usunę klasę A z tego interfejsu i ustawię klasę B, to klasa B powinna zapewniają również te same funkcje klientowi. Standardowym przykładem jest klasa Kaczki, która pływa, a jeśli ToyDuck rozszerzy Kaczkę, to powinna również pływać i nie narzeka, że nie umie pływać, w przeciwnym razie ToyDuck nie powinien mieć rozszerzonej klasy Kaczki.
źródło