Istnieją dwie szkoły myślenia o tym, jak najlepiej rozszerzać, ulepszać i ponownie wykorzystywać kod w systemie obiektowym:
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.
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.
źródło
Odpowiedzi:
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.
Istnieje jednak duża szara strefa. Potrzebujemy więc kilku innych sztuczek.
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”.
Potrzebujemy teraz klasy „Kot”, która wymaga „Jedz”, „Spacer”, „Mruczenie” i „Zabawa”. Więc najpierw spróbuj przedłużyć go z psa.
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.
Ok, to działa, ale brzydko pachnie. Spróbujmy więc agregacji:
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:
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.
źródło
makeSound
i 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 robiszfor_each pet in the list of pets, pet.makeSound
.Na początku GOF podają
Jest to szerzej omówione tutaj
źródło
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.
źródło
Oto mój najczęstszy argument:
W każdym systemie zorientowanym obiektowo każda klasa składa się z dwóch części:
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.
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.
źródło
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.
źródło
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ą:
Dlatego mam tendencję do wybierania agregacji - nawet jeśli wydaje się, że istnieje silna relacja.
źródło
Pytanie to jest zwykle formułowane jako skład a dziedziczenie i zostało zadane tutaj wcześniej.
źródło
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ę).
źródło
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.
źródło
Myślę, że to nie jest debata „albo / albo”. Po prostu:
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.
źródło
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
źródło
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.
źródło