W ciągu ostatnich kilku miesięcy mantra „sprzyjanie kompozycjom nad dziedziczeniem” wydaje się pojawiać znikąd i stać się niemal rodzajem memu w społeczności programistów. I za każdym razem, gdy to widzę, jestem trochę zdziwiony. To tak, jakby ktoś powiedział: „faworyzuj wiertarki nad młotami”. Z mojego doświadczenia wynika, że skład i dziedziczenie to dwa różne narzędzia o różnych przypadkach użycia, a traktowanie ich tak, jakby były one wymienne, a jedno z natury lepsze od drugiego, nie ma sensu.
Poza tym nigdy nie widzę prawdziwego wyjaśnienia, dlaczego dziedziczenie jest złe, a kompozycja dobra, co czyni mnie bardziej podejrzliwym. Czy należy to po prostu przyjmować na wiarę? Podstawienie i polimorfizm Liskowa mają dobrze znane, wyraźne korzyści, a IMO obejmuje cały sens używania programowania obiektowego i nikt nigdy nie wyjaśnia, dlaczego należy je odrzucić na korzyść kompozycji.
Czy ktoś wie, skąd pochodzi ta koncepcja i jakie jest jej uzasadnienie?
źródło
Odpowiedzi:
Chociaż wydaje mi się, że słyszałem dyskusje na temat składu vs dziedziczenia na długo przed GoF, nie mogę jednak wskazać konkretnego źródła. Może i tak to był Booch.
<rant>
Ach, ale jak wiele mantr, ta uległa degeneracji wzdłuż typowych linii:
Mem, pierwotnie przeznaczony do doprowadzenia n00bs do oświecenia, jest teraz używany jako kij do nieprzytomności.
Skład i dziedzictwo to bardzo różne rzeczy i nie należy ich mylić ze sobą. Chociaż prawdą jest, że kompozycję można wykorzystać do symulacji dziedziczenia z dużą ilością dodatkowej pracy , nie czyni to spadku obywatelem drugiej kategorii ani nie czyni kompozycji ulubionym synem. Fakt, że wiele n00bs próbuje użyć dziedziczenia jako skrótu, nie unieważnia mechanizmu, a prawie wszystkie n00bs uczą się na błędach i tym samym poprawiają.
Proszę POMYŚL o swoich projektach i zatrzymać strzykając slogany.
</rant>
źródło
Doświadczenie.
Tak jak mówisz, są narzędziami do różnych zadań, ale wyrażenie pojawiło się, ponieważ ludzie nie używali go w ten sposób.
Dziedziczenie jest przede wszystkim narzędziem polimorficznym, ale niektórzy, ku swojemu późniejszemu niebezpieczeństwu, próbują użyć go jako sposobu ponownego użycia / współdzielenia kodu. Uzasadnieniem jest „cóż, jeśli odziedziczę, wówczas wszystkie metody otrzymuję za darmo”, ale ignorując fakt, że te dwie klasy potencjalnie nie mają związku polimorficznego.
Po co więc faworyzować kompozycję nad dziedziczeniem - po prostu dlatego, że często relacja między klasami nie jest polimorficzna. Istnieje tylko po to, aby pomóc ludziom przypomnieć, aby nie szarpać kolanem przez dziedziczenie.
źródło
To nie jest nowy pomysł, uważam, że został wprowadzony do książki wzorców projektowych GoF, która została opublikowana w 1994 roku.
Główny problem z dziedziczeniem polega na tym, że jest to biała skrzynka. Z definicji musisz znać szczegóły implementacji klasy, którą dziedziczysz. Z drugiej strony w kompozycji zależy ci tylko na publicznym interfejsie tworzonej klasy.
Z książki GoF:
Artykuł w Wikipedii na temat książki GoF ma przyzwoite wprowadzenie.
źródło
Aby odpowiedzieć na część twojego pytania, uważam, że ta koncepcja pojawiła się po raz pierwszy w książce GOF Design Patterns: Elements of Reusable Object-Oriented Software , opublikowanej po raz pierwszy w 1994 roku. Fraza pojawia się na górze strony 20, we wstępie:
Wstępują do tego stwierdzenia poprzez krótkie porównanie dziedziczenia ze składem. Nie mówią „nigdy nie używaj dziedziczenia”.
źródło
„Kompozycja nad dziedziczeniem” to krótki (i pozornie wprowadzający w błąd) sposób powiedzenia „Gdy czujesz, że dane (lub zachowanie) klasy powinny zostać włączone do innej klasy, zawsze rozważ użycie kompozycji przed ślepym zastosowaniem dziedziczenia”.
Dlaczego to prawda? Ponieważ dziedziczenie tworzy ścisłe sprzężenie w czasie kompilacji między dwiema klasami. Natomiast kompozycja jest luźnym sprzężeniem, które umożliwia między innymi wyraźne rozdzielenie problemów, możliwość przełączania zależności w czasie wykonywania oraz łatwiejsze, bardziej izolowane testowanie zależności.
Oznacza to, że z dziedziczeniem należy obchodzić się ostrożnie, ponieważ wiąże się ono z kosztami, a nie z tym, że nie jest użyteczne. W rzeczywistości „Kompozycja nad dziedziczeniem” często kończy się na „Kompozycja + dziedziczenie nad dziedziczeniem”, ponieważ często chcesz, aby złożona zależność była abstrakcyjną nadklasą, a nie konkretną podklasą. Pozwala przełączać się między różnymi konkretnymi implementacjami zależności w czasie wykonywania.
Z tego powodu (między innymi) prawdopodobnie zobaczysz dziedziczenie używane częściej w postaci implementacji interfejsu lub klas abstrakcyjnych niż dziedziczenie waniliowe.
Przykładem (metaforycznym) może być:
„Mam klasę Snake i chcę uwzględnić jako część tej klasy, co dzieje się, gdy Snake gryzie. Byłbym kuszony, aby Snake odziedziczył klasę BiterAnimal, która ma metodę Bite () i zastąpił tę metodę, aby odzwierciedlić jadowity zgryz Ale Kompozycja nad Dziedziczeniem ostrzega mnie, że powinienem zamiast tego spróbować użyć kompozycji ... W moim przypadku może to przełożyć się na Węża posiadającego członka Bite. Klasa Bite może być abstrakcyjna (lub interfejs) z kilkoma podklasami. mi fajne rzeczy, takie jak posiadanie podklas VenomousBite i DryBite oraz możliwość zmiany zgryzu w tej samej instancji Snake'a, gdy wąż starzeje się. Dodatkowo obsługa wszystkich efektów Ugryzienia w osobnej klasie może pozwolić mi na ponowne użycie go w tej klasie Frost , ponieważ mróz gryzie, ale nie jest BiterAnimal i tak dalej ... ”
źródło
Kilka możliwych argumentów za składem:
Skład jest nieco bardziej
dziedziczony niezależnie od języka / frameworka. To, co wymusza / wymaga / włącza, będzie się różnić między językami pod względem tego, do czego ma dostęp podklasa / nadklasa i jakie mogą mieć wpływ na wydajność metody wirtualne itp. Skład jest dość prosty i wymaga bardzo mała obsługa języków, a zatem implementacje na różnych platformach / platformach mogą łatwiej współdzielić wzorce kompozycji.
Kompozycja jest bardzo prostym i dotykowym sposobem budowania obiektów
Dziedziczenie jest stosunkowo łatwe do zrozumienia, ale wciąż nie jest tak łatwe do wykazania w prawdziwym życiu. Wiele przedmiotów w prawdziwym życiu można podzielić na części i skomponować. Załóżmy, że rower można zbudować za pomocą dwóch kół, ramy, siedziska, łańcucha itp. Łatwo to wytłumaczyć składem. Podczas gdy w metaforze dziedziczenia można powiedzieć, że rower rozwija monocykl, co jest wykonalne, ale wciąż znacznie dalej od rzeczywistego obrazu niż kompozycja (oczywiście nie jest to bardzo dobry przykład dziedziczenia, ale kwestia pozostaje ta sama). Nawet słowo dziedziczenie (przynajmniej większości amerykańskich użytkowników języka angielskiego, którego się spodziewałbym) automatycznie przywołuje znaczenie w stylu „Coś, co przeszło od zmarłego krewnego”, które ma pewną korelację z jego znaczeniem w oprogramowaniu, ale nadal jest luźne.
Kompozycja jest prawie zawsze bardziej elastyczna
Za pomocą kompozycji możesz zawsze zdefiniować własne zachowanie lub po prostu odsłonić tę część skomponowanych części. W ten sposób nie napotkasz żadnych ograniczeń, które mogą zostać nałożone przez hierarchię dziedziczenia (wirtualną vs. niewirtualną itp.)
Być może dlatego, że Kompozycja jest naturalnie prostszą metaforą, która ma mniej ograniczeń teoretycznych niż dziedziczenie. Co więcej, te szczególne powody mogą być bardziej widoczne w czasie projektowania lub ewentualnie wystają, gdy mamy do czynienia z niektórymi bolącymi punktami dziedziczenia.
Zastrzeżenie:
Oczywiście nie jest to ta jednoznaczna ulica / droga jednokierunkowa. Każdy projekt wymaga oceny kilku wzorów / narzędzi. Dziedziczenie jest szeroko stosowane, ma wiele zalet i wiele razy jest bardziej eleganckie niż kompozycja. To tylko niektóre z możliwych powodów, które można wykorzystać, faworyzując kompozycję.
źródło
Być może właśnie zauważyłeś ludzi, którzy to mówili w ciągu ostatnich kilku miesięcy, ale dobrzy programiści znali to znacznie dłużej. Z pewnością mówiłem to w stosownych przypadkach przez około dekadę.
Chodzi o to, że dziedziczenie wiąże się z dużym pojęciowym kosztem. Gdy używasz dziedziczenia, każde wywołanie metody ma w sobie niejawne przesłanie. Jeśli masz drzewa o głębokim dziedzictwie, wielokrotną wysyłkę lub (co gorsza) jedno i drugie, to zastanawianie się, dokąd konkretna metoda zostanie wysłana w danym wezwaniu, może stać się królewską PITA. To sprawia, że poprawne rozumowanie na temat kodu jest bardziej złożone i utrudnia debugowanie.
Dam prosty przykład do zilustrowania. Załóżmy, że głęboko w drzewie spadkowym ktoś nazwał metodę
foo
. Potem pojawia się ktoś inny i dodajefoo
na szczycie drzewa, ale robi coś innego. (Ten przypadek jest bardziej powszechny w przypadku wielokrotnego dziedziczenia.) Teraz osoba pracująca w klasie root złamała niejasną klasę potomną i prawdopodobnie nie zdaje sobie z tego sprawy. Możesz mieć 100% pokrycia testami jednostkowymi i nie zauważyć tego złamania, ponieważ osoba na szczycie nie pomyślałaby o przetestowaniu klasy podrzędnej, a testy dla klasy podrzędnej nie pomyślą o przetestowaniu nowych metod utworzonych u góry . (Wprawdzie istnieją sposoby na napisanie testów jednostkowych, które to wychwycą, ale są też przypadki, w których nie można łatwo napisać testów w ten sposób.)W przeciwieństwie do tego, gdy używasz kompozycji, przy każdym połączeniu jest zwykle wyraźniejsze, do czego wysyłasz połączenie. (OK, jeśli używasz odwrócenia kontroli, na przykład z wstrzykiwaniem zależności, wtedy zastanawianie się, dokąd idzie połączenie, może być również problematyczne. Ale zwykle łatwiej jest to rozgryźć.) Ułatwia to rozumowanie. Jako bonus, kompozycja powoduje oddzielenie metod od siebie. Powyższy przykład nie powinien się tam zdarzyć, ponieważ klasa potomna przeniósłaby się do jakiegoś niejasnego komponentu i nigdy nie ma pytania, czy wywołanie to
foo
było przeznaczone dla niejasnego komponentu, czy głównego obiektu.Teraz masz całkowitą rację, że dziedziczenie i kompozycja to dwa bardzo różne narzędzia, które służą dwóm różnym rodzajom rzeczy. Jasne, że dziedziczenie niesie ze sobą koncepcyjne koszty, ale gdy jest to właściwe narzędzie do pracy, niesie ze sobą mniej pojęć koncepcyjnych niż próba nieużywania go i robienia ręcznie tego, co robi dla ciebie. Nikt, kto wie, co robią, nie powiedziałby, że nigdy nie powinieneś używać dziedziczenia. Ale upewnij się, że to jest właściwe.
Niestety, wielu programistów dowiaduje się o oprogramowaniu obiektowym, uczy się dziedziczenia, a następnie jak najczęściej używa swojego nowego topora. Co oznacza, że próbują użyć dziedziczenia, w którym kompozycja była właściwym narzędziem. Mam nadzieję, że nauczą się lepiej z czasem, ale często dzieje się tak dopiero po kilku usuniętych kończynach itp. Mówienie im z przodu, że to zły pomysł, przyspiesza proces uczenia się i zmniejsza liczbę kontuzji.
źródło
Jest to reakcja na spostrzeżenie, że początkujący OO mają tendencję do korzystania z dziedziczenia, kiedy nie muszą. Dziedziczenie z pewnością nie jest złą rzeczą, ale może być nadużywane. Jeśli jedna klasa potrzebuje tylko funkcji od innej, to prawdopodobnie kompozycja będzie działać. W innych okolicznościach dziedziczenie będzie działać, a kompozycja nie.
Dziedziczenie z klasy implikuje wiele rzeczy. Sugeruje to, że Pochodna jest rodzajem Bazy (szczegóły dotyczące krwawych szczegółów można znaleźć w zasadzie Zastępstwa Liskowa), w tym sensie, że za każdym razem, gdy używasz Bazy, rozsądnie byłoby użyć Pochodnej. Daje pochodnemu dostęp do chronionych członków i funkcji członka Base. Jest to ścisła relacja, co oznacza, że ma wysoki stopień sprzężenia, a zmiany w jednym są bardziej prawdopodobne, że wymagają zmian w drugim.
Sprzęganie jest złą rzeczą. Utrudnia to zrozumienie i modyfikację programów. Jeśli inne rzeczy są takie same, zawsze należy wybrać opcję z mniejszym sprzężeniem.
Dlatego jeśli kompozycja lub dziedziczenie wykona zadanie skutecznie, wybierz kompozycję, ponieważ jest to niższe sprzężenie. Jeśli kompozycja nie wykona zadania skutecznie, a dziedziczenie zrobi, wybierz dziedziczenie, ponieważ musisz.
źródło
Oto moje dwa centy (poza wszystkimi już podniesionymi doskonałymi punktami):
IMHO, sprowadza się to do tego, że większość programistów tak naprawdę nie dostaje dziedziczenia i ostatecznie przesadza, częściowo w wyniku tego, jak naucza się tej koncepcji. Ta koncepcja istnieje jako sposób na zniechęcenie ich do przesadzania, zamiast skupiania się na uczeniu ich, jak robić to dobrze.
Każdy, kto spędził czas na nauczaniu lub mentoringu, wie, że tak się często dzieje, szczególnie w przypadku nowych programistów, którzy mają doświadczenie w innych paradygmatach:
Ci programiści początkowo uważają, że dziedziczenie jest tą przerażającą koncepcją. W rezultacie tworzą typy z nakładającymi się interfejsami (np. Takie same określone zachowanie bez wspólnego podtytułu) i globale do implementowania typowych funkcji.
Następnie (często w wyniku nadgorliwego nauczania) dowiadują się o korzyściach z dziedziczenia, ale często uczy się go jako uniwersalne rozwiązanie do ponownego użycia. W rezultacie powstaje przekonanie, że wszelkie wspólne zachowania muszą być dzielone przez dziedziczenie. Wynika to z faktu, że często nacisk kładziony jest na dziedziczenie implementacji, a nie na podtyp.
W 80% przypadków jest to wystarczająco dobre. Ale pozostałe 20% jest tam, gdzie zaczyna się problem. Aby uniknąć przepisywania i upewnić się, że skorzystali z udostępniania współdzielonego, zaczynają projektować swoją hierarchię wokół zamierzonej implementacji, a nie bazowych abstrakcji. „Stos dziedziczy z podwójnie połączonej listy” jest tego dobrym przykładem.
Większość nauczycieli nie nalega na wprowadzenie koncepcji interfejsów w tym momencie, ponieważ jest to albo inna koncepcja, albo dlatego, że (w C ++) musisz sfałszować je za pomocą klas abstrakcyjnych i wielokrotnego dziedziczenia, których nie uczy się na tym etapie. W Javie wielu nauczycieli nie odróżnia „bez wielokrotnego dziedziczenia” lub „wielokrotne dziedziczenie jest złe” od znaczenia interfejsów.
Wszystko to komplikuje fakt, że tak wiele nauczyliśmy się o pięknie niepotrzebnego pisania zbędnego kodu z dziedziczeniem implementacji, że mnóstwo prostego kodu delegacji wygląda nienaturalnie. Tak więc kompozycja wygląda na bałagan, dlatego potrzebujemy tych zasad, aby zachęcić nowych programistów, aby i tak z nich skorzystali (co też przesadzają).
źródło
W jednym z komentarzy Mason wspomniał, że pewnego dnia będziemy mówić o Dziedzictwie uważanym za szkodliwe .
Mam nadzieję.
Problem z dziedziczeniem jest jednocześnie prosty i zabójczy, nie uwzględnia idei, że jedna koncepcja powinna mieć jedną funkcjonalność.
W większości języków OO dziedzicząc po klasie podstawowej:
I tu stają się kłopoty.
To nie jest jedyne podejście, chociaż 00 języków jest w większości utkniętych z tym. Na szczęście istnieją interfejsy / klasy abstrakcyjne.
Poza tym brak łatwości działania w inny sposób przyczynia się do tego, że jest on w dużej mierze używany: szczerze mówiąc, nawet wiedząc o tym, czy odziedziczyłbyś po interfejsie i osadziłby klasę podstawową poprzez kompozycję, delegując większość wywołań metod? Byłoby znacznie lepiej, byłbyś nawet ostrzeżony, gdyby nagle pojawiła się nowa metoda w interfejsie i musiałbyś świadomie wybrać sposób jej wdrożenia.
Jako kontrapunkt
Haskell
zezwalaj na stosowanie zasady Liskowa tylko wtedy, gdy „czerpiesz” z czystych interfejsów (zwanych pojęciami) (1). Nie możesz wywodzić się z istniejącej klasy, tylko skład pozwala osadzić jej dane.(1) koncepcje mogą stanowić sensowne ustawienie domyślne dla implementacji, ale ponieważ nie zawierają one danych, to ustawienie domyślne można zdefiniować tylko za pomocą innych metod zaproponowanych przez pojęcie lub za pomocą stałych.
źródło
Prosta odpowiedź brzmi: dziedziczenie ma większe sprzężenie niż skład. Biorąc pod uwagę dwie opcje, w przeciwnym razie równoważne cechy, wybierz tę, która jest mniej sprzężona.
źródło
Myślę, że taka rada jest jak powiedzenie „wolę jazdę niż latanie”. Oznacza to, że samoloty mają wiele zalet w stosunku do samochodów, ale ma to pewną złożoność. Więc jeśli wiele osób spróbuje latać z centrum miasta na przedmieścia, tak naprawdę, naprawdę, naprawdę muszą usłyszeć, że nie muszą latać, a latanie tylko skomplikuje je na dłuższą metę, nawet jeśli wydaje się fajny / wydajny / łatwy w krótkim okresie. Natomiast gdy nie trzeba lecieć, to generalnie powinno być oczywiste.
Podobnie, dziedziczenie może sprawić, że kompozycja nie może tego zrobić, ale powinieneś go używać, kiedy jest to potrzebne, a nie wtedy, gdy tego nie potrzebujesz. Jeśli więc nigdy nie masz ochoty zakładać, że potrzebujesz dziedzictwa, a nie potrzebujesz porady „preferuj kompozycję”. Ale wielu ludzi zrobić , a zrobić trzeba, że porady.
Powinno być domyślnym, że kiedy naprawdę potrzebujesz dziedziczenia, jest to oczywiste i powinieneś z niego skorzystać.
Również odpowiedź Stevena Lowe'a. Naprawdę, naprawdę to.
źródło
Dziedziczenie nie jest z natury złe, a kompozycja z natury nie jest dobra. Są to tylko narzędzia, których programista OO może używać do projektowania oprogramowania.
Kiedy patrzysz na klasę, czy robi ona więcej niż absolutnie powinna ( SRP )? Czy niepotrzebnie powiela funkcjonalność ( OSUSZANIE ), czy też jest nadmiernie zainteresowany właściwościami lub metodami innych klas ( Zazdroszczenie funkcji ) ?. Jeśli klasa narusza wszystkie te pojęcia (i być może więcej), to stara się być klasą boską . Jest to szereg problemów, które mogą wystąpić podczas projektowania oprogramowania, z których żaden niekoniecznie jest problemem dziedziczenia, ale który może szybko powodować poważne bóle głowy i kruche zależności, w których zastosowano również polimorfizm.
Przyczyną problemu może być w mniejszym stopniu niezrozumienie dziedziczenia, a bardziej zły wybór pod względem projektu lub być może brak rozpoznawania „zapachów kodu” związanych z klasami, które nie przestrzegają zasady pojedynczej odpowiedzialności . Polimorfizm i podstawienie Liskowa nie muszą być odrzucane na korzyść składu. Sam polimorfizm można zastosować bez polegania na dziedziczeniu, wszystkie te pojęcia są dość komplementarne. Jeśli zostanie zastosowany w zamyśleniu. Sztuczka polega na tym, aby twój kod był prosty i przejrzysty, a nie ulegać nadmiernej trosce o liczbę klas, które musisz utworzyć, aby stworzyć solidny projekt systemu.
Jeśli chodzi o kwestię faworyzowania kompozycji nad dziedziczeniem, jest to tak naprawdę kolejny przypadek rozważnego zastosowania elementów projektu, które są najbardziej sensowne z punktu widzenia rozwiązania problemu. Jeśli nie musisz odziedziczyć zachowania, prawdopodobnie nie powinieneś, ponieważ kompozycja pomoże uniknąć niezgodności i późniejszych poważnych działań związanych z refaktoryzacją. Jeśli z drugiej strony okaże się, że powtarzasz dużo kodu, tak że cała duplikacja jest skoncentrowana na grupie podobnych klas, może być tak, że utworzenie wspólnego przodka pomogłoby zmniejszyć liczbę identycznych wywołań i klas może być konieczne powtarzanie między poszczególnymi klasami. Dlatego faworyzujesz kompozycję, ale nie zakładasz, że dziedziczenie nigdy nie ma zastosowania.
źródło
Prawdziwy cytat pochodzi, jak sądzę, z „Skutecznej Jawy” Joshua Blocha, gdzie jest to tytuł jednego z rozdziałów. Twierdzi, że dziedziczenie jest bezpieczne w pakiecie i wszędzie tam, gdzie klasa została specjalnie zaprojektowana i udokumentowana do rozszerzenia. Twierdzi, jak zauważyli inni, że dziedziczenie przerywa enkapsulację, ponieważ podklasa zależy od szczegółów implementacyjnych jej nadklasy. Prowadzi to, jak mówi, do kruchości.
Chociaż nie wspomina o tym, wydaje mi się, że pojedyncze dziedziczenie w Javie oznacza, że kompozycja daje znacznie większą elastyczność niż dziedziczenie.
źródło