Jak obsługiwać metody dodane dla podtypów w kontekście polimorfizmu?

14

Kiedy używasz koncepcji polimorfizmu, tworzysz hierarchię klas, a korzystając z referencji rodziców wywołujesz funkcje interfejsu, nie wiedząc, który konkretny typ ma obiekt. To jest świetne. Przykład:

Masz kolekcję zwierząt i przywołujesz funkcje wszystkich zwierząt eati nie obchodzi cię, czy to pies je czy kot. Ale w tej samej hierarchii klas masz zwierzęta, które mają dodatkowe - inne niż odziedziczone i zaimplementowane z klasy Animal, npmakeEggs , getBackFromTheFreezedStatei tak dalej. Tak więc w niektórych przypadkach w twojej funkcji możesz chcieć znać konkretny typ, aby wywołać dodatkowe zachowania.

Na przykład, w przypadku, gdy jest poranek, a jeśli jest to tylko zwierzę, wtedy wołasz eat, w przeciwnym razie, jeśli jest to człowiek, najpierw zadzwoń washHands, getDresseda dopiero potem zadzwoń eat. Jak poradzić sobie z tymi sprawami? Polimorfizm umiera. Musisz znaleźć typ obiektu, który brzmi jak zapach kodu. Czy istnieje wspólne podejście do obsługi tych przypadków?

Narek
źródło
7
Opisany typ polimorfizmu nazywa się polimorfizmem podtytułu , ale nie jest to jedyny rodzaj (patrz polimorfizm ). Nie musisz tworzyć hierarchii klas, aby wykonywać polimorfizm (i faktycznie twierdzę, że dziedziczenie nie jest najczęstszą metodą osiągnięcia polimorfizmu podtypów, implementacja interfejsu jest znacznie bardziej powszechna).
Vincent Savard
24
Jeśli zdefiniujesz Eaterinterfejs za pomocą tej eat()metody, to jako klient nie będziesz się przejmować tym, że Humanimplementacja musi najpierw wywołać washHands()i getDressed()jest to szczegół implementacji tej klasy. Jeśli jako klient zależy Ci na tym fakcie, najprawdopodobniej nie używasz odpowiedniego narzędzia do tego zadania.
Vincent Savard
3
Trzeba także wziąć pod uwagę, że rano człowiek może potrzebować getDressedprzed nimi eat, tak nie jest w przypadku obiadu. W zależności od okoliczności washHands();if !dressed then getDressed();[code to actually eat]może być najlepszym sposobem na wdrożenie tego dla człowieka. Inną możliwością jest to, czy inne rzeczy tego wymagają washHandsi / lub getDressedsą nazywane? Załóżmy, że masz leaveForWork? Być może będziesz musiał tak ustrukturyzować przepływ programu, aby tak się nazywał na długo wcześniej.
Duncan X Simpson
1
Należy pamiętać, że sprawdzanie dokładnego typu może być zapachem kodu w OOP, ale jest to bardzo powszechna praktyka w FP (tj. Użyj dopasowania wzorca, aby określić typ dyskryminowanego związku, a następnie działaj zgodnie z nim).
Theodoros Chatzigiannakis
3
Uwaga na szkolne przykłady hierarchii OO, takich jak zwierzęta. Prawdziwe programy prawie nigdy nie mają tak czystych taksonomii. Np . Ericlippert.com/2015/04/27/wizards-and-warriors-part-one . Lub jeśli chcesz przejść cały wieprz i zakwestionować cały paradygmat: Programowanie obiektowe jest złe .
jpmc26

Odpowiedzi:

18

Zależy. Niestety nie ma ogólnego rozwiązania. Pomyśl o swoich wymaganiach i spróbuj dowiedzieć się, co te rzeczy powinny zrobić.

Na przykład powiedziałeś rano, że różne zwierzęta robią różne rzeczy. Co powiesz na wprowadzenie metody getUp()lubprepareForDay() czy coś takiego. Następnie możesz kontynuować polimorfizm i pozwolić każdemu zwierzęciu wykonywać swoją poranną rutynę.

Jeśli chcesz rozróżniać zwierzęta, nie powinieneś przechowywać ich bez rozróżnienia na liście.

Jeśli nic innego nie działa, możesz wypróbować Wzorzec Odwiedzającego , który jest swego rodzaju hackiem, umożliwiającym dynamiczne wysyłanie, w którym możesz przesłać gościa, który otrzyma od zwierząt wywołania zwrotne dokładnie takie jak w przypadku danego typu. Chciałbym jednak podkreślić, że powinno to być ostatecznością, jeśli wszystko inne zawiedzie.

Robert Bräutigam
źródło
33

To dobre pytanie i jest to rodzaj kłopotów dla wielu ludzi, którzy próbują zrozumieć, jak korzystać z OO. Myślę, że większość programistów ma z tym problem. Chciałbym móc powiedzieć, że większość mi się to udaje, ale nie jestem pewien, czy tak jest. Z mojego doświadczenia wynika, że ​​większość programistów używa toreb pseudo-OO .

Po pierwsze, wyjaśnię. To nie twoja wina. Sposób, w jaki zwykle naucza się OO, jest wysoce wadliwy. AnimalPrzykładem jest sprawca premier, IMO. Zasadniczo mówimy, porozmawiajmy o przedmiotach, co mogą zrobić. AnimalMoże eat()a możespeak() . Wspaniały. Teraz stwórz zwierzęta i napisz, jak jedzą i mówią. Teraz znasz OO, prawda?

Problem polega na tym, że zbliża się to do OO ze złego kierunku. Dlaczego w tym programie są zwierzęta i dlaczego muszą mówić i jeść?

Trudno mi myśleć o prawdziwym użyciu danego Animaltypu. Jestem pewien, że istnieje, ale omówmy coś, co moim zdaniem jest łatwiejsze do uzasadnienia: symulację ruchu. Załóżmy, że chcemy modelować ruch w różnych scenariuszach. Oto kilka podstawowych rzeczy, które musimy mieć, aby móc to zrobić.

Vehicle
Road
Signal

Możemy zagłębiać się w różnego rodzaju rzeczy dla pieszych i pociągów, ale utrzymamy prostotę.

Zastanówmy się Vehicle. Jakie możliwości potrzebuje pojazd? Musi podróżować po drodze. Musi być w stanie zatrzymać się na sygnałach. Musi być w stanie nawigować na skrzyżowaniach.

interface Vehicle {
  move(Road road);
  navigate(Road... intersection);
}

Jest to prawdopodobnie zbyt proste, ale to początek. Teraz. A co ze wszystkimi innymi rzeczami, które może zrobić pojazd? Mogą skręcić z drogi i wpuścić się do rowu. Czy to część symulacji? Nie. Nie potrzebuję tego. Niektóre samochody i autobusy mają układ hydrauliczny, który pozwala im odpowiednio podskakiwać lub klęczeć. Czy to część symulacji? Nie. Nie potrzebuję tego. Większość samochodów pali benzynę. Niektórzy nie. Czy elektrownia jest częścią symulacji? Nie. Nie potrzebuję tego. Rozmiar koła? Nie potrzebuję tego Nawigacja GPS? System audio-nawigacyjny? Nie potrzebuję ich.

Musisz tylko zdefiniować zachowania, których zamierzasz użyć. W tym celu myślę, że często lepiej jest budować interfejsy OO z kodu, który z nimi współdziała. Zaczynasz z pustym interfejsem, a następnie zaczynasz pisać kod, który wywołuje nieistniejące metody. W ten sposób wiesz, jakich metod potrzebujesz w interfejsie. Gdy to zrobisz, zaczniesz definiować klasy, które implementują te zachowania. Zachowania, które nie są używane, są nieistotne i nie trzeba ich definiować.

Chodzi o to, że możesz dodawać nowe implementacje tych interfejsów później bez zmiany kodu wywołującego. Jedynym sposobem, który działa, jest to, że potrzeby kodu wywołującego określają, co dzieje się w interfejsie. Nie ma sposobu, aby zdefiniować wszystkie zachowania wszystkich możliwych rzeczy, które można by wymyślić później.

JimmyJames
źródło
13
To dobra odpowiedź. „W tym celu myślę, że często lepiej jest budować interfejsy OO z kodu, który z nimi współdziała”. Oczywiście i twierdzę, że to jedyny sposób. Kontraktu publicznego na interfejs nie można poznać tylko poprzez jego implementację, zawsze jest on definiowany z perspektywy jego klientów. (I na marginesie, o to właśnie chodzi w TDD.)
Vincent Savard
@VincentSavard „Twierdzę, że to jedyny sposób”. Masz rację. Wydaje mi się, że powodem, dla którego nie uczyniłem tego absolutnym, jest to, że kiedy już wpadniesz na ten pomysł, możesz w pewien sposób udoskonalić interfejs, a następnie udoskonalić go w ten sposób. Ostatecznie, kiedy przejdziesz do mosiężnych haków, to jedyna rzecz, która ma znaczenie.
JimmyJames,
@ jpmc26 Być może trochę mocno sformułowane. Nie jestem pewien, czy zgadzam się, że rzadko to wdraża. Nie jestem pewien, w jaki sposób interfejsy mogą być przydatne, jeśli nie używasz ich w ten sposób oprócz interfejsów znaczników, które moim zdaniem są okropnym pomysłem.
JimmyJames
9

TL; DR:

Pomyśl o abstrakcji i metodach, które mają zastosowanie do wszystkich podklas i obejmuj wszystko, czego potrzebujesz.

Najpierw zostańmy z twoim eat()przykładem.

Jest to właściwość bycia człowiekiem, który jako warunek jedzenia, chce umyć ręce i ubrać się przed jedzeniem. Jeśli chcesz, żeby ktoś przyszedł do ciebie, aby zjeść z tobą śniadanie, nie każesz mu myć rąk i ubierać się, robią to sami, gdy ich zaprosisz, lub odpowiadają: „Nie, nie mogę przyjść ponad nie umyłem rąk i jeszcze nie jestem ubrany ".

Powrót do oprogramowania:

Jako Humanprzykład nie chce jeść bez warunków wstępnych, że mam Human„s eat()metody zrobienia washHands()i getDressed()jeśli to nie zostało zrobione. To nie powinno być twoja praca jakoeat()Wiedzieć o tej osobliwości dzwoniącej. Alternatywą upartego człowieka byłoby rzucenie wyjątku („Nie jestem gotowy do jedzenia!”), Jeśli warunki wstępne nie są spełnione, co powoduje, że jesteś sfrustrowany, ale przynajmniej informowany, że jedzenie nie działa.

Co makeEggs()?

Polecam zmienić twój sposób myślenia. Prawdopodobnie chcesz wykonać zaplanowane poranne obowiązki wszystkich istot. Ponownie, jako dzwoniący, nie powinno być twoim obowiązkiem wiedzieć, jakie są ich obowiązki. Polecam więc doMorningDuties()metodę, którą wszystkie klasy implementują.

Ralf Kleberhoff
źródło
Zgadzam się z tą odpowiedzią. Narek ma rację co do zapachu kodu. Jest to śmierdzący interfejs, więc napraw to i swoje.
Jonathan van de Veen
To, co opisuje ta odpowiedź, jest zwykle określane jako Zasada Zastępowania Liskowa .
Filip
2

Odpowiedź jest dość prosta.

Jak obchodzić się z obiektami, które mogą zrobić więcej, niż się spodziewasz?

Nie musisz sobie z tym poradzić, ponieważ nie miałoby to sensu. Interfejs jest zwykle projektowany w zależności od tego, jak będzie używany. Jeśli twój interfejs nie definiuje mycia rąk, nie obchodzi cię to jako rozmówcy interfejsu; gdybyś to zrobił, zaprojektowałbyś to inaczej.

Na przykład, jeśli jest poranek, a jeśli jest to tylko zwierzę, wtedy wołasz jeść, w przeciwnym razie, jeśli jest to człowiek, wówczas wzywaj najpierw washHands, getDressed, a dopiero potem jedz. Jak poradzić sobie z tymi sprawami?

Na przykład w pseudokodzie:

interface IEater { void Eat(); }
interface IMorningRoutinePerformer { void DoMorningRoutine(); }
interface IAnimal : IEater, IMorningPerformer;
interface IHuman : IEater, IMorningPerformer; 
{
  void WashHands();
  void GetDressed();
}

void MorningTime()
{
   IList<IMorningRoutinePerformer> items = Service.GetMorningPerformers();
   foreach(item in items) { item.DoMorningRoutine(); }
}

Teraz wdrażasz IMorningPerformerdo Animalpo prostu jedzenia, a Humantakże do mycia rąk i ubierania się. Osoba dzwoniąca z twojej metody MorningTime może mniej obchodzić, czy jest to człowiek czy zwierzę. Wszystko, czego chce, to poranna rutyna, którą każdy obiekt zachwyca dzięki OO.

Polimorfizm umiera.

A może to?

Musisz dowiedzieć się, jaki jest typ obiektu

Dlaczego to zakładasz? Myślę, że może to być błędne założenie.

Czy istnieje wspólne podejście do obsługi tych przypadków?

Tak, zwykle rozwiązuje się to dzięki starannie zaprojektowanej hierarchii klas lub interfejsów. Zauważ, że w powyższym przykładzie nic nie stoi w sprzeczności z podanym przez ciebie przykładem, jednak prawdopodobnie poczujesz się niezadowolony, ponieważ poczyniłeś pewne założenia, których nie napisałeś w pytaniu od momentu pisania , a te założenia prawdopodobnie zostały naruszone.

Możliwe jest udanie się do króliczej nory przez zaostrzenie twoich założeń i modyfikowanie odpowiedzi, aby nadal je spełniać, ale nie sądzę, by to było przydatne.

Projektowanie dobrych hierarchii klas jest trudne i wymaga dużego wglądu w domenę biznesową. W przypadku złożonych domen można przejść przez dwie, trzy lub nawet więcej iteracji, ponieważ dopracowują one swoje zrozumienie interakcji różnych podmiotów w swojej domenie biznesowej, dopóki nie uzyskają odpowiedniego modelu.

Tam brakuje uproszczonych przykładów zwierząt. Chcemy uczyć w prosty sposób, ale problem, który staramy się rozwiązać, nie jest oczywisty, dopóki nie zagłębisz się w to, co wiąże się z bardziej złożonymi przemyśleniami i domenami.

Andrew Savinykh
źródło