Skąd bierze się ta koncepcja „faworyzowania kompozycji nad dziedziczeniem”?


143

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?


45
Jak zauważyli inni, minęło już sporo czasu - jestem zaskoczony, że właśnie o tym słyszysz. Jest intuicyjny dla każdego, kto buduje duże systemy w językach takich jak Java od dłuższego czasu. Jest to podstawa każdego wywiadu, jaki kiedykolwiek udzielam, a kiedy kandydat zaczyna mówić o dziedziczeniu, zaczynam wątpić w jego umiejętności i doświadczenie. Oto dobre wprowadzenie do tego, dlaczego dziedziczenie jest kruchym rozwiązaniem (istnieje wiele innych): artima.com/lejava/articles/designprinciples4.html
Janx

6
@Janx: Może to jest to. Nie buduję dużych systemów w językach takich jak Java; Buduję je w Delfach i bez substytucji Liskowa i polimorfizmu nigdy byśmy nic nie zrobili. Jego model obiektowy różni się pod pewnymi względami od języka Java lub C ++, a wiele problemów, które ta maksyma wydaje się rozwiązać, tak naprawdę nie istnieje lub jest znacznie mniej problematycznych w Delphi. Chyba różne perspektywy z różnych punktów widzenia.
Mason Wheeler,

14
Spędziłem kilka lat w zespole budującym stosunkowo duże systemy w Delfach, a wysokie drzewa spadkowe z pewnością ugryzły nasz zespół i spowodowały znaczny ból. Podejrzewam, że twoja uwaga na zasadach SOLID pomaga ci uniknąć problemów, a nie korzystania z Delphi.
Bevan,

8
Ostatnie kilka miesięcy?!?
Jas

6
IMHO ta koncepcja nigdy nie była w pełni dostosowana do różnych języków, które obsługują zarówno dziedziczenie interfejsu (tj. Podtypy z czystymi interfejsami), jak i dziedziczenie implementacji. Zbyt wiele osób podąża za tą mantrą i nie używa wystarczającej liczby interfejsów.
Uri

Odpowiedzi:


136

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:

  1. został wprowadzony ze szczegółowym wyjaśnieniem i argumentem przez szanowane źródło, które tworzy frazę jako przypomnienie oryginalnej złożonej dyskusji
  2. jest dzielony z pewnym mrugnięciem części klubu przez kilka osób, które znają się na chwilę, zazwyczaj podczas komentowania błędów n00b
  3. wkrótce powtarza się bezmyślnie przez tysiące ludzi, którzy nigdy nie czytają wyjaśnienia, ale uwielbiają je wykorzystywać jako wymówkę, by nie myśleć , oraz jako tani i łatwy sposób, aby czuć się lepszym od innych
  4. ostatecznie żadna ilość rozsądnego obalenia nie może powstrzymać fali „memów” - i paradygmat przekształca się w religię i dogmat.

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>


16
Booch twierdzi, że dziedziczenie implementacji wprowadza wysokie sprzężenie między klasami. Z drugiej strony uważa dziedziczenie za kluczową koncepcję odróżniającą OOP od programowania proceduralnego.
Nemanja Trifunovic

13
Na takie rzeczy powinno być słowo. Może przedwczesna niedomykalność ?
Jörg W Mittag,

8
@ Jörg: Wiesz, co stanie się z terminem takim jak Przedwczesna niedomykalność? Dokładnie to, co wyjaśniono powyżej. :) (Btw. Kiedy regurgitacja nigdy nie jest przedwczesna?)
Bjarke Freund-Hansen

6
@Nemanja: Racja. Pytanie brzmi, czy to połączenie jest naprawdę złe. Jeśli klasy są silnie sprzężone koncepcyjnie i koncepcyjnie tworzą relację nadtyp-podtyp i nigdy nie mogłyby zostać rozdzielone pojęciowo, nawet jeśli są formalnie rozdzielone na poziomie językowym, silne sprzężenie jest w porządku.
dsimcha

5
Mantra jest prosta: „tylko dlatego, że możesz kształtować play-doh tak, aby wyglądał jak coś, nie oznacza, że ​​powinieneś robić wszystko z play-doh”. ;)
Evan Plaice,

73

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.


7
Zasadniczo nie można więc powiedzieć: „nie powinieneś używać OOP, jeśli nie rozumiesz podstawienia Liskowa”, więc mówisz „faworyzuj kompozycję zamiast dziedziczenia” jako próbę ograniczenia szkód wyrządzonych przez niekompetentne kodery?
Mason Wheeler,

13
@Mason: Jak każda mantra, „faworyzowanie kompozycji nad dziedziczeniem” jest skierowane do początkujących programistów. Jeśli już wiesz, kiedy stosować dziedziczenie, a kiedy kompozycję, nie ma sensu powtarzać takiej mantry.
Dean Harding

7
@Dean - Nie jestem pewien, czy początkujący jest tym samym, który nie rozumie najlepszych praktyk dotyczących dziedziczenia. Myślę, że jest w tym coś więcej. Złe dziedzictwo jest przyczyną wielu, wielu problemów w mojej pracy i nie jest to kod napisany przez programistów, którzy byliby uważani za „początkujących”.
Nicole,

6
@Reneesis: Pierwszą rzeczą, o której myślę, kiedy to słyszę, jest: „niektórzy ludzie mają dziesięć lat doświadczenia, a niektórzy mają rok doświadczenia powtarzany dziesięć razy”.
Mason Wheeler,

8
Projektowałem dość duży projekt zaraz po pierwszym OO i mocno na nim polegałem. Mimo, że działał dobrze, ciągle uważałem, że projekt jest kruchy - powodując drobne podrażnienia tu i tam, a czasem czyniąc niektórych refaktorów kompletną suką. Trudno jest wyjaśnić całe doświadczenie, ale wyrażenie „Faworyzuj Kompresję nad Dziedzictwem” dokładnie to opisuje. Zauważ, że nie mówi ani nawet nie sugeruje „Unikaj dziedziczenia”, po prostu daje ci trochę troski, gdy wybór nie jest oczywisty.
Bill K

43

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:

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

Artykuł w Wikipedii na temat książki GoF ma przyzwoite wprowadzenie.


14
Nie zgadzam się z tym. Nie musisz znać szczegółów implementacji dziedziczonej klasy; tylko członkowie publiczni i chronieni narażeni przez klasę. Jeśli musisz znać szczegóły implementacji, to albo ty, albo ktokolwiek napisałeś klasę podstawową, robi coś złego, a jeśli wada dotyczy klasy podstawowej, kompozycja nie pomoże ci naprawić / obejść tego problemu.
Mason Wheeler,

18
Jak możesz nie zgodzić się z czymś, czego nie przeczytałeś? Jest solidna strona i połowa dyskusji z GoF, na którą dostajesz tylko mały widok.
Philodad

7
@ pholosodad: Nie zgadzam się z czymś, czego nie przeczytałem. Nie zgadzam się z tym, co napisał Dean, że „Z definicji musisz znać szczegóły implementacji klasy, którą dziedziczysz”, którą przeczytałem.
Mason Wheeler,

10
To, co napisałem, to tylko podsumowanie tego, co opisano w książce GoF. Być może sformułowałem to nieco mocno (nie musisz znać wszystkich szczegółów implementacji), ale to jest ogólny powód, dla którego GoF mówi, że preferuje kompozycję nad dziedziczeniem.
Dean Harding,

2
Popraw mnie, jeśli się mylę, ale widzę to jako: „faworyzuj jawne relacje klasowe (tj. Interfejsy) nad niejawnymi (tj. Dziedziczenie)”. Te pierwsze powiedzą ci, czego potrzebujesz, nie mówiąc ci, jak to zrobić. Te ostatnie nie tylko podpowiedzą ci, jak to zrobić, ale sprawią, że będziesz żałować na później.
Evan Plaice,

26

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:

Preferuj kompozycję obiektów nad dziedziczeniem

Wstępują do tego stwierdzenia poprzez krótkie porównanie dziedziczenia ze składem. Nie mówią „nigdy nie używaj dziedziczenia”.


23

„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 ... ”


4
Bardzo dobry przykład z Wężem. Podobny przypadek spotkałem ostatnio na własnych zajęciach
Maksee,

22

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


1
„Oczywiście nie jest to ta jednoznaczna ulica / droga jednokierunkowa”. Kiedy skład nie jest bardziej (lub jednakowo) elastyczny niż dziedziczenie? Twierdziłbym, że jest to ulica jednokierunkowa. Dziedziczenie to po prostu cukier syntaktyczny na specjalny przypadek składu.
weberc2,

Kompozycja często nie jest elastyczna w przypadku używania języka, który implementuje kompozycję przy użyciu jawnie zaimplementowanych interfejsów, ponieważ interfejsy te nie mogą ewoluować w miarę kompatybilności wstecznej w czasie. #jmtcw
MikeSchinkel

11

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 dodaje foona 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 foobył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.


3
W porządku, myślę, że ma to sens z perspektywy C ++. To coś, o czym nigdy nie myślałem, ponieważ nie jest to problem w Delphi, z czego najczęściej korzystam. (Nie ma wielokrotnego dziedziczenia, a jeśli masz metodę w klasie bazowej i inną metodę o tej samej nazwie w klasie pochodnej, która nie zastępuje metody podstawowej, kompilator wygeneruje ostrzeżenie, abyś nie przypadkowo z tego rodzaju problemem).
Mason Wheeler,

1
@Mason: Wersja Object Pascal (znana również jako Delphi) Andera jest lepsza niż C ++ i Java, jeśli chodzi o problem delikatnego dziedziczenia klas podstawowych. W przeciwieństwie do C ++ i Java przeciążenie odziedziczonej metody wirtualnej nie jest niejawne.
bit-twiddler

2
@ bit-twiddler, to, co mówisz o C ++ i Javie, można powiedzieć o Smalltalk, Perlu, Pythonie, Ruby, JavaScript, Common Lisp, Objective-C i każdym innym języku, którego się nauczyłem, który zapewnia dowolną formę obsługi OO. To powiedziawszy, googling sugeruje, że C # podąża za przykładem Object Pascal.
btilly,

3
Jest tak, ponieważ Anders Hejlsberg zaprojektował smak Borlanda dla obiektu Pascal (alias Delphi) i C #.
bit-twiddler

8

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.


„W innych okolicznościach dziedziczenie zadziała, a kompozycja nie.” Kiedy? Dziedziczenie można modelować przy użyciu polimorfizmu kompozycji i interfejsu, więc byłbym zaskoczony, gdyby istniały jakiekolwiek okoliczności, w których kompozycja nie zadziała.
weberc2

8

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ą).


To prawdziwa część edukacji. Masz do zdobycia wyższe kursy, tj. Resztę 20%, ale jako student właśnie uczyłeś kursów podstawowych i być może pośrednich. Na końcu czujesz się dobrze nauczany, ponieważ dobrze spisałeś się na kursach (i po prostu nie widzisz tego po prostu szpilką na drabinie). To tylko część rzeczywistości, a naszym najlepszym rozwiązaniem jest zrozumienie jej skutków ubocznych, a nie atakowanie programistów.
Niezależny

8

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:

  • dziedziczy po interfejsie
  • dziedziczy po jego wdrożeniu (zarówno dane, jak i metody)

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


7

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.


2
Ale o to chodzi. Oni nie „inaczej równoważny”. Dziedziczenie umożliwia podstawienie i polimorfizm Liskowa, które są głównym celem korzystania z OOP. Kompozycja nie.
Mason Wheeler,

4
Polimorfizm / LSP nie są „sednem” OOP, są jedną cechą. Istnieje również enkapsulacja, abstrakcje itp. Dziedziczenie oznacza związek „jest”, modele agregacji „mają” związek.
Steve

@ Steve: Możesz tworzyć abstrakcje i enkapsulację w dowolnym paradygmacie, który wspiera tworzenie struktur danych i procedur / funkcji. Ale polimorfizm (w tym kontekście odnoszący się do wirtualnego wywoływania metod w tym kontekście) i podstawienie Liskowa są unikalne dla programowania obiektowego. Właśnie to miałem na myśli.
Mason Wheeler,

3
@Mason: Wytyczną jest „faworyzowanie kompozycji nad dziedziczeniem”. W żaden sposób nie oznacza to, że nie należy używać, ani nawet unikać dziedziczenia. Wszystko mówi, że gdy wszystkie inne rzeczy są równe, wybierz kompozycję. Ale jeśli potrzebujesz dziedzictwa, skorzystaj z niego!
Jeffrey Faust,

7

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.


1
Podoba mi się twoja analogia
barrylloyd

2

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.


-1

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.

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.