„Wolę kompozycję niż dziedziczenie” - Czy to jedyny powód, aby bronić się przed zmianami podpisu?


13

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?



1
zobacz także: Omów ten $ {blog}
gnat

2
Cytat nie mówi, że to „jedyny” powód.
Tulains Córdova

3
Kompozycja jest prawie zawsze lepsza, ponieważ dziedziczenie generalnie zwiększa złożoność, sprzężenie, czytanie kodu i zmniejsza elastyczność. Ale są godne uwagi wyjątki, czasem klasa podstawowa lub dwie nie bolą. Dlatego „ woli ”, a nie „ zawsze ”.
Mark Rogers

Odpowiedzi:


27

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


6
Było wiele telewizorów z magnetowidami i odtwarzaczami DVD wbudowanych na przełomie lat 90. i 2000.
JAB

@JAB i wiele (większość?) Sprzedawanych obecnie telewizorów obsługuje transmisję strumieniową.
Casey

4
@JAB I żaden z nich nie może sprzedać swojego odtwarzacza DVD, aby pomóc w zakupie odtwarzacza Bluray
Alexander - Przywróć Monikę

1
@JAB Right. Stwierdzenie „Czy widziałeś kiedyś telewizor z wbudowanym odtwarzaczem VCR?” sugeruje, że istnieją.
JimmyJames

4
@Casey Ponieważ oczywiście nigdy nie będzie innej technologii, która zastąpi tę, prawda? Założę się, że któryś z tych telewizorów obsługuje również wejścia z urządzeń zewnętrznych na wypadek, gdyby ktoś chciał użyć czegoś innego bez kupowania nowego telewizora. A raczej niewielu wykształconych nabywców jest skłonnych kupić telewizor bez takich materiałów. Moim założeniem nie jest to, że takie podejścia nie istnieją i nie twierdzę, że dziedziczenie nie istnieje. Chodzi o to, że takie podejścia są z natury mniej elastyczne niż kompozycja.
JimmyJames

4

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.


Widzę tutaj mentalność Java. OOP polega na pakowaniu danych wraz z działającymi na nie funkcjami - to, co opisujesz, lepiej określa się mianem „modułów”. Masz rację, że unikanie dziedziczenia jest dobrym pomysłem, gdy zmieniasz przeznaczenie obiektów jako moduły, ale wydaje mi się niepokojące, że wydajesz się polecać to jako preferowany sposób używania obiektów.
Brilliand

1
@Brilliand Gdybyś tylko wiedział, ile kodu java jest napisane w tym stylu, jak opisujesz i ile to jest horroru ... Smalltalk wprowadził termin OOP i został zaprojektowany wokół obiektów odbierających wiadomości. : „Przetwarzanie danych należy postrzegać jako nieodłączną właściwość obiektów, które można jednolicie wywoływać przez wysyłanie wiadomości”. Nie ma wzmianki o danych i funkcjach na nim działających. Przepraszam, ale moim zdaniem poważnie się mylisz. Gdyby chodziło tylko o dane i funkcje, można z radością kontynuować pisanie struktur.
marstato

@ Tsar niestety, mimo że Java obsługuje metody statyczne, kultura Java nie
k_g

@Brilliand Kogo obchodzi, czym jest „OOP”? Liczy się to, w jaki sposób można go oraz wyniki używając go w tych sposobów użycia.
user253751

1
Po dyskusji z @Tsar: klasy Java nie muszą być tworzone, a klasy takie jak UserRegistrationService i EmailService zwykle nie powinny być tworzone - klasy bez powiązanych danych działają najlepiej jak klasy statyczne (i jako takie są nieistotne dla oryginału pytanie). Skasuję niektóre z moich wcześniejszych komentarzy, ale moja pierwotna krytyka odpowiedzi Marsato trwa.
Brilli i

4

„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 Vehicleklasę, a następnie wyprowadzić Car, Airplane, Ballooni Ship.

Uwaga : jeśli musisz uziemić ten przykład, udawaj, że są to rodzaje obiektów w grze wideo.

Wtedy Cari Airplanemoż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ędzy Airplanei Balloon. 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ą FlyingVehiculeklasę. Mówiliby, że Airplanejest to FlyingVehicule(dziedziczenie klas), ale moglibyśmy zamiast tego powiedzieć, że Airplanema Flyingskładnik (skład) i Airplanejest IFlyingVehicule(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 Adziedziczyć czy komponować B?

Jeśli Aspecjalizacja Btego typu powinna spełniać zasadę podstawienia Liskowa , dziedziczenie jest opłacalne, a nawet pożądane. Jeśli zdarzają się sytuacje, w których Anie jest to poprawne zastąpienie, Bnie 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ć Bdo innych celów, może pojawić się presja zmiany lub rozszerzenia, aby była bardziej odpowiednia do tych celów. Jeśli istnieje ryzyko, że Bmoże ujawnić metody, które mogą spowodować nieprawidłowy stan A, powinniśmy użyć kompozycji zamiast dziedziczenia. Nawet jeśli jesteśmy autorami obu Bi Amartw się o to jedno, dlatego kompozycja ułatwia ponowne użycie B.

Możemy nawet argumentować, że jeśli istnieją funkcje, Bktóre Anie są potrzebne (i nie wiemy, czy te funkcje mogą spowodować nieprawidłowy stan A, 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:

Dziedziczenie naraża podklasę na szczegóły implementacji jej rodzica, często mówi się, że „dziedziczenie przerywa enkapsulację”

- Wzorce projektowe: elementy oprogramowania obiektowego wielokrotnego użytku, Gang of Four

Cóż, to źle zaprojektowana klasa rodzica. Dlatego powinieneś:

Projektuj dziedziczenie lub zabraniaj go.

- Skuteczna Java, Josh Bloch


Problem jo-jo

Innym przypadkiem, w którym pomaga kompozycja, jest problem jo-jo . Oto cytat z Wikipedii:

Podczas opracowywania oprogramowania problem jo-jo to anty-wzorzec, który pojawia się, gdy programista musi czytać i rozumieć program, którego wykres dziedziczenia jest tak długi i skomplikowany, że programista musi ciągle przerzucać między różnymi definicjami klas w celu śledzenia przepływ kontrolny programu.

Możesz rozwiązać na przykład: Twoja klasa Cnie będzie dziedziczyć po klasie B. Zamiast tego twoja klasa Cbędzie miała element typu A, który może, ale nie musi, być obiektem typu B. W ten sposób nie będziesz programować w oparciu o szczegóły implementacji B, ale wbrew umowie, którą Aoferuje 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.


Uh ... .NET Framework używa kilku rodzajów odziedziczonych strumieni, aby wykonać swoją pracę związaną ze strumieniem. Działa niesamowicie dobrze i jest bardzo łatwy w użyciu i rozszerzaniu. Twój przykład nie jest zbyt dobry ...
T. Sar

1
@TSar ”jest bardzo łatwy w użyciu i rozszerza” Czy próbowałeś kiedyś udostępnić standardowy strumień wyjściowy do debugowania? To było moje jedyne doświadczenie próbujące rozszerzyć ich strumienie. To nie było proste. To był koszmar, który skończył się tym, że wyraźnie zastąpiłem każdą metodę w klasie. Niezwykle powtarzalne. A jeśli spojrzysz na kod źródłowy .NET, jawne przesłonięcie wszystkiego jest prawie tak, jak oni to robią. Nie jestem więc przekonany, że przedłużenie jest tak łatwe.
jpmc26

Odczytywanie i zapisywanie struktury ze strumienia lepiej jest wykonywać za pomocą bezpłatnych funkcji lub multimetod.
user253751

@ jpmc26 Przykro mi, że twoje doświadczenie nie było zbyt dobre. Jednak nie miałem żadnych problemów z używaniem lub wdrażaniem rzeczy związanych ze strumieniem do różnych rzeczy.
T. Sar

3

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.


Więc ta sekcja daje Przykład kompozycji, prawda? Pytam, ponieważ nie jest to oczywiste w tym artykule.
Utku

1
Tak, „Kompozycja nad dziedziczeniem” nie oznacza, że ​​nie masz interfejsów, jeśli spojrzysz na obiekt Player, możesz zobaczyć, że dobrze pasuje do hierarchii, ale przy (przepraszam, że brutalność) kod nie pochodzi z dziedziczenia, ale z kompozycji z jakąś klasą, która działa
lbenini

2

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.


0

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.


Lub tworzysz klasę pracownika ze wskaźnikiem do listy ról, a każde rzeczywiste zadanie rozszerza rolę. Twój przykład może być wykorzystany do dziedziczenia w bardzo dobry i rozsądny sposób, bez zbytniej pracy.
T. Sar

0

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.


0

Czy to naprawdę jedyny powód, aby preferować kompozycję nad dziedziczeniem?

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 JFrameklasa reprezentuje okno. Aby utworzyć okno, należy utworzyć instancję, JFramea 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 instancje JFrame. 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ę JFramei wywołać funkcje w tej instancji. W JFrametym 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 JPanelklasy i zastąpienie paintComponent()funkcji. Jest to dobre zastosowanie dziedziczenia. Jeśli przeglądam twój kod i widzę, że rozszerzyłeś JPaneli 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.


Dodanie całej klasy fabrycznej za pomocą jednej metody, która zwraca instancję obiektu, dzięki czemu można użyć kompozycji zamiast dziedziczenia, tak naprawdę nie czyni kodu bardziej czytelnym ... (Tak, potrzebujesz tego, jeśli potrzebujesz nowej instancji za każdym razem wywoływana jest określona metoda.) Nie oznacza to, że dziedziczenie jest dobre, ale kompozycja nie zawsze poprawia czytelność.
jpmc26

1
@ jpmc26 ... dlaczego, do licha, miałbyś potrzebować klasy fabrycznej, aby stworzyć nową instancję klasy? W jaki sposób dziedziczenie poprawia czytelność tego, co opisujesz?
Kevin Workman

Czy w powyższym komentarzu nie podałem wprost powodu takiej fabryki? Jest bardziej czytelny, ponieważ powoduje mniej kodu do odczytania . To samo zachowanie można osiągnąć za pomocą jednej metody abstrakcyjnej w klasie podstawowej i kilku podklas. (W każdym razie zamierzałeś mieć inną implementację swojej klasy złożonej dla każdej z nich.) Jeśli szczegóły budowy obiektu są silnie powiązane z klasą podstawową, nie znajdzie większego zastosowania w innym miejscu w kodzie, co dodatkowo zmniejszy korzyści wynikające z utworzenia oddzielnej klasy. Następnie utrzymuje blisko spokrewnione części kodu blisko siebie.
jpmc26

Jak powiedziałem, niekoniecznie oznacza to, że dziedziczenie jest dobre, ale ilustruje, w jaki sposób kompozycja nie poprawia czytelności w niektórych sytuacjach.
jpmc26

1
Bez konkretnego przykładu trudno jest naprawdę skomentować. Ale to, co opisałeś, brzmi dla mnie jak nadmierna inżynieria. Potrzebujesz instancji klasy? Słowo newkluczowe daje jeden. W każdym razie dzięki za dyskusję.
Kevin Workman
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.