Dziedziczenie a agregacja [zamknięte]

151

Istnieją dwie szkoły myślenia o tym, jak najlepiej rozszerzać, ulepszać i ponownie wykorzystywać kod w systemie obiektowym:

  1. Dziedziczenie: rozszerz funkcjonalność klasy poprzez utworzenie podklasy. Zastąp elementy członkowskie nadklasy w podklasach, aby zapewnić nową funkcjonalność. Uczyń metody abstrakcyjnymi / wirtualnymi, aby wymusić na podklasach „wypełnianie pustych miejsc”, gdy nadklasa chce określonego interfejsu, ale nie ma pojęcia o jego implementacji.

  2. Agregacja: stwórz nową funkcjonalność, biorąc inne klasy i łącząc je w nową klasę. Dołącz wspólny interfejs do tej nowej klasy, aby zapewnić współdziałanie z innym kodem.

Jakie są korzyści, koszty i konsekwencje każdego z nich? Czy są inne alternatywy?

Widzę, że ta debata pojawia się regularnie, ale nie sądzę, aby była jeszcze o to pytana na Stack Overflow (chociaż jest jakaś podobna dyskusja). Zaskakujący jest też brak dobrych wyników w Google.

Craig Walker
źródło
9
Dobre pytanie, niestety nie mam teraz wystarczająco dużo czasu.
Toon Krijthe,
4
Dobra odpowiedź jest lepsza niż szybsza ... Będę obserwował własne pytanie, więc zagłosuję przynajmniej na ciebie :-P
Craig Walker

Odpowiedzi:

181

Nie jest kwestią tego, co jest najlepsze, ale kiedy z czego korzystać.

W „normalnych” przypadkach wystarczy proste pytanie, aby dowiedzieć się, czy potrzebujemy dziedziczenia czy agregacji.

  • Jeśli nowa klasa jest mniej więcej taka, jak oryginalna klasa. Użyj dziedziczenia. Nowa klasa jest teraz podklasą oryginalnej klasy.
  • Jeśli nowa klasa musi mieć klasę oryginalną. Użyj agregacji. Nowa klasa ma teraz klasę oryginalną jako składową.

Istnieje jednak duża szara strefa. Potrzebujemy więc kilku innych sztuczek.

  • Jeśli korzystaliśmy z dziedziczenia (lub planujemy z niego korzystać), ale używamy tylko części interfejsu, lub jesteśmy zmuszeni nadpisać wiele funkcji, aby zachować logiczną korelację. Następnie mamy duży nieprzyjemny zapach, który wskazuje, że musieliśmy użyć agregacji.
  • Jeśli korzystaliśmy z agregacji (lub planujemy z niej korzystać), ale okazuje się, że musimy skopiować prawie całą funkcjonalność. Następnie mamy zapach wskazujący na kierunek dziedziczenia.

Krótko mówiąc. Powinniśmy używać agregacji, jeśli część interfejsu nie jest używana lub musi zostać zmieniona, aby uniknąć nielogicznej sytuacji. Dziedziczenie jest potrzebne tylko wtedy, gdy potrzebujemy prawie całej funkcjonalności bez większych zmian. W razie wątpliwości użyj funkcji Agregacja.

Inną możliwością w przypadku, gdy mamy klasę, która potrzebuje części funkcjonalności oryginalnej klasy, jest podzielenie oryginalnej klasy na klasę główną i podklasę. I niech nowa klasa dziedziczy po klasie głównej. Należy jednak uważać, aby nie tworzyć nielogicznej separacji.

Dodajmy przykład. Mamy klasę „Pies” z metodami: „Zjedz”, „Spacer”, „Kora”, „Zabawa”.

class Dog
  Eat;
  Walk;
  Bark;
  Play;
end;

Potrzebujemy teraz klasy „Kot”, która wymaga „Jedz”, „Spacer”, „Mruczenie” i „Zabawa”. Więc najpierw spróbuj przedłużyć go z psa.

class Cat is Dog
  Purr; 
end;

Wygląda dobrze, ale czekaj. Ten kot może szczekać (miłośnicy kotów mnie za to zabiją). A szczekający kot narusza zasady wszechświata. Musimy więc zastąpić metodę Bark, aby nic nie robiła.

class Cat is Dog
  Purr; 
  Bark = null;
end;

Ok, to działa, ale brzydko pachnie. Spróbujmy więc agregacji:

class Cat
  has Dog;
  Eat = Dog.Eat;
  Walk = Dog.Walk;
  Play = Dog.Play;
  Purr;
end;

Ok, to jest miłe. Ten kot już nie szczeka, nawet milczy. Ale nadal ma wewnętrznego psa, który chce się wydostać. Wypróbujmy więc rozwiązanie numer trzy:

class Pet
  Eat;
  Walk;
  Play;
end;

class Dog is Pet
  Bark;
end;

class Cat is Pet
  Purr;
end;

To jest dużo czystsze. Żadnych psów wewnętrznych. A koty i psy są na tym samym poziomie. Możemy nawet wprowadzić inne zwierzaki, aby rozszerzyć model. Chyba że jest to ryba lub coś, co nie chodzi. W takim przypadku musimy ponownie dokonać refaktoryzacji. Ale to coś na inny czas.

Toon Krijthe
źródło
5
„Ponowne wykorzystanie prawie całej funkcjonalności klasy” to jedyny przypadek, w którym wolę dziedziczenie. To, co naprawdę chciałbym, to język, który ma możliwość łatwego powiedzenia „deleguj do tego zagregowanego obiektu dla tych konkretnych metod”; to najlepsze z obu światów.
Craig Walker
Nie jestem pewien co do innych języków, ale Delphi ma mechanizm, który pozwala członkom implementować część interfejsu.
Toon Krijthe,
2
Cel C wykorzystuje protokoły, które pozwalają Ci zdecydować, czy Twoja funkcja jest wymagana, czy opcjonalna.
cantfindaname88
1
Świetna odpowiedź z praktycznym przykładem. Zaprojektowałbym klasę Pet za pomocą metody o nazwie makeSoundi pozwoliłbym każdej podklasie implementować swój własny rodzaj dźwięku. Pomoże to w sytuacjach, w których masz wiele zwierząt i po prostu to robisz for_each pet in the list of pets, pet.makeSound.
blongho
38

Na początku GOF podają

Preferuj kompozycję obiektów zamiast dziedziczenia klas.

Jest to szerzej omówione tutaj

zestaw narzędzi
źródło
27

Różnica jest zwykle wyrażana jako różnica między „jest” a „ma”. Dziedziczenie, relacja „jest”, ładnie podsumowuje Zasada Zastępstwa Liskova . Agregacja, relacja „ma”, jest po prostu tym - pokazuje, że obiekt agregujący ma jeden z obiektów zagregowanych.

Istnieją również dalsze różnice - dziedziczenie prywatne w C ++ wskazuje, że relacja „jest implementowana w kategoriach”, która może być również modelowana przez agregację (niewidocznych) obiektów członkowskich.

Harper Shelby
źródło
Również różnicę podsumowuje się ładnie w samym pytaniu, na które powinieneś odpowiedzieć. Chciałbym, żeby ludzie najpierw przeczytali pytania, zanim zaczną tłumaczyć. Czy ślepo promujesz wzorce projektowe, aby zostać członkiem elitarnego klubu?
Val
Nie podoba mi się takie podejście, bardziej chodzi o kompozycję
Alireza Rahmani Khalili
15

Oto mój najczęstszy argument:

W każdym systemie zorientowanym obiektowo każda klasa składa się z dwóch części:

  1. Jego interfejs : „publiczna twarz” obiektu. To jest zestaw możliwości, które ogłasza reszcie świata. W wielu językach zbiór jest dobrze zdefiniowany jako „klasa”. Zwykle są to sygnatury metod obiektu, chociaż różnią się one nieco w zależności od języka.

  2. Jego realizacja : praca „za kulisami” wykonywana przez obiekt w celu dostosowania się do jego interfejsu i zapewnienia funkcjonalności. Zwykle jest to kod i dane składowe obiektu.

Jedną z podstawowych zasad OOP jest to, że implementacja jest hermetyzowana (tj. Ukryta) w klasie; jedyną rzeczą, którą osoby postronne powinny zobaczyć, jest interfejs.

Gdy podklasa dziedziczy po podklasie, zazwyczaj dziedziczy zarówno implementację, jak i interfejs. To z kolei oznacza, że ​​jesteś zmuszony zaakceptować oba jako ograniczenia swojej klasy.

Dzięki agregacji możesz wybrać implementację lub interfejs, lub oba - ale nie jesteś do tego zmuszony. Funkcjonalność obiektu jest pozostawiona samemu obiektowi. Może odkładać inne obiekty, jak chce, ale ostatecznie odpowiada za siebie. Z mojego doświadczenia wynika, że ​​prowadzi to do bardziej elastycznego systemu: łatwiejszego do modyfikacji.

Dlatego zawsze, gdy tworzę oprogramowanie obiektowe, prawie zawsze wolę agregację niż dziedziczenie.

Craig Walker
źródło
Wydaje mi się, że do zdefiniowania interfejsu używasz klasy abstrakcyjnej, a następnie używasz bezpośredniego dziedziczenia dla każdej konkretnej klasy implementacji (co czyni ją dość płytką). Wszelkie agregacje są objęte konkretną realizacją.
orcmid
Jeśli potrzebujesz dodatkowych interfejsów niż to, zwróć je za pomocą metod interfejsu abstrakcyjnego interfejsu głównego poziomu, powtórz płukanie.
orcmid
Czy uważasz, że agregacja jest lepsza w tym scenariuszu? Książka to element sprzedaży, dysk cyfrowy
LCJ
11

Odpowiedziałem na pytanie „Czy” kontra „Czy”: który jest lepszy? .

Zasadniczo zgadzam się z innymi ludźmi: używaj dziedziczenia tylko wtedy, gdy twoja klasa pochodna jest naprawdę rozszerzanym typem, a nie tylko dlatego, że zawiera te same dane. Pamiętaj, że dziedziczenie oznacza, że ​​podklasa zyskuje zarówno metody, jak i dane.

Czy ma sens, aby Twoja klasa pochodna miała wszystkie metody klasy nadrzędnej? A może po cichu obiecujesz sobie, że te metody powinny być ignorowane w klasie pochodnej? A może zdarza Ci się, że zastępujesz metody z superklasy, czyniąc je nie-opami, więc nikt nie nazywa ich przypadkowo? A może podpowiadasz swojemu narzędziu do generowania dokumentów API, aby pominąć metodę w dokumencie?

To są mocne wskazówki, że agregacja jest lepszym wyborem w tym przypadku.

Bill Karwin
źródło
6

Widzę wiele odpowiedzi typu „jest-a-ma-a”; są one koncepcyjnie różne ”na to i powiązane pytania.

Jedyną rzeczą, jaką odkryłem z mojego doświadczenia, jest to, że próba ustalenia, czy związek jest „jest”, czy „ma”, jest skazana na niepowodzenie. Nawet jeśli teraz możesz poprawnie określić te obiekty dla obiektów, zmieniające się wymagania oznaczają, że prawdopodobnie będziesz się mylić w pewnym momencie w przyszłości.

Inną rzeczą, jaką odkryłem, jest to, że bardzo trudno jest przekonwertować dziedziczenie na agregację, gdy jest dużo kodu napisanego wokół hierarchii dziedziczenia. Samo przejście z nadklasy do interfejsu oznacza zmianę prawie każdej podklasy w systemie.

I, jak wspomniałem w innym miejscu tego posta, agregacja jest mniej elastyczna niż dziedziczenie.

Masz więc idealną burzę argumentów przeciwko dziedziczeniu, ilekroć musisz wybrać jedną lub drugą:

  1. Twój wybór prawdopodobnie w pewnym momencie będzie zły
  2. Zmiana tego wyboru jest trudna, gdy już to zrobisz.
  3. Dziedziczenie wydaje się być gorszym wyborem, ponieważ jest bardziej ograniczające.

Dlatego mam tendencję do wybierania agregacji - nawet jeśli wydaje się, że istnieje silna relacja.

Craig Walker
źródło
Zmieniające się wymagania oznaczają, że projekt OO będzie nieuchronnie zły. Dziedziczenie a agregacja to tylko wierzchołek góry lodowej. Nie możesz być architektem dla wszystkich możliwych przyszłości, więc po prostu idź z pomysłem XP: rozwiąż dzisiejsze wymagania i zaakceptuj, że być może będziesz musiał refaktoryzować.
Bill Karwin
Podoba mi się ta linia pytań i zapytań. Zaniepokojenie rozmyciem jest-a, ma-a i zastosowaniami-a. Zaczynam od interfejsów (wraz z identyfikacją zaimplementowanych abstrakcji, do których chcę interfejsów). Dodatkowy poziom pośrednictwa jest nieoceniony. Nie podążaj za wnioskiem dotyczącym agregacji.
orcmid
Jednak w zasadzie o to mi chodzi. Dziedziczenie i agregacja to metody rozwiązania problemu. Jedna z tych metod pociąga za sobą różnego rodzaju kary, gdy jutro będziesz musiał rozwiązać problemy.
Craig Walker
Moim zdaniem agregacja + interfejsy są alternatywą dla dziedziczenia / podklasy; nie można zrobić polimorfizmu opartego na samej agregacji.
Craig Walker
3

Pytanie to jest zwykle formułowane jako skład a dziedziczenie i zostało zadane tutaj wcześniej.

Bill the Lizard
źródło
Racja, jesteś ... zabawny, że SO nie podał tego jako jednego z powiązanych pytań.
Craig Walker
2
Więc
Zgoda. Dlatego tego nie zamknąłem. To i wiele osób już tutaj odpowiedziało.
Bill the Lizard
1
SO potrzebuje funkcji, aby rzucić 2 pytania (i ich odpowiedzi) na 1
Craig Walker
3

Chciałem zrobić to jako komentarz do pierwotnego pytania, ale 300 znaków ugryza się [; <).

Myślę, że musimy być ostrożni. Po pierwsze, jest więcej smaków niż dwa raczej konkretne przykłady podane w pytaniu.

Sugeruję również, aby nie mylić celu z instrumentem. Chce się mieć pewność, że wybrana technika lub metodologia wspiera osiągnięcie głównego celu, ale nie myślę poza kontekstem, która technika jest najlepsza, dyskusja jest bardzo przydatna. Pomaga to poznać pułapki różnych podejść wraz z ich wyraźnymi słodkimi punktami.

Na przykład, co zamierzasz osiągnąć, od czego możesz zacząć i jakie są ograniczenia?

Czy tworzysz framework komponentowy, nawet specjalny? Czy interfejsy można oddzielić od implementacji w systemie programowania, czy też jest to realizowane przez praktykę wykorzystującą inny rodzaj technologii? Czy potrafisz oddzielić strukturę dziedziczenia interfejsów (jeśli istnieje) od struktury dziedziczenia klas, które je implementują? Czy ważne jest, aby ukryć strukturę klas implementacji przed kodem, który opiera się na interfejsach dostarczonych przez implementację? Czy istnieje wiele implementacji, które mogą być używane w tym samym czasie, czy też te zmiany są bardziej widoczne w czasie w wyniku konserwacji i ulepszania? To i wiele więcej należy wziąć pod uwagę, zanim zdecydujesz się na narzędzie lub metodologię.

Wreszcie, czy to ważne, aby zablokować rozróżnienia w abstrakcji i jak o niej myślisz (jak w przypadku jest-a-ma-a) z różnymi cechami technologii obiektowej? Być może tak, jeśli zachowa spójną strukturę koncepcyjną i będzie możliwe do zarządzania dla Ciebie i innych. Ale mądrze jest nie dać się zniewolić przez to i skrzywienie, które możesz w końcu zrobić. Może najlepiej jest cofnąć się o poziom i nie być tak sztywnym (ale zostaw dobrą narrację, aby inni mogli powiedzieć, co się dzieje). [Szukam tego, co sprawia, że ​​dana część programu jest możliwa do wyjaśnienia, ale czasami stawiam na elegancję, gdy jest większa wygrana. Nie zawsze jest to najlepszy pomysł.]

Jestem purystą interfejsu i pociągają mnie rodzaje problemów i podejść, w których puryzm interfejsów jest odpowiedni, czy to budowanie frameworka Java, czy organizowanie niektórych implementacji COM. To nie czyni go odpowiednim do wszystkiego, nawet blisko wszystkiego, nawet jeśli przysięgam. (Mam kilka projektów, które wydają się dostarczać poważnych przykładów przeciwdziałania puryzmowi interfejsów, więc ciekawie będzie zobaczyć, jak sobie z tym radzę).

orcmid
źródło
2

Opowiem o części, w której te mogą mieć zastosowanie. Oto przykład jednego i drugiego w scenariuszu gry. Załóżmy, że istnieje gra, w której występują różne typy żołnierzy. Każdy żołnierz może mieć plecak, który może pomieścić różne rzeczy.

Dziedziczenie tutaj? Jest morski, zielony beret i snajper. To są typy żołnierzy. Mamy więc klasę podstawową Soldier z klasami pochodnymi Marine, Green Beret i Sniper

Agregacja tutaj? Plecak może zawierać granaty, pistolety (różne typy), nóż, apteczkę itp. Żołnierz może być wyposażony w dowolny z nich w dowolnym momencie, a także może mieć kamizelkę kuloodporną, która działa jak zbroja podczas ataku. obrażenia spadają do określonego procentu. Klasa żołnierza zawiera obiekt klasy kamizelki kuloodpornej oraz klasę plecaka, która zawiera odniesienia do tych przedmiotów.

Salman Kasbati
źródło
2

Myślę, że to nie jest debata „albo / albo”. Po prostu:

  1. Relacje typu is-a (dziedziczenie) występują rzadziej niż relacje typu has-a (skład).
  2. Dziedziczenie jest trudniejsze do uzyskania, nawet jeśli jest to właściwe, dlatego należy zachować należytą staranność, ponieważ może zepsuć hermetyzację, zachęcić do ścisłego powiązania poprzez ujawnienie implementacji i tak dalej.

Obie mają swoje miejsce, ale dziedziczenie jest bardziej ryzykowne.

Chociaż oczywiście nie miałoby sensu posiadanie klas Shape „mających” Punkt i Kwadrat. Tutaj należy się dziedziczenie.

Ludzie mają skłonność do myślenia o dziedziczeniu w pierwszej kolejności, gdy próbują zaprojektować coś rozszerzalnego, to jest błąd.

Vinko Vrsalovic
źródło
1

Przychylność ma miejsce, gdy obaj kandydaci kwalifikują się. A i B to opcje, a ty wolisz A. Powód jest taki, że kompozycja oferuje więcej możliwości rozszerzenia / elastyczności niż uogólnienie. To rozszerzenie / elastyczność odnosi się głównie do elastyczności w czasie wykonywania / dynamicznej.

Korzyści nie są od razu widoczne. Aby zobaczyć korzyści, musisz poczekać na kolejną nieoczekiwaną prośbę o zmianę. Tak więc w większości przypadków ci, którzy trzymali się uogólnień, zawodzą w porównaniu z tymi, którzy przyjęli kompozycję (z wyjątkiem jednego oczywistego przypadku wspomnianego później). Stąd zasada. Z punktu widzenia uczenia się, jeśli możesz pomyślnie zaimplementować wstrzyknięcie zależności, powinieneś wiedzieć, który z nich preferować i kiedy. Reguła pomaga również w podjęciu decyzji; jeśli nie jesteś pewien, wybierz kompozycję.

Podsumowanie: Kompozycja: Sprzężenie jest zredukowane przez po prostu posiadanie kilku mniejszych rzeczy, które podłączasz do czegoś większego, a większy obiekt po prostu przywołuje mniejszy obiekt z powrotem. Uogólnianie: z punktu widzenia interfejsu API określenie, że metoda może zostać zastąpiona, jest silniejszym zobowiązaniem niż zdefiniowanie, że można wywołać metodę. (bardzo niewiele przypadków, gdy wygrywa Generalizacja). I nigdy nie zapominaj, że w przypadku kompozycji używasz również dziedziczenia, z interfejsu zamiast dużej klasy

Niebieskie chmury
źródło
0

Oba podejścia są używane do rozwiązywania różnych problemów. Podczas dziedziczenia z jednej klasy nie zawsze musisz agregować dwie lub więcej klas.

Czasami musisz zagregować pojedynczą klasę, ponieważ ta klasa jest zapieczętowana lub ma inne niewirtualne elementy członkowskie, które musisz przechwycić, więc tworzysz warstwę proxy, która oczywiście nie jest ważna pod względem dziedziczenia, ale tak długo, jak klasa, którą pośredniczysz ma interfejs, który możesz zasubskrybować, może działać całkiem dobrze.

cfeduke
źródło