Czy istnieje język lub wzorzec projektowy, który umożliwia * usunięcie * zachowania lub właściwości obiektu w hierarchii klas?

28

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.

Sebastien Diot
źródło
3
„bez konieczności stosowania wszędzie okropnych hacków”: wyłączenie zachowania JEST okropnym hackem: oznaczałoby, że function save_yourself_from_crashing_airplane(Bird b) { f.fly() }byłoby to znacznie bardziej skomplikowane. (jak powiedział Peter Török, narusza LSP)
keppla
Kombinacja wzorca strategii i dziedziczenia może pozwolić ci „skomponować” odziedziczone zachowanie dla określonych super typów? Kiedy mówisz: " 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?
StuperUser,
1
Można by oczywiście tylko rzut NotSupportedExceptionz Penguin.fly().
Felix Dombek
Jeśli chodzi o języki, z pewnością można odinstalować metodę w klasie potomnej. Na przykład, w Ruby: class Penguin < Bird; undef fly; end;. Czy powinieneś to inne pytanie.
Nathan Long,
To złamałoby zasadę Liskowa i prawdopodobnie cały punkt OOP.
deadalnix

Odpowiedzi:

17

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:

var Penguin = Object.create(Bird);
Penguin.fly = undefined;
Penguin.swim = function () { ... };

W tym konkretnym przypadku Penguinaktywnie śledzi Bird.flydziedziczoną przez siebie metodę, pisząc flywłaściwość z wartością undefineddo obiektu.

Teraz możesz powiedzieć, że Penguinnie można tego już traktować jako normalnego Bird. Ale jak wspomniano, w prawdziwym świecie po prostu nie może. Ponieważ modelujemy Birdjako latająca istota.

Alternatywą jest nie przyjąć szerokiego założenia, że ​​Ptak może latać. Rozsądne byłoby posiadanie Birdabstrakcji, 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:

// for some value of Object.make
var Penguin = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Swimmer, ...
);
var Hawk = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Flyer, Carnivore, ...
);

Jeśli jesteś ciekawy, mam wdrożenieObject.make

Dodanie:

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”.

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.

Raynos
źródło
10
„w prawdziwym świecie po prostu nie może”. Tak, może. Pingwin to ptak. Zdolność do latania nie jest własnością ptaka, jest po prostu przypadkową własnością większości gatunków ptaków. Właściwości, które definiują ptaki, to „pierzaste, skrzydlate, dwunożne, endotermiczne, składające jaja, kręgowce” (Wikipedia) - nic o lataniu tam.
pdr
2
@pdr ponownie zależy od twojej definicji ptaka. Kiedy używałem terminu „Ptak”, miałem na myśli abstrakcję klasy, której używamy do reprezentowania ptaków, w tym metody muchy. Wspomniałem również, że możesz sprawić, by abstrakcja twojej klasy była mniej szczegółowa. Również pingwin nie jest upierzony.
Raynos,
2
@Raynos: Pingwiny rzeczywiście są pierzaste. Ich pióra są oczywiście dość krótkie i gęste.
Jon Purdy,
@JonPurdy wystarczy, zawsze wyobrażam sobie, że mieli futro.
Raynos
+1 ogólnie, w szczególności dla „mamuta”. LOL!
Sebastien Diot
28

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.

Péter Török
źródło
1
LSP dotyczy typów , a nie klas .
Jörg W Mittag
2
@ PéterTörök: To pytanie nie istniałoby inaczej :-) Mogę wymyślić dwa przykłady z Ruby. Classjest podklasą, Modulechociaż ClassIS-NOT-A Module. Ale nadal ma sens bycie podklasą, ponieważ wykorzystuje wiele kodu. OTOH, StringIOIS-A IO, ale ta dwójka nie ma żadnego związku dziedziczenia (poza oczywistym obojem dziedziczenia Object, oczywiście), ponieważ nie dzielą żadnego kodu. Klasy służą do udostępniania kodu, typy opisują protokoły. IOi StringIOmają ten sam protokół, a zatem ten sam typ, ale ich klasy nie są ze sobą powiązane.
Jörg W Mittag
1
@ JörgWMittag, OK, teraz rozumiem lepiej, co masz na myśli. Jednak dla mnie twój pierwszy przykład brzmi bardziej jak nadużycie dziedziczenia niż wyrażenie jakiegoś podstawowego problemu, który wydajesz się sugerować. Dziedziczenia publicznego IMO nie należy wykorzystywać do ponownego wykorzystania implementacji, a jedynie do wyrażania relacji podtypów (is-a). A fakt, że można go niewłaściwie wykorzystać, nie dyskwalifikuje go - nie wyobrażam sobie żadnego użytecznego narzędzia z żadnej domeny, którego nie można niewłaściwie wykorzystać.
Péter Török
2
Do osób popierających tę odpowiedź: zauważ, że tak naprawdę nie odpowiada na pytanie, zwłaszcza po zredagowaniu wyjaśnień. Nie sądzę, aby ta odpowiedź zasługiwała na głosowanie, ponieważ to, co mówi, jest bardzo prawdziwe i ważne, aby wiedzieć, ale tak naprawdę nie odpowiedział na pytanie.
jhocking 15.11.11
1
Wyobraź sobie Javę, w której tylko interfejsy są typami, klasy nimi nie są, a podklasy są w stanie „odinstalować” interfejsy ich nadklasy, a myślę, że masz ogólny pomysł.
Jörg W Mittag
15

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, FlightlessBirdktóre mają fabrycznie wprowadzone prawidłowe zachowanie, że odpowiednie podtypy np. Dostają Penguin : FlightlessBirdsię automatycznie, a wszystko inne naprawdę specyficzne jest obsługiwane przez Fabrykę.

StuperUser
źródło
1
W swojej odpowiedzi wspomniałem o wzorze Dekoratora, ale wzór Strategii również działa całkiem dobrze.
jhocking
1
+1 za „Preferuj kompozycję nad dziedziczeniem”. Jednak konieczność specjalnych wzorców projektowych do implementacji kompozycji w językach o typie statycznym wzmacnia moje nastawienie do języków dynamicznych, takich jak Ruby.
Roy Tinker
11

Czy prawdziwy problem, jaki zakładasz, Birdnie ma Flymetody? Dlaczego nie:

class Bird
{
    // features that all birds have
}

class BirdThatCanSwim : Bird
{
    public void Swim() {...};
}

class BirdThatCanFly : Bird
{
    public void Fly() {...};
}


class Penguin : BirdThatCanSwim { }
class Sparrow : BirdThatCanFly { }

Teraz oczywistym problemem jest wielokrotne dziedziczenie ( Duck), więc tak naprawdę potrzebujesz interfejsów:

interface IBird { }
interface IBirdThatCanSwim : IBird { public void Swim(); }
interface IBirdThatCanFly : IBird { public void Fly(); }
interface IBirdThatCanQuack : IBird { public void Quack(); }

class Duck : BirdThatCanFly, IBirdThatCanSwim, IBirdThatCanQuack
{
    public void Swim() {...};
    public void Quack() {...};
}
Scott Whitlock
źródło
3
Problem polega na tym, że ewolucja nie jest zgodna z zasadą podstawienia Liskowa i dziedziczy po usunięciu funkcji.
Donal Fellows,
7

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:

  • Bird to klasa z metodą fly ()
  • Pingwin musi odziedziczyć po Ptaku
  • Pingwin nie umie latać ()
  • Nie obchodzi mnie, czy jest to dobry projekt, czy pasuje do realnego świata, ponieważ jest to przykład podany w tym pytaniu.

Powiedziałeś :

Z jednej strony nie chcesz definiować dla każdej właściwości i zachowania jakiejś flagi określającej, 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

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):

class Penguin extends Bird {
     function fly():void {
          throw new Exception("Hey, I'm a penguin, I can't fly !");
     }
}

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):

var bird:Bird = new Penguin();
bird.fly();

zgłosi wyjątek czasu wykonywania.

David
źródło
„Po prostu spraw, aby Twój pingwin rzucił wyjątek lub odziedziczył klasę NonFlyingBird, która zgłasza wyjątek” To wciąż naruszenie LSP. Nadal sugeruje, że pingwin może latać, nawet jeśli jego realizacja w locie zakończy się niepowodzeniem. Pingwin nigdy nie powinien mieć metody latania.
pdr
@pdr: nie sugeruje, że pingwin może latać, ale że powinien latać (jest to umowa). Wyjątek powie ci, że nie może . Nawiasem mówiąc, nie twierdzę, że jest to dobra praktyka OOP, po prostu udzielam odpowiedzi na część pytania
David,
Chodzi o to, że pingwin nie powinien latać tylko dlatego, że jest ptakiem. Jeśli chcę napisać kod z napisem „Jeśli x może latać, zrób to; Muszę użyć try / catch w twojej wersji, gdzie powinienem móc zapytać obiekt, czy może on latać (istnieje metoda rzutowania lub sprawdzania). Może to być tylko sformułowanie, ale twoja odpowiedź sugeruje, że zgłoszenie wyjątku jest zgodne z LSP.
pdr
@pdr „Muszę użyć try / catch w twojej wersji” -> o to chodzi w prośbie o wybaczenie, a nie o pozwolenie (ponieważ nawet kaczka mogła złamać skrzydła i nie być w stanie latać). Naprawię sformułowanie.
David
„Właśnie o to chodzi w prośbie o wybaczenie, a nie o pozwolenie”. Tak, z wyjątkiem tego, że pozwala frameworkowi na zgłoszenie tego samego typu wyjątku dla dowolnej brakującej metody, więc „spróbuj: oprócz AttributeError:” w Pythonie jest dokładnie równoważne z C # ”, jeśli (X to Y) {} else {}” i natychmiast rozpoznawalne takie jak. Ale jeśli celowo rzuciłeś CannotFlyException, aby zastąpić domyślną funkcję fly () w Birdie, staje się ona mniej rozpoznawalna.
pdr
7

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.

alex
źródło
Zgadzam się, Fly powinien być interfejsem, który ptak może wdrożyć. Można go również zaimplementować jako metodę z zachowaniem domyślnym, które można zastąpić, ale bardziej przejrzyste podejście wykorzystuje interfejs.
Jon Raynor
6

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:

class Bird {
public:
   virtual void fly()=0;
};

Powinieneś mieć coś takiego:

   class Bird {
   public:
      virtual float fly(float x) const=0;
   };

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 ().

tp1
źródło
1
Bardzo podoba mi się twoja odpowiedź. Myślę, że zasługuje na więcej głosów.
Sebastien Diot
2
Doskonała odpowiedź. Problem polega na tym, że pytanie tylko macha rękami, co faktycznie robi fly (). Każda rzeczywista implementacja mucha miałaby przynajmniej miejsce docelowe - mucha (miejsce docelowe współrzędnych), które w przypadku pingwina mogłoby zostać zastąpione w celu zaimplementowania {return currentPosition)}
Chris Cudmore
4

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.

jhocking
źródło
3

Peter wspomniał o zasadzie substytucji Liskowa, ale uważam, że należy to wyjaśnić.

Niech q (x) będzie właściwością dającą się udowodnić o obiektach x typu T. Następnie q (y) powinno być możliwe do udowodnienia dla obiektów y typu S, gdzie S jest podtypem T.

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ść”.

pdr
źródło
2

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.

DJClayworth
źródło
0

Typowym sposobem radzenia sobie z taką sytuacją jest rzucenie czegoś w rodzaju UnsupportedOperationException(Java) resp. NotImplementedException(DO#).

użytkownik 281377
źródło
Tak długo, jak dokumentujesz tę możliwość w Bird.
DJClayworth
0

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).

Sebastien Diot
źródło
0

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.

class eagle : bird, IFly
class penguin : bird

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.

Jon Raynor
źródło
-1

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:

  • nic
  • zgłasza wyjątek
  • zamiast tego wywołuje metodę Penguin :: swim ()
  • zapewnia, że ​​pingwin jest podwodny (coś w rodzaju „latania” przez wodę)

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.

Caleb
źródło
2
-1 za sugerowanie nawet wywołania Penguin :: swim (). To narusza zasadę najmniejszego zdziwienia i spowoduje, że programiści zajmujący się konserwacją na całym świecie będą przeklinać twoje imię.
DJClayworth
1
@DJClayworth Ponieważ przykład był po stronie absurdalnej, głosowanie w dół za naruszenie wywnioskowanego zachowania fly () i swim () wydaje się trochę za duże. Ale jeśli naprawdę chcesz spojrzeć na to poważnie, zgodziłbym się, że bardziej prawdopodobne jest, że pójdziesz w inną stronę i zastosujesz swim () pod względem fly (). Kaczki pływają wiosłując stopami; pingwiny pływają trzepocząc skrzydłami.
Caleb
1
Zgadzam się, że pytanie było głupie, ale problem polega na tym, że widziałem, jak ludzie robią to w prawdziwym życiu - używaj istniejących połączeń, które „tak naprawdę nic nie robią”, aby zaimplementować rzadką funkcjonalność. Naprawdę psuje kod i zwykle kończy się na napisaniu „if (! (MyBird instanceof Penguin)) fly ();” w wielu miejscach, mając nadzieję, że nikt nie stworzy klasy strusia.
DJClayworth
Twierdzenie jest jeszcze gorsze. Jeśli mam tablicę Ptaków, z których wszystkie mają metodę fly (), nie chcę niepowodzenia asercji podczas wywoływania na nich fly ().
DJClayworth
1
Nie przeczytałem dokumentacji Pingwina , ponieważ dostałem tablicę Ptaków i nie wiedziałem, że Pingwin będzie w tablicy. Przeczytałem dokumentację Bird, która mówi, że kiedy wywołuję fly (), ptak leci. Gdyby w tej dokumentacji wyraźnie stwierdzono, że można by zgodzić się na wyjątek, gdyby ptak był nielotem, zgodziłbym się na to. Gdyby powiedział, że wywołanie fly () czasami zmuszałoby go do pływania, zmieniłbym się na używanie innej biblioteki klas. Lub poszedł na bardzo duży napój.
DJClayworth,