Dobrze znaną wadą tradycyjnych hierarchii klas jest to, że są one złe, jeśli chodzi o modelowanie świata rzeczywistego. Na przykład próba przedstawienia gatunku zwierząt za pomocą klas. W rzeczywistości robi to kilka problemów, ale jednym z nich, którego nigdy nie widziałem, jest rozwiązanie, gdy podklasa „traci” zachowanie lub właściwość zdefiniowaną w superklasie, na przykład pingwina niezdolnego do latania (tam są prawdopodobnie lepszymi przykładami, ale to pierwszy, który przychodzi mi do głowy).
Z jednej strony nie chcesz definiować, dla każdej właściwości i zachowania, jakiejś flagi, która określa, czy jest ona w ogóle obecna, i sprawdzaj ją za każdym razem przed uzyskaniem dostępu do tego zachowania lub właściwości. Chciałbyś tylko powiedzieć, że ptaki mogą latać, prosto i wyraźnie, w klasie Bird. Ale wtedy byłoby miło, gdyby można było później zdefiniować „wyjątki”, bez konieczności wszędzie używania okropnych hacków. Zdarza się to często, gdy system jest produktywny przez pewien czas. Nagle znajdujesz „wyjątek”, który w ogóle nie pasuje do oryginalnego projektu i nie chcesz zmieniać dużej części kodu, aby go dostosować.
Czy istnieją jakieś wzorce językowe lub projektowe, które mogą w czysty sposób poradzić sobie z tym problemem, nie wymagając poważnych zmian w „superklasie” i całym kodzie, który z niej korzysta? Nawet jeśli rozwiązanie obsługuje tylko konkretny przypadek, kilka rozwiązań może razem stanowić kompletną strategię.
Po dłuższym zastanowieniu zdaję sobie sprawę, że zapomniałem o zasadzie substytucji Liskowa. Dlatego nie możesz tego zrobić. Zakładając, że zdefiniujesz „cechy / interfejsy” dla wszystkich głównych „grup funkcji”, możesz swobodnie wdrażać cechy w różnych gałęziach hierarchii, tak jak cecha Latanie może być zaimplementowana przez Ptaki, a także specjalne rodzaje wiewiórek i ryb.
Więc moje pytanie może brzmieć: „Jak mogę cofnąć wdrożenie cechy?” Jeśli twoją nadklasą jest Java Serializable, musisz też nią być, nawet jeśli nie ma możliwości serializacji swojego stanu, na przykład jeśli zawierasz „Socket”.
Jednym ze sposobów na to jest zawsze zdefiniowanie wszystkich swoich cech od początku w parach: Latanie i Niepowodzenie (co wyrzuciłoby UnsupportedOperationException, jeśli nie jest sprawdzone). Nie-cecha nie definiuje żadnego nowego interfejsu i można ją po prostu sprawdzić. Brzmi jak „tanie” rozwiązanie, zwłaszcza jeśli jest stosowane od samego początku.
źródło
function save_yourself_from_crashing_airplane(Bird b) { f.fly() }
byłoby to znacznie bardziej skomplikowane. (jak powiedział Peter Török, narusza LSP)" it would be nice if one could define "exceptions" afterward, without having to use some horrible hacks everywhere"
czy uważasz, że metoda fabryczna kontrolująca zachowanie jest nieuczciwa?NotSupportedException
zPenguin.fly()
.class Penguin < Bird; undef fly; end;
. Czy powinieneś to inne pytanie.Odpowiedzi:
Jak wspomnieli inni, będziesz musiał przeciwstawić się LSP.
Można jednak argumentować, że podklasa jest jedynie arbitralnym rozszerzeniem superklasy. Jest to nowy przedmiot sam w sobie, a jedyną relacją do superklasy jest to, że wykorzystano fundament.
Może to mieć logiczny sens, zamiast mówić, że pingwin jest ptakiem. Twoje powiedzenie Pingwin dziedziczy pewien podzbiór zachowań od Ptaka.
Zasadniczo języki dynamiczne pozwalają to łatwo wyrazić, poniżej pokazano przykład użycia JavaScript:
W tym konkretnym przypadku
Penguin
aktywnie śledziBird.fly
dziedziczoną przez siebie metodę, piszącfly
właściwość z wartościąundefined
do obiektu.Teraz możesz powiedzieć, że
Penguin
nie można tego już traktować jako normalnegoBird
. Ale jak wspomniano, w prawdziwym świecie po prostu nie może. Ponieważ modelujemyBird
jako latająca istota.Alternatywą jest nie przyjąć szerokiego założenia, że Ptak może latać. Rozsądne byłoby posiadanie
Bird
abstrakcji, która pozwoli wszystkim ptakom odziedziczyć po niej bez żadnych awarii. Oznacza to jedynie przyjmowanie założeń, które mogą utrzymać wszystkie podklasy.Ogólnie rzecz biorąc, pomysł na Mixin jest tutaj przyjemny. Mają bardzo cienką klasę podstawową i mieszają z nią wszystkie inne zachowania.
Przykład:
Jeśli jesteś ciekawy, mam wdrożenie
Object.make
Dodanie:
Nie „usuwasz” cechy. Po prostu naprawiasz swoją hierarchię dziedziczenia. Albo możesz wypełnić umowę o super klasy, albo nie powinieneś udawać, że jesteś tego typu.
To tutaj świeci kompozycja obiektu.
Nawiasem mówiąc, Serializable nie oznacza, że wszystko powinno być zserializowane, oznacza tylko, że „stan, na którym ci zależy” powinno być zserializowane.
Nie powinieneś używać cechy „NotX”. To po prostu straszne wzdęcie kodu. Jeśli funkcja oczekuje obiektu latającego, powinna rozbić się i spalić, gdy dasz mu mamuta.
źródło
AFAIK wszystkie języki oparte na spadku są oparte na zasadzie podstawienia Liskowa . Usunięcie / wyłączenie właściwości klasy podstawowej w podklasie wyraźnie naruszyłoby LSP, więc nie sądzę, aby taka możliwość była nigdzie zaimplementowana. Rzeczywisty świat jest rzeczywiście brudny i nie można go dokładnie modelować za pomocą matematycznych abstrakcji.
Niektóre języki zapewniają cechy lub miksy, właśnie w celu bardziej elastycznego rozwiązywania takich problemów.
źródło
Class
jest podklasą,Module
chociażClass
IS-NOT-AModule
. Ale nadal ma sens bycie podklasą, ponieważ wykorzystuje wiele kodu. OTOH,StringIO
IS-AIO
, ale ta dwójka nie ma żadnego związku dziedziczenia (poza oczywistym obojem dziedziczeniaObject
, oczywiście), ponieważ nie dzielą żadnego kodu. Klasy służą do udostępniania kodu, typy opisują protokoły.IO
iStringIO
mają ten sam protokół, a zatem ten sam typ, ale ich klasy nie są ze sobą powiązane.Fly()
znajduje się w pierwszym przykładzie w: Head First Design Patterns for The Strategy Pattern , i jest to dobra sytuacja, dlaczego powinieneś „Preferować kompozycję zamiast dziedziczenia”. .Możesz mieszać kompozycję i dziedziczenie, mając nadtypy
FlyingBird
,FlightlessBird
które mają fabrycznie wprowadzone prawidłowe zachowanie, że odpowiednie podtypy np. DostająPenguin : FlightlessBird
się automatycznie, a wszystko inne naprawdę specyficzne jest obsługiwane przez Fabrykę.źródło
Czy prawdziwy problem, jaki zakładasz,
Bird
nie maFly
metody? Dlaczego nie:Teraz oczywistym problemem jest wielokrotne dziedziczenie (
Duck
), więc tak naprawdę potrzebujesz interfejsów:źródło
Po pierwsze, TAK, każdy język, który umożliwia łatwą dynamiczną modyfikację obiektu, pozwoliłby ci to zrobić. Na przykład w Ruby możesz łatwo usunąć metodę.
Ale jak powiedział Péter Török, naruszyłoby to LSP .
W tej części zapomnę o LSP i założę, że:
Powiedziałeś :
Wygląda na to, że chcesz, aby Python „ prosił o wybaczenie, a nie o pozwolenie ”
Po prostu spraw, aby Twój pingwin zgłosił wyjątek lub odziedziczył klasę NonFlyingBird, która zgłasza wyjątek (pseudo kod):
Nawiasem mówiąc, cokolwiek wybierzesz: zgłoszenie wyjątku lub usunięcie metody, w końcu następujący kod (zakładając, że Twój język obsługuje usuwanie metod):
zgłosi wyjątek czasu wykonywania.
źródło
Jak ktoś zauważył powyżej w komentarzach, pingwiny są ptakami, pingwiny nie latają, ergo nie wszystkie ptaki potrafią latać.
Dlatego funkcja Bird.fly () nie powinna istnieć ani nie powinna działać. Wolę ten pierwszy.
Posiadanie FlyingBird rozszerza Birda, metoda .fly () byłaby oczywiście poprawna.
źródło
Prawdziwy problem z przykładem fly () polega na tym, że dane wejściowe i wyjściowe operacji nie są poprawnie zdefiniowane. Co jest wymagane, aby ptak latał? A co się stanie po udanym lataniu? Typy parametrów i typy zwracane dla funkcji fly () muszą mieć tę informację. W przeciwnym razie twój projekt zależy od losowych efektów ubocznych i wszystko może się zdarzyć. Cokolwiek częścią jest to, co powoduje, że cały problem, interfejs nie jest poprawnie zdefiniowane i wszystkie rodzaje realizacji jest dozwolone.
Zamiast tego:
Powinieneś mieć coś takiego:
Teraz wyraźnie określa granice funkcjonalności - twoje zachowanie podczas lotu ma tylko jeden pływak do ustalenia - odległość od podłoża, gdy zostanie podana pozycja. Teraz cały problem sam się rozwiązuje. Ptak, który nie może latać, po prostu zwraca 0,0 z tej funkcji, nigdy nie opuszcza ziemi. Jest to poprawne zachowanie, a kiedy już zdecydujemy się na jedną zmiennoprzecinkową, wiesz, że w pełni zaimplementowałeś interfejs.
Prawdziwe zachowanie może być trudne do zakodowania dla typów, ale jest to jedyny sposób na prawidłowe określenie interfejsów.
Edycja: Chcę wyjaśnić jeden aspekt. Ta wersja float-> float funkcji fly () jest ważna również dlatego, że definiuje ścieżkę. Ta wersja oznacza, że jeden ptak nie może magicznie powielić się podczas lotu. Dlatego parametrem jest single float - jest to pozycja na ścieżce, którą podąża ptak. Jeśli chcesz bardziej złożonych ścieżek, to Point2d posinpath (float x); który używa tego samego x co funkcja fly ().
źródło
Technicznie możesz to zrobić w dowolnym języku dynamicznym / kaczym (JavaScript, Ruby, Lua itp.), Ale prawie zawsze jest to naprawdę zły pomysł. Usuwanie metod z klasy jest koszmarem konserwacyjnym, podobnym do używania zmiennych globalnych (tzn. Nie można powiedzieć w jednym module, że stan globalny nie został zmodyfikowany w innym miejscu).
Dobre wzorce dla opisanego problemu to Dekorator lub Strategia, projektujący architekturę komponentu. Zasadniczo, zamiast usuwać niepotrzebne zachowania z podklas, budujesz obiekty, dodając potrzebne zachowania. Aby zbudować większość ptaków, dodaj latający komponent, ale nie dodawaj go do pingwinów.
źródło
Peter wspomniał o zasadzie substytucji Liskowa, ale uważam, że należy to wyjaśnić.
Zatem jeśli Ptak (obiekt x typu T) może latać (q (x)), to Pingwin (obiekt y typu S) może latać (q (y)), z definicji. Ale oczywiście tak nie jest. Istnieją również inne stworzenia, które mogą latać, ale nie są typu Ptak.
Sposób radzenia sobie z tym zależy od języka. Jeśli język obsługuje wielokrotne dziedziczenie, powinieneś użyć klasy abstrakcyjnej dla stworzeń, które potrafią latać; jeśli język woli interfejsy, to jest to rozwiązanie (a implementacja fly powinna być raczej enkapsulowana niż dziedziczona); lub, jeśli język obsługuje Duck Typing (bez zamierzonej gry słów), możesz po prostu zaimplementować metodę fly na tych klasach, które mogą, i wywołać ją, jeśli ona istnieje.
Ale każda właściwość nadklasy powinna mieć zastosowanie do wszystkich jej podklas.
[W odpowiedzi na edycję]
Zastosowanie „cechy” CanFly do Birda nie jest lepsze. Nadal sugeruje zawołanie kodu, że wszystkie ptaki mogą latać.
Cechą w zdefiniowanych przez ciebie terminach jest dokładnie to, co Liskov miał na myśli, mówiąc „własność”.
źródło
Zacznę od wspomnienia (jak wszyscy inni) zasady substytucji Liskowa, która wyjaśnia, dlaczego nie należy tego robić. Jednak kwestią tego, co powinieneś zrobić, jest projekt. W niektórych przypadkach Pingwin nie może latać. Być może możesz poprosić Pingwina o rzucenie InsufficientWingsException, gdy zostaniesz poproszony o latanie, pod warunkiem, że w dokumentacji Bird :: fly () jest jasne, że może to wyrzucić dla ptaków, które nie mogą latać. Zrób test, aby sprawdzić, czy naprawdę potrafi latać, ale to powoduje rozdęcie interfejsu.
Alternatywą jest restrukturyzacja twoich klas. Stwórzmy klasę „FlyingCreature” (lub lepiej interfejs, jeśli masz do czynienia z językiem, który na to pozwala). „Bird” nie dziedziczy od FlyingCreature, ale możesz utworzyć „FlyingBird”, który to robi. Lark, Vulture i Eagle dziedziczą po FlyingBird. Pingwin nie. Po prostu dziedziczy po Birdie.
Jest to nieco bardziej skomplikowane niż naiwna struktura, ale ma tę zaletę, że jest dokładne. Zauważysz, że istnieją wszystkie oczekiwane klasy (Ptak), a użytkownik może zwykle zignorować te „wymyślone” (FlyingCreature), jeśli nie jest ważne, czy twoje stworzenie może latać, czy nie.
źródło
Typowym sposobem radzenia sobie z taką sytuacją jest rzucenie czegoś w rodzaju
UnsupportedOperationException
(Java) resp.NotImplementedException
(DO#).źródło
Wiele dobrych odpowiedzi z wieloma komentarzami, ale nie wszyscy się z tym zgadzają, a ja mogę wybrać tylko jedną, dlatego streszczę tutaj wszystkie opinie, z którymi się zgadzam.
0) Nie zakładaj „pisania statycznego” (zrobiłem to, gdy o to poprosiłem, ponieważ robię Java prawie wyłącznie). Zasadniczo problem jest bardzo zależny od rodzaju używanego języka.
1) Należy oddzielić hierarchię typów od hierarchii ponownego użycia kodu w projekcie i głowie, nawet jeśli w większości się pokrywają. Zasadniczo używaj klas do ponownego użycia i interfejsów dla typów.
2) Powodem, dla którego normalnie Bird IS-A Fly jest taki, że większość ptaków może latać, więc jest to praktyczne z punktu widzenia ponownego wykorzystania kodu, ale stwierdzenie, że Bird IS-A Fly jest w rzeczywistości błędny, ponieważ istnieje co najmniej jeden wyjątek (Pingwin).
3) W językach statycznych i dynamicznych możesz po prostu rzucić wyjątek. Ale należy tego używać tylko wtedy, gdy jest to wyraźnie zadeklarowane w „umowie” klasy / interfejsu deklarującej funkcjonalność, w przeciwnym razie jest to „naruszenie umowy”. Oznacza to również, że teraz musisz być przygotowany na wychwycenie wyjątku wszędzie, więc piszesz więcej kodu na stronie wywoływania, a to brzydki kod.
4) W niektórych dynamicznych językach faktycznie można „usunąć / ukryć” funkcjonalność superklasy. Jeśli sprawdzanie obecności funkcji jest sposobem sprawdzania „IS-A” w tym języku, jest to odpowiednie i rozsądne rozwiązanie. Z drugiej strony, jeśli operacja „IS-A” jest czymś innym, co nadal mówi, że Twój obiekt „powinien” zaimplementować brakującą teraz funkcjonalność, wówczas kod wywołujący zakłada, że funkcjonalność jest obecna i wywołuje ją i ulega awarii, więc to rodzaj rzucenia wyjątku.
5) Lepszą alternatywą jest oddzielenie cechy muchy od cechy ptaka. Więc latający ptak musi wyraźnie rozszerzyć / wdrożyć zarówno Bird, jak i Fly / Flying. Jest to prawdopodobnie najczystszy projekt, ponieważ nie trzeba niczego „usuwać”. Jedyną wadą jest to, że prawie każdy ptak musi implementować zarówno Bird, jak i Fly, więc piszesz więcej kodu. Rozwiązaniem tego problemu jest posiadanie klasy pośredniej FlyingBird, która implementuje zarówno Bird, jak i Fly, i reprezentuje typowy przypadek, ale to obejście może mieć ograniczone zastosowanie bez wielokrotnego dziedziczenia.
6) Inną alternatywą, która nie wymaga wielokrotnego dziedziczenia, jest użycie kompozycji zamiast spadków. Każdy aspekt zwierzęcia jest modelowany przez niezależną klasę, a konkretny ptak jest kompozycją ptaka, a być może latania lub pływania ... Otrzymujesz pełne ponowne użycie kodu, ale musisz wykonać jeden lub więcej dodatkowych kroków, aby uzyskać funkcjonalność Latanie, gdy masz odniesienie do konkretnego ptaka. Ponadto język naturalny „obiekt IS-A Fly” i „obiekt AS-A Fly (obsada) Fly” nie będzie już działał, więc musisz wymyślić własną składnię (niektóre języki dynamiczne mogą temu zaradzić). Może to spowodować, że Twój kod będzie bardziej kłopotliwy.
7) Zdefiniuj swoją cechę Fly, aby zapewniała wyraźne wyjście z czegoś, co nie może latać. Fly.getNumberOfWings () może zwrócić 0. Jeśli Fly.fly (kierunek, currentPotinion) powinien zwrócić nową pozycję po locie, to Penguin.fly () może po prostu zwrócić bieżącą pozycję bez jej zmiany. Możesz skończyć z kodem, który technicznie działa, ale są pewne zastrzeżenia. Po pierwsze, niektóre kody mogą nie mieć oczywistego zachowania „nic nie rób”. Ponadto, jeśli ktoś wywoła x.fly (), oczekiwałby, że coś zrobi , nawet jeśli komentarz mówi, że fly () może nic nie robić . W końcu pingwin IS-A Flying nadal zwraca wartość true, co może być mylące dla programisty.
8) Wykonaj jak 5), ale użyj kompozycji, aby obejść przypadki, które wymagałyby wielokrotnego dziedziczenia. Jest to opcja, którą wolałbym dla języka statycznego, ponieważ 6) wydaje się bardziej kłopotliwy (i prawdopodobnie wymaga więcej pamięci, ponieważ mamy więcej obiektów). Język dynamiczny może sprawić, że 6) będzie mniej kłopotliwy, ale wątpię, aby stał się mniej kłopotliwy niż 5).
źródło
Zdefiniuj domyślne zachowanie (oznacz jako wirtualne) w klasie bazowej i przesłoń je w razie potrzeby. W ten sposób każdy ptak może „latać”.
Nawet pingwiny latają, szybując po lodzie na zerowej wysokości!
Zachowanie latania można w razie potrzeby zastąpić.
Inną możliwością jest posiadanie Fly Interface. Nie wszystkie ptaki zaimplementują ten interfejs.
Właściwości nie można usunąć, dlatego ważne jest, aby wiedzieć, jakie właściwości są wspólne dla wszystkich ptaków. Wydaje mi się, że bardziej kwestią projektową jest upewnienie się, że wspólne właściwości są implementowane na poziomie podstawowym.
źródło
Myślę, że wzór, którego szukasz, to dobry stary polimorfizm. Chociaż możesz usunąć interfejs z klasy w niektórych językach, prawdopodobnie nie jest to dobry pomysł z powodów podanych przez Pétera Töröka. Jednak w dowolnym języku OO można zastąpić metodę zmiany jego zachowania, co obejmuje również brak działania. Aby pożyczyć swój przykład, możesz podać metodę Penguin :: fly (), która wykonuje jedną z następujących czynności:
Właściwości mogą być nieco łatwiejsze do dodania i usunięcia, jeśli planujesz z wyprzedzeniem. Zamiast przechowywać zmienne instancji, możesz przechowywać właściwości w tablicy mapy / słownika / tablicy asocjacyjnej. Możesz użyć Wzorca Fabrycznego do stworzenia standardowych instancji takich struktur, więc Ptak pochodzący z BirdFactory zawsze zacznie od tego samego zestawu właściwości. Kodowanie kluczowej wartości Objective-C jest dobrym przykładem tego rodzaju rzeczy.
Uwaga: Poważną lekcją z poniższych komentarzy jest to, że chociaż nadpisywanie w celu usunięcia zachowania może działać, nie zawsze jest to najlepsze rozwiązanie. Jeśli musisz to zrobić w jakikolwiek znaczący sposób, powinieneś wziąć pod uwagę silny sygnał, że twój wykres spadkowy jest wadliwy. Nie zawsze jest możliwe refaktoryzacja klas, które dziedziczysz, ale kiedy to jest, jest to często lepsze rozwiązanie.
Korzystając z przykładu pingwina, jednym ze sposobów na refaktoryzację byłoby oddzielenie umiejętności latania od klasy ptaków. Ponieważ nie wszystkie ptaki potrafią latać, w tym metoda fly () w programie Bird była nieodpowiednia i prowadziła bezpośrednio do rodzaju problemu, o który pytasz. Tak więc przenieś metodę fly () (i być może start () i land ()) do klasy lub interfejsu Aviator (w zależności od języka). Pozwala to stworzyć klasę FlyingBird, która dziedziczy zarówno od Birda, jak i Aviatora (lub dziedziczy od Birda i implementuje Aviator). Pingwin może nadal dziedziczyć bezpośrednio po Ptaku, ale nie Aviator, dzięki czemu unika się problemu. Taki układ może również ułatwić tworzenie klas dla innych latających rzeczy: FlyingFish, FlyingMammal, FlyingMachine, AnnoyingInsect i tak dalej.
źródło