Ta strona opowiada się za kompozycją za dziedziczeniem za pomocą następującego argumentu (sformułował to w moich słowach):
Zmiana podpisu metody nadklasy (która nie została zastąpiona w podklasie) powoduje dodatkowe zmiany w wielu miejscach, gdy używamy dziedziczenia. Jednak gdy korzystamy ze składu, wymagana dodatkowa zmiana występuje tylko w jednym miejscu: podklasie.
Czy to naprawdę jedyny powód, aby preferować kompozycję nad dziedziczeniem? Ponieważ w takim przypadku problem ten można łatwo rozwiązać poprzez wymuszenie stylu kodowania, który zaleca przesłonięcie wszystkich metod nadklasy, nawet jeśli podklasa nie zmienia implementacji (to znaczy umieszczenie zastępczych nadpisań w podklasie). Czy coś mi umyka?
Odpowiedzi:
Lubię analogie, więc oto jedna: Czy widziałeś kiedyś telewizor z wbudowanym odtwarzaczem VCR? Co powiesz na ten z magnetowidem i odtwarzaczem DVD? Lub taki, który ma Blu-ray, DVD i magnetowid. Och, a teraz wszyscy przesyłają strumieniowo, więc musimy zaprojektować nowy telewizor ...
Najprawdopodobniej, jeśli posiadasz telewizor, nie masz takiego jak wyżej. Prawdopodobnie większość z nich nigdy nie istniała. Najprawdopodobniej masz monitor lub zestaw, który ma wiele wejść, dzięki czemu nie potrzebujesz nowego zestawu za każdym razem, gdy pojawia się nowy typ urządzenia wejściowego.
Dziedziczenie jest jak telewizor z wbudowanym magnetowidem. Jeśli masz 3 różne typy zachowań, których chcesz używać razem i każdy ma 2 opcje implementacji, potrzebujesz 8 różnych klas, aby przedstawić je wszystkie. W miarę dodawania kolejnych opcji liczby wybuchają. Jeśli zamiast tego zastosujesz kompozycję, unikniesz tego problemu kombinatorycznego, a projekt będzie miał tendencję do rozszerzania się.
źródło
Nie, nie jest. Z mojego doświadczenia wynika, że kompozycja nad dziedziczeniem jest wynikiem myślenia o twoim oprogramowaniu jako grupie obiektów z zachowaniami, które rozmawiają ze sobą / obiektami świadczącymi sobie usługi zamiast klas i danych .
W ten sposób masz kilka obiektów, które zapewniają serviecs. Używasz ich jako elementów składowych (tak jak Lego), aby umożliwić inne zachowanie (np. UserRegistrationService korzysta z usług świadczonych przez EmailerService i UserRepository ).
Przy budowaniu większych systemów z takim podejściem naturalnie korzystałem z dziedziczenia w bardzo niewielu przypadkach. Na koniec dnia dziedziczenie jest bardzo potężnym narzędziem, z którego należy korzystać ostrożnie i tylko w razie potrzeby.
źródło
„Wolę kompozycję niż dziedziczenie” to po prostu dobra heurystyka
Należy wziąć pod uwagę kontekst, ponieważ nie jest to reguła uniwersalna. Nie należy rozumieć, że nigdy nie należy używać dziedziczenia, gdy można tworzyć kompozycje. Gdyby tak było, naprawilibyście to, zakazując dziedziczenia.
Mam nadzieję, że wyjaśnię ten punkt w tym poście.
Nie będę próbował sam bronić zalet kompozycji. Które uważam za nie na temat. Zamiast tego porozmawiam o niektórych sytuacjach, w których programista może rozważyć dziedziczenie, które lepiej byłoby zastosować kompozycję. W tej kwestii dziedziczenie ma swoje zalety, które również uważam za nie na temat.
Przykład pojazdu
Piszę o programistach, którzy próbują robić rzeczy głupio, do celów narracyjnych
Pójdźmy za wariantem klasycznych przykładów, że niektóre kursy OOP używać ... Mamy
Vehicle
klasę, a następnie wyprowadzićCar
,Airplane
,Balloon
iShip
.Uwaga : jeśli musisz uziemić ten przykład, udawaj, że są to rodzaje obiektów w grze wideo.
Wtedy
Car
iAirplane
może mieć jakiś wspólny kod, ponieważ mogą one zarówno toczyć na kole na ziemi. Deweloperzy mogą rozważyć utworzenie w tym celu klasy pośredniej w łańcuchu dziedziczenia. Jednak w rzeczywistości istnieje również wspólny kod międzyAirplane
iBalloon
. Mogą rozważyć utworzenie innej klasy pośredniej w łańcuchu spadkowym.Dlatego twórca szukałby wielokrotnego dziedziczenia. W momencie, gdy programiści szukają wielokrotnego dziedziczenia, projekt już się nie udał.
Lepiej jest modelować to zachowanie jako interfejsy i kompozycję, abyśmy mogli go ponownie wykorzystać bez konieczności dziedziczenia wielu klas. Jeśli na przykład programiści utworzą
FlyingVehicule
klasę. Mówiliby, żeAirplane
jest toFlyingVehicule
(dziedziczenie klas), ale moglibyśmy zamiast tego powiedzieć, żeAirplane
maFlying
składnik (skład) iAirplane
jestIFlyingVehicule
(dziedziczenie interfejsu).Używając interfejsów, w razie potrzeby możemy mieć wielokrotne dziedziczenie (interfejsów). Ponadto nie łączysz się z konkretną implementacją. Zwiększanie możliwości ponownego użycia i testowania kodu.
Pamiętaj, że dziedziczenie jest narzędziem polimorfizmu. Ponadto polimorfizm jest narzędziem do ponownego użycia. Jeśli możesz zwiększyć możliwość ponownego użycia kodu za pomocą kompozycji, zrób to. Jeśli nie masz pewności, czy jakakolwiek kompozycja zapewnia lepszą przydatność do ponownego użycia, „Preferuj kompozycję zamiast dziedziczenia” jest dobrą heurystyką.
Wszystko to bez wzmianki
Amphibious
.W rzeczywistości możemy nie potrzebować rzeczy, które zejdą z ziemi. Stephen Hurn ma bardziej wymowny przykład w swoich artykułach „Preferuj kompozycję nad spadkiem” część 1 i część 2 .
Substytucyjność i enkapsulacja
Czy powinien
A
dziedziczyć czy komponowaćB
?Jeśli
A
specjalizacjaB
tego typu powinna spełniać zasadę podstawienia Liskowa , dziedziczenie jest opłacalne, a nawet pożądane. Jeśli zdarzają się sytuacje, w którychA
nie jest to poprawne zastąpienie,B
nie powinniśmy używać dziedziczenia.Możemy być zainteresowani kompozycją jako formą programowania obronnego do obrony klasy pochodnej . W szczególności, gdy zaczniesz używać
B
do innych celów, może pojawić się presja zmiany lub rozszerzenia, aby była bardziej odpowiednia do tych celów. Jeśli istnieje ryzyko, żeB
może ujawnić metody, które mogą spowodować nieprawidłowy stanA
, powinniśmy użyć kompozycji zamiast dziedziczenia. Nawet jeśli jesteśmy autorami obuB
iA
martw się o to jedno, dlatego kompozycja ułatwia ponowne użycieB
.Możemy nawet argumentować, że jeśli istnieją funkcje,
B
któreA
nie są potrzebne (i nie wiemy, czy te funkcje mogą spowodować nieprawidłowy stanA
, zarówno w obecnej implementacji, jak iw przyszłości), dobrym pomysłem jest użycie kompozycji zamiast dziedziczenia.Kompozycja ma również tę zaletę, że umożliwia przełączanie implementacji i ułatwia kpiny.
Uwaga : istnieją sytuacje, w których chcemy zastosować kompozycję, mimo że podstawienie jest ważne. Archiwizujemy tę substytucyjność za pomocą interfejsów lub klas abstrakcyjnych (które można wykorzystać, gdy jest innym tematem), a następnie stosujemy kompozycję z zastrzykiem zależności rzeczywistej implementacji.
Wreszcie, oczywiście, istnieje argument, że powinniśmy używać kompozycji do obrony klasy nadrzędnej, ponieważ dziedziczenie przerywa enkapsulację klasy nadrzędnej:
- Wzorce projektowe: elementy oprogramowania obiektowego wielokrotnego użytku, Gang of Four
Cóż, to źle zaprojektowana klasa rodzica. Dlatego powinieneś:
- Skuteczna Java, Josh Bloch
Problem jo-jo
Innym przypadkiem, w którym pomaga kompozycja, jest problem jo-jo . Oto cytat z Wikipedii:
Możesz rozwiązać na przykład: Twoja klasa
C
nie będzie dziedziczyć po klasieB
. Zamiast tego twoja klasaC
będzie miała element typuA
, który może, ale nie musi, być obiektem typuB
. W ten sposób nie będziesz programować w oparciu o szczegóły implementacjiB
, ale wbrew umowie, którąA
oferuje interfejs (lub) .Przykłady liczników
Wiele ram preferuje dziedziczenie nad kompozycją (co jest przeciwieństwem tego, o co argumentowaliśmy). Deweloper może to zrobić, ponieważ włożył dużo pracy w swoją klasę podstawową, ponieważ zaimplementowanie jej z kompozycją zwiększyłoby rozmiar kodu klienta. Czasami wynika to z ograniczeń języka.
Na przykład struktura PHP ORM może tworzyć klasę podstawową, która używa magicznych metod, aby umożliwić pisanie kodu tak, jakby obiekt miał prawdziwe właściwości. Zamiast tego kod obsługiwany przez magiczne metody będzie przechodził do bazy danych, wyszukiwał określone pole (być może buforował je na przyszłe żądanie) i zwrócił. Wykonanie tego z kompozycją wymagałoby od klienta utworzenia właściwości dla każdego pola lub napisania jakiejś wersji kodu metod magicznych.
Dodatek : Istnieją inne sposoby na rozszerzenie obiektów ORM. Dlatego nie sądzę, aby w tej sprawie konieczne było dziedziczenie. To jest tańsze.
W innym przykładzie silnik gier wideo może utworzyć klasę podstawową, która korzysta z kodu natywnego w zależności od platformy docelowej do renderowania 3D i obsługi zdarzeń. Ten kod jest złożony i specyficzny dla platformy. Zajmowanie się tym kodem byłoby bardzo kosztowne i podatne na błędy, ponieważ jest to jeden z powodów korzystania z silnika.
Co więcej, bez części do renderowania 3D, tak działa wiele ram widgetów. Dzięki temu nie musisz się martwić obsługą komunikatów systemu operacyjnego… w wielu językach nie możesz napisać takiego kodu bez rodzimej obsługi. Co więcej, jeśli to zrobisz, zmniejszy to twoją przenośność. Zamiast tego, z dziedziczeniem, pod warunkiem, że programista nie złamie kompatybilności (za dużo); w przyszłości będziesz mógł łatwo przenieść swój kod na dowolne nowe platformy, które obsługują.
Ponadto należy wziąć pod uwagę, że wiele razy chcemy zastąpić tylko kilka metod i pozostawić wszystko inne z domyślnymi implementacjami. Gdybyśmy korzystali z kompozycji, musielibyśmy stworzyć wszystkie te metody, nawet jeśli tylko delegować do zawiniętego obiektu.
W tym argumencie istnieje punkt, w którym kompozycja może być gorsza pod względem łatwości konserwacji niż dziedziczenie (gdy klasa podstawowa jest zbyt złożona). Pamiętaj jednak, że możliwość dziedziczenia dziedziczenia może być gorsza niż w przypadku kompozycji (gdy drzewo dziedziczenia jest zbyt złożone), o czym mówię w przypadku problemu jo-jo.
W prezentowanych przykładach programiści rzadko zamierzają ponownie wykorzystywać kod wygenerowany przez dziedziczenie w innych projektach. Łagodzi to zmniejszoną możliwość ponownego użycia dziedziczenia zamiast kompozycji. Ponadto, korzystając z dziedziczenia, programiści frameworków mogą zapewnić wiele łatwego w użyciu i łatwego do odkrycia kodu.
Końcowe przemyślenia
Jak widać, kompozycja ma pewną przewagę nad dziedziczeniem w niektórych sytuacjach, nie zawsze. Ważne jest, aby wziąć pod uwagę kontekst i różne czynniki (takie jak możliwość ponownego użycia, łatwość konserwacji, testowalność itp.) W celu podjęcia decyzji. Powrót do pierwszego punktu: „Wolę kompozycję niż dziedziczenie” to po prostu dobra heurystyka.
Możesz także zauważyć, że wiele opisywanych przeze mnie sytuacji można w pewnym stopniu rozwiązać za pomocą Cech lub Mixin. Niestety, nie są to wspólne cechy na wspaniałej liście języków i zazwyczaj wiążą się z pewnym kosztem wydajności. Na szczęście ich popularne metody rozszerzeń kuzyna i domyślne implementacje łagodzą niektóre sytuacje.
Mam najnowszy post, w którym mówię o niektórych zaletach interfejsów, dlaczego potrzebujemy interfejsów między interfejsem użytkownika, biznesem i dostępem do danych w języku C # . Pomaga odsprzęgnąć i ułatwia ponowne użycie i testowanie, możesz być zainteresowany.
źródło
Zwykle główną ideą Composition-Over-Inheritance jest zapewnienie większej elastyczności projektowania i nie ograniczanie ilości kodu, który należy zmienić, aby propagować zmianę (co uważam za wątpliwe).
Przykład na wikipedii daje lepszy przykład siły jego podejścia:
Definiując podstawowy interfejs dla każdego „elementu kompozycji”, można łatwo stworzyć różne „obiekty złożone” o różnorodności, do której trudno byłoby dotrzeć tylko z dziedziczeniem.
źródło
Wiersz: „Wolę kompozycję niż dziedziczenie” jest niczym innym, jak przesuwaniem się we właściwym kierunku. To nie uchroni cię przed własną głupotą i da ci szansę na pogorszenie sytuacji. To dlatego, że tutaj jest dużo mocy.
Głównym powodem, dla którego trzeba to powiedzieć, jest to, że programiści są leniwi. Tak leniwi, że wybiorą każde rozwiązanie, które oznacza mniej pisania na klawiaturze. To główny punkt sprzedaży spadku. Dzisiaj piszę mniej, a jutro ktoś się pieprzy. Więc co, chcę iść do domu.
Następnego dnia szef nalega na użycie kompozycji. Nie wyjaśnia, dlaczego, ale nalega na to. Biorę przykład z tego, co dziedziczyłem, ujawniam interfejs identyczny z tym, co dziedziczyłem, i implementuję ten interfejs, delegując całą pracę na rzecz, którą dziedziczyłem. Wszystko polega na pisaniu bezmyślnie, co można było zrobić za pomocą narzędzia do refaktoryzacji i wydaje mi się bezcelowe, ale szef tego chciał, więc to robię.
Następnego dnia pytam, czy tego właśnie chciałem. Jak myślisz, co mówi szef?
O ile nie było potrzeby dynamicznej (w czasie wykonywania) zmiany tego, co zostało odziedziczone (patrz wzorzec stanu), była to ogromna strata czasu. Jak szef może to powiedzieć i nadal opowiadać się za kompozycją zamiast dziedziczenia?
Oprócz tego, że nie robi się nic, aby zapobiec złamaniu z powodu zmian sygnatur metody, całkowicie traci to największą zaletę kompozycji. W przeciwieństwie do dziedziczenia możesz stworzyć inny interfejs . Jeszcze jeden odpowiedni sposób, w jaki będzie on używany na tym poziomie. Wiesz, abstrakcja!
Istnieje kilka drobnych zalet automatycznego zastępowania dziedziczenia składem i delegowaniem (oraz pewne wady), ale utrzymywanie wyłączonego mózgu podczas tego procesu nie ma ogromnej szansy.
Pośrednictwo jest bardzo potężne. Używaj rozważnie.
źródło
Oto kolejny powód: w wielu językach OO (mam tu na myśli takie jak C ++, C # lub Java), nie ma możliwości zmiany klasy obiektu po jego utworzeniu.
Załóżmy, że tworzymy bazę danych pracowników. Mamy abstrakcyjną podstawową klasę pracownika i zaczynamy czerpać nowe klasy dla określonych ról - inżyniera, ochroniarza, kierowcę ciężarówki i tak dalej. Każda klasa ma specjalne zachowanie dla tej roli. Wszystko jest dobrze.
Pewnego dnia inżynier zostaje awansowany na kierownika inżynierii. Nie możesz przeklasyfikować istniejącego Inżyniera. Zamiast tego musisz napisać funkcję, która tworzy Menedżera inżynierii, kopiując wszystkie dane z Inżyniera, usuwa Inżyniera z bazy danych, a następnie dodaje nowego Menedżera inżynierii. I musisz napisać taką funkcję dla każdej możliwej zmiany roli.
Co gorsza, załóżmy, że kierowca ciężarówki korzysta z długoterminowego zwolnienia lekarskiego, a ochroniarz oferuje niepełne etaty dla kierowców ciężarówek, aby je wypełnić. Musisz teraz wymyślić dla nich nową klasę ochroniarzy i kierowców ciężarówek.
Ułatwia życie, tworząc konkretną klasę pracownika i traktując ją jako kontener, do którego można dodawać właściwości, takie jak rola stanowiska.
źródło
Dziedziczenie zapewnia dwie rzeczy:
1) Ponowne użycie kodu: Można to łatwo osiągnąć poprzez kompozycję. W rzeczywistości lepiej osiąga się go poprzez kompozycję, ponieważ lepiej zachowuje on kapsułkowanie.
2) Polimorfizm: Można tego dokonać poprzez „interfejsy” (klasy, w których wszystkie metody są czysto wirtualne).
Silnym argumentem przemawiającym za wykorzystaniem dziedziczenia niezwiązanego z interfejsem jest to, że wymagana liczba metod jest duża, a podklasa chce jedynie zmienić niewielki ich podzbiór. Zasadniczo jednak interfejs użytkownika powinien mieć bardzo małe wymagania, jeśli przestrzegasz zasady „pojedynczej odpowiedzialności” (powinieneś), więc takie przypadki powinny być rzadkie.
Posiadanie stylu kodowania wymagającego przesłonięcia wszystkich metod klasy nadrzędnej i tak nie pozwala uniknąć pisania dużej liczby metod tranzytowych, więc dlaczego nie użyć ponownie kodu poprzez kompozycję i uzyskać dodatkową korzyść z enkapsulacji? Dodatkowo, jeśli superklasa miałaby zostać rozszerzona poprzez dodanie innej metody, styl kodowania wymagałby dodania tej metody do wszystkich podklas (i istnieje duża szansa, że naruszymy wówczas „segregację interfejsu” ). Ten problem rośnie szybko, gdy zaczniesz mieć wiele poziomów dziedziczenia.
Wreszcie, w wielu językach testowanie jednostkowe obiektów opartych na „składzie” jest znacznie łatwiejsze niż testowanie jednostkowe obiektów opartych na „dziedziczeniu” poprzez zastąpienie obiektu składowego obiektem próbnym / testowym / obojętnym.
źródło
Nie.
Istnieje wiele powodów, dla których preferuje się kompozycję zamiast dziedziczenia. Nie ma jednej poprawnej odpowiedzi. Ale wrzucę do mieszanki kolejny powód, aby faworyzować kompozycję zamiast dziedziczenia:
Preferowanie składu zamiast dziedziczenia poprawia czytelność kodu , co jest bardzo ważne w przypadku pracy nad dużym projektem z wieloma osobami.
Oto, jak o tym myślę: kiedy czytam czyjś kod, jeśli widzę, że rozszerzył on klasę, zapytam, jakie zachowanie w klasie super zmieniają?
A potem przejrzę ich kod, szukając nadpisanej funkcji. Jeśli nie widzę żadnego, to klasyfikuję to jako niewłaściwe wykorzystanie dziedziczenia. Powinni byli zamiast tego użyć kompozycji, choćby po to, by uratować mnie (i każdą inną osobę w zespole) od 5 minut czytania ich kodu!
Oto konkretny przykład: w Javie
JFrame
klasa reprezentuje okno. Aby utworzyć okno, należy utworzyć instancję,JFrame
a następnie wywołać funkcje w tej instancji, aby dodać do niej elementy i je wyświetlić.Wiele samouczków (nawet oficjalnych) zaleca przedłużenie
JFrame
, abyś mógł traktować instancje swojej klasy jak instancjeJFrame
. Ale imho (i opinia wielu innych) jest takie, że jest to dość zły nawyk, aby się w to wpakować! Zamiast tego należy po prostu utworzyć instancjęJFrame
i wywołać funkcje w tej instancji. WJFrame
tym przypadku absolutnie nie ma potrzeby rozszerzania , ponieważ nie zmieniasz żadnego domyślnego zachowania klasy nadrzędnej.Z drugiej strony jednym ze sposobów wykonania niestandardowego malowania jest rozszerzenie
JPanel
klasy i zastąpieniepaintComponent()
funkcji. Jest to dobre zastosowanie dziedziczenia. Jeśli przeglądam twój kod i widzę, że rozszerzyłeśJPanel
i zastąpiłeś tępaintComponent()
funkcję, będę dokładnie wiedział, jakie zachowanie zmieniasz.Jeśli tylko piszesz kod samodzielnie, może być równie łatwe w użyciu dziedziczenie, jak w przypadku kompozycji. Udawaj, że jesteś w środowisku grupowym, a inni ludzie będą czytać Twój kod. Preferowanie składu zamiast dziedziczenia ułatwia czytanie kodu.
źródło
new
kluczowe daje jeden. W każdym razie dzięki za dyskusję.