it-swarm-eu.dev

Odkud pochází tento koncept „příznivého složení před dědičností“?

V posledních několika měsících se zdá, že mantra „upřednostňuje složení před dědičností“, vyšla z ničeho a stala se téměř nějakým druhem meme v programovací komunitě. A pokaždé, když to vidím, jsem trochu zmatená. Je to, jako by někdo řekl „laskavost cvičení nad kladiva“. Podle mých zkušeností jsou složení a dědičnost dva různé nástroje s různými případy použití a zacházení s nimi, jako by byly vzájemně zaměnitelné a jeden byl ze své podstaty nadřazený, nedává žádný smysl.

Také jsem nikdy neviděl skutečné vysvětlení pro proč dědičnost je špatná a složení je dobré, což mě jen děsí. Má to být přijato jen na víře? Liskovská substituce a polymorfismus mají dobře známé, jasné výhody a IMO představují celý bod používání objektově orientovaného programování a nikdo nikdy nevysvětluje, proč by měl být vyřazen ve prospěch kompozice.

Ví někdo, odkud tento koncept pochází a z jakého důvodu?

145
Mason Wheeler

I když si myslím, že jsem slyšel diskuse o kompozici vs. dědičnosti dlouho před GoF, nemohu dát prst na konkrétní zdroj. Mohlo to být Booch stejně.

<rant>

Ah, ale stejně jako mnoho mantras, i tato se zvrhla podle typických linií:

  1. to je představeno s podrobným vysvětlením a argumentem dobře respektovaným zdrojem, který vymyslí catch-větu jako připomínka původní komplexní diskuse
  2. to je sdíleno s vědomou část-of-the-klub mrknutí pár in-the-know na chvíli, obvykle při komentování k n00b chyby
  3. brzy je to bezmyšlenkovitě opakováno tisíci a tisíci, kteří nikdy nečetli vysvětlení, ale láska jej používá jako výmluvu, aby nemyslela , a jako levný a snadný způsob, jak se cítit lépe než ostatní
  4. nakonec žádné rozumné odhalení nemůže zastavit příliv „meme“ - a paradigma degeneruje na náboženství a dogma.

Méma, původně určená k vedení n00bs k osvícení, se nyní používá jako klub, aby je zahladila v bezvědomí.

Složení a dědictví jsou velmi odlišné věci a neměly by se navzájem zaměňovat. I když je pravda, že složení lze použít k simulaci dědičnosti se spoustou práce navíc , nedělá to dědičnost občana druhé třídy, ani nedělá dělá z kompozice oblíbeného syna. Skutečnost, že mnoho n00bs se snaží použít dědičnost jako zkratku, nezbavuje tento mechanismus a téměř všechny n00bs se poučí z chyby a tím se zlepší.

PŘEMÝŠLEJTE o svých návrzích a zastavte hubení sloganů.

</rant>

139
Steven A. Lowe

Zkušenosti.

Jak říkáte, že jsou nástroji pro různé úkoly, ale tato věta vznikla, protože ji lidé nepoužívali tímto způsobem.

Dědičnost je primárně polymorfním nástrojem, ale někteří lidé se do svého pozdějšího nebezpečí pokusí použít jako způsob opětovného použití/sdílení kódu. Důvodem je „dobře, pokud zdědím, pak získám všechny metody zdarma“, ale ignoruji skutečnost, že tyto dvě třídy potenciálně nemají polymorfní vztah.

Proč tedy dávat přednost kompozici před dědičností - prostě proto, že vztah mezi třídami není častěji polymorfní. Existuje prostě proto, abychom lidem připomněli, aby na kolena nereagovali zděděním.

75
Stephen Bailey

Nejedná se o nový nápad, věřím, že byl skutečně představen v knize návrhových vzorů GoF, která byla vydána v roce 1994.

Hlavním problémem dědičnosti je to, že je to bílá skříňka. Podle definice musíte znát podrobnosti implementace třídy, kterou zdědíte. Na druhé straně se skladbou zajímáte pouze o veřejné rozhraní třídy, kterou skládáte.

Z knihy GoF:

Dědičnost vystavuje podtřídu podrobnostem implementace rodičů, často se říká, že „dědičnost přerušuje zapouzdření“

Wikipedia článek o knize GoF má slušný úvod.

43
Dean Harding

Abych odpověděl na část vaší otázky, věřím, že se tento koncept poprvé objevil v knize GOF Design Patterns: Elements of Reusable Object-Oriented Software kniha, která byla poprvé publikována v roce 1994. Fráze se objevuje v horní části stránky 20, v úvodu:

Upřednostňujte složení objektu před dědičností

Předmluvu uvádějí krátkým porovnáním dědičnosti s kompozicí. Neříkají „nikdy nepoužívají dědičnost“.

27
vjones

„Složení pro dědičnost“ je krátký (a zjevně zavádějící) způsob, jak říci „Když cítíte, že data (nebo chování) třídy by měla být začleněna do jiné třídy, vždy zvažte použití kompozice před slepým použitím dědičnosti“.

Proč je to pravda? Protože dědičnost vytváří těsné spojení kompilace mezi těmito dvěma třídami. Složení je naopak volná vazba, která mimo jiné umožňuje jasné oddělení problémů, možnost přepínání závislostí za běhu a snadnější, izolovanější testovatelnost závislosti.

To znamená, že s dědictvím by se mělo zacházet opatrně, protože to stojí za cenu, ne že to není užitečné. Ve skutečnosti „Složení nad dědičností“ často končí „Složení + dědičnost nad dědičností“, protože často chcete, aby vaše složená závislost byla spíše abstraktní nadtřídou než samotnou konkrétní podtřídou. Umožňuje vám přepínat mezi různými konkrétními implementacemi vaší závislosti za běhu.

Z tohoto důvodu (mimo jiné) pravděpodobně uvidíte dědičnost používanou častěji ve formě implementace rozhraní nebo abstraktních tříd než dědictví Vanilky.

Příkladem (metaforickým) může být:

„Mám třídu Snake a chci do této třídy zahrnout to, co se stane, když se Snake kousne. Byl bych v pokušení, aby Snake zdědil třídu BiterAnimal, která má metodu Bite () a tuto metodu potlačím, aby odrážel jedovatý skus. "Složení nad dědičností mě ale varuje, že bych se měl místo toho pokusit použít kompozici ... V mém případě by se to mohlo proměnit v hada s členem Bite. Třída Bite by mohla být abstraktní (nebo rozhraní) s několika podtřídami. To by umožnilo mě Pěkné věci, jako kdybych měl podtřídy VenomousBite a DryBite a schopnost změnit skus ve stejné instanci Snake, kdy had stárne. Plus zvládnutí všech efektů Bite ve své vlastní oddělené třídě mi může umožnit znovu použít v této Frost třídě , protože mráz kousne, ale není BiterAnimal, a tak dále ... "

24
guillaume31

Některé možné argumenty pro složení:

Složení je o něco více jazyk/rámec agnostické
Dědičnost a to, co vynucuje/vyžaduje/umožňuje, se bude lišit mezi jazyky, pokud jde o to, k čemu má podtřída/nadřazená třída přístup a jaké důsledky pro výkon může mít virtuální metody wrt atd. Složení je zcela základní a vyžaduje velmi malý jazyk podpora, a tak implementace napříč různými platformami/rámci mohou snáze sdílet vzory složení.

Složení je velmi jednoduchý a hmatový způsob vytváření objektů
Dědičnost je poměrně snadno srozumitelná, ale v reálném životě stále není tak snadno prokázána. Mnoho objektů v reálném životě lze rozdělit na části a složit. Řekněme, že kolo lze postavit pomocí dvou kol, rámu, sedadla, řetězu atd. Snadno vysvětlitelné kompozicí. Zatímco v metaforě dědičnosti lze říci, že kolo rozšiřuje jednokolku, poněkud proveditelné, ale stále mnohem dále od skutečného obrazu než složení (zjevně to není velmi dobrý příklad dědičnosti, ale bod zůstává stejný). Dokonce i dědičnost slova (alespoň u většiny amerických anglických mluvčích, kterou bych očekával) automaticky vyvolává význam podle řádků „Něco předáno zesnulému příbuznému“, který má určitou korelaci s jeho významem v softwaru, ale stále jen volně padne.

Složení je téměř vždy flexibilnější
Pomocí kompozice si můžete vždy zvolit, jak definovat své vlastní chování, nebo jednoduše odhalit tu část vašich složených částí. Tímto způsobem nebudete čelit žádným omezením, která mohou být uložena hierarchií dědičnosti (virtuální vs. nevirtuální atd.)

Mohlo by to být proto, že Složení je přirozeně jednodušší metafora, která má méně teoretických omezení než dědičnost. Kromě toho mohou být tyto konkrétní důvody zjevnější během návrhu, nebo se mohou vytrhnout, když se zabýváme některými bolestivými body dědičnosti.

Zřeknutí se odpovědnosti:
Je zřejmé, že to není tak jednoznačná ulice/jednosměrná ulice. Každý návrh si zaslouží vyhodnocení několika vzorů/nástrojů. Dědičnost je široce používaná, má spoustu výhod a mnohokrát je elegantnější než složení. To jsou jen některé možné důvody, které lze použít při upřednostňování kompozice.

22
TJB

Možná jste si všimli, že to lidé říkali v posledních několika měsících, ale dobré programátory je známo mnohem déle. Určitě to říkám tam, kde je to vhodné, asi deset let.

Smyslem tohoto konceptu je, že existuje velká koncepční režie dědičnosti. Pokud používáte dědičnost, pak každé volání metody má implicitní odeslání. Máte-li hluboké dědičné stromy nebo vícenásobné odeslání, nebo (ještě horší) obojí, pak zjišťování, kam se konkrétní metoda odešle, se může stát královskou PITA. To komplikuje správné zdůvodnění kódu a ztěžuje ladění.

Dovolte mi uvést jednoduchý příklad pro ilustraci. Předpokládejme, že hluboko ve stromu dědičnosti někdo jmenoval metodu foo. Potom přijde někdo jiný a přidá na vrchol stromu foo, ale udělá něco jiného. (Tento případ je běžnější u vícenásobného dědictví.) Nyní osoba pracující v kořenové třídě rozbila nejasnou podřízenou třídu a pravděpodobně si ji neuvědomila. Mohli byste mít 100% pokrytí jednotkovými testy a toto porušení byste si nevšimli, protože osoba nahoře by nemyslela na testování podřízené třídy a testy na podřízené třídě nemysly na testování nových metod vytvořených na vrcholu . (Je pravda, že existují způsoby, jak psát testy jednotek, které to zachytí, ale existují i ​​případy, kdy tímto způsobem nemůžete snadno psát testy.)

Na rozdíl od toho, když používáte kompozici, při každém hovoru je obvykle jasnější, k čemu posíláte hovor. (Dobře, pokud používáte inverzi kontroly, například pomocí závislostního vstřikování, pak může být problematické také zjistit, kam se hovor uskutečňuje. Obvykle je však snazší to zjistit.) To usnadňuje zdůvodnění. Bonusem je, že složení má za následek oddělení metod od sebe navzájem. Výše uvedený příklad by se tam neměl stát, protože by se podřízená třída přesunula k nějaké temné komponentě a nikdy neexistuje otázka, zda volání foo bylo určeno pro temnou komponentu nebo hlavní objekt.

Nyní máte naprostou pravdu, že dědičnost a složení jsou dva velmi odlišné nástroje, které slouží dvěma různým druhům věcí. Jistě dědictví má koncepční režii, ale když je to ten správný nástroj pro danou práci, přináší méně koncepční režii, než když se ho snaží nepoužívat a dělá ručně, co pro tebe dělá. Nikdo, kdo ví, co dělají, by řekl, že byste nikdy neměli používat dědičnost. Ale ujistěte se, že je to správné.

Bohužel mnoho vývojářů se dozví o objektově orientovaném softwaru, dozví se o dědičnosti a poté se snaží používat svou novou sekeru tak často, jak je to jen možné. Což znamená, že se snaží použít dědičnost tam, kde složení bylo tím správným nástrojem. Doufejme, že se naučí lépe v čase, ale často k tomu nedojde, až po několika odstraněných končetinách atd. Řeknutí jim dopředu, že je špatný nápad, má tendenci urychlit proces učení a snížit zranění.

11
btilly

Je to reakce na pozorování, že OO začátečníci mají tendenci používat dědičnost, když to nepotřebují. Dědičnost rozhodně není špatná věc, ale může být nadužívána. Pokud jedna třída potřebuje pouze funkčnost od jiného, ​​pak složení pravděpodobně bude fungovat. Za jiných okolností bude dědictví fungovat a složení nebude.

Zdědění ze třídy znamená spoustu věcí. Znamená to, že Derived je typ Základny (viz Princip substituce Liskov pro podrobnosti o gory), že kdykoli použijete Základnu, mělo by smysl použít Derived. Poskytuje odvozenému přístupu chráněným členům a členským funkcím základny. Je to úzký vztah, což znamená, že má vysokou vazbu a změny jednoho pravděpodobně vyžadují změny druhého.

Spojování je špatná věc. To ztěžuje pochopení a úpravy programů. Pokud jsou věci stejné, měli byste vždy vybrat možnost s menší vazbou.

Proto pokud složení nebo dědičnost bude dělat práci efektivně, zvolte složení, protože je to nižší vazba. Pokud složení nebude dělat práci efektivně a dědictví bude, vyberte dědičnost, protože musíte.

8
David Thornley

Tady jsou moje dva centy (kromě všech vynikajících bodů, které již byly vzneseny):

IMHO, Přichází k tomu, že většina programátorů ve skutečnosti nezdědí dědičnost, a nakonec to přehánějí, částečně v důsledku toho, jak se tento koncept učí. Tento koncept existuje jako způsob, jak je odradit od přehání, namísto toho, aby se zaměřil na to, jak to udělat správně.

Každý, kdo strávil čas výukou nebo mentorováním, ví, že k tomu často dochází, zejména u nových vývojářů, kteří mají zkušenosti s jinými vzory:

Tito vývojáři zpočátku cítí, že dědičnost je tento děsivý koncept. Nakonec vytvářejí typy s přesahy rozhraní (např. Se stejným specifikovaným chováním bez společného podtypování) as globály pro implementaci společných funkcí.

Poté (často v důsledku horlivého učení) se učí o výhodách dědičnosti, ale často se to učí jako univerzální řešení pro opětovné použití. Nakonec se domnívají, že jakékoli sdílené chování musí být sdíleno dědičností. Je tomu tak proto, že se často zaměřuje spíše na dědičnost implementace než na subtypování.

V 80% případů je to dost dobré. Ale dalších 20% je místem, kde problém začíná. Aby se vyhnuli přepisování a aby se ujistili, že využili sdílení implementace, začnou navrhovat svou hierarchii spíše než zamýšlenou implementaci než základní abstrakce. Dobrým příkladem je „Stack zděděný ze dvojitě propojeného seznamu“.

Většina učitelů v tomto bodě netrvá na zavedení konceptu rozhraní, protože je to buď jiný koncept, nebo protože (v C++) je musíte předstírat pomocí abstraktních tříd a vícenásobného dědictví, které se v této fázi neučí. V Javě mnoho učitelů nerozlišuje „žádná vícenásobná dědičnost“ nebo „vícenásobná dědičnost je zlá“ od důležitosti rozhraní.

To vše ještě umocňuje skutečnost, že jsme se toho tolik naučili, že nemusíme psát nadbytečný kód s implementační dědičností, že tuny přímého delegačního kódu vypadají nepřirozeně. Takže složení vypadá chaoticky, proto potřebujeme tato pravidla palců, abychom mohli tlačit nové programátory, aby je stejně používali (což také přehánějí).

8
Uri

V jednom komentáři Mason zmínil, že jednoho dne budeme mluvit o Dědičnost považována za škodlivo.

Doufám.

Problém s dědičností je najednou jednoduchý a smrtící, nerešpektuje myšlenku, že jeden koncept by měl mít jednu funkčnost.

Ve většině OO jazyků při zdědění ze základní třídy:

  • zdědí z jeho rozhraní
  • zdědit jeho implementaci (data i metody)

A tady se stávají potíže.

Toto je ne jediný přístup, ačkoli 00-jazyky jsou většinou přilepené s ním. Naštěstí v nich existují rozhraní/abstraktní třídy.

Nedostatečná jednoduchost pro to, že by jinak byla způsobena tím, že se do značné míry používá: upřímně řečeno, i kdybychom o tom věděli, zdědili byste rozhraní a vložili základní třídu podle složení a delegovali většinu volání metod? Bylo by ale mnohem lepší, dokonce byste byli varováni, pokud se najednou objeví nová metoda v rozhraní a bude si muset vědomě zvolit, jak ji implementovat.

Jako kontrapunkt umožňuje Haskell použít princip liskovského principu pouze tehdy, když „vychází“ z čistých rozhraní (nazývaných koncepty) (1). Nelze odvodit z existující třídy, pouze složení vám umožní vložit její data.

1) koncepty mohou poskytovat smysluplnou výchozí implementaci, ale protože nemají žádná data, lze tuto výchozí hodnotu definovat pouze z hlediska jiných metod navržených konceptem nebo z hlediska konstant.

8
Matthieu M.

Myslím, že tento druh rady je jako říkat „raději jezdit před létáním“. To znamená, že letadla mají v porovnání s automobily nejrůznější výhody, ale to má určitou složitost. Takže pokud se mnoho lidí pokusí letět z centra města na předměstí, rada, kterou opravdu, opravd potřebují slyšet, je, že nemusí létat, a ve skutečnosti to létání jen povede v dlouhodobém výhledu komplikovanější, i když se zdá, že je v krátkodobém horizontu chladný/účinný/snadný. Zatímco když do musíte létat, mělo by to být zřejmé.

Podobně nemůže dědičnost dělat věci, které nelze, ale měli byste ji použít, když ji potřebujete, a ne, když ji nemáte. Takže pokud jste nikdy v pokušení jen předpokládat, že potřebujete dědičnost, když nemáte, pak nepotřebujete radu „preferovaného složení“. Ale mnoho lidí do a do tuto radu potřebují.

Má to být implicitní, že když opravdu potřebujete dědičnost, je to zřejmé, a měli byste ji poté použít.

Také odpověď Stevena Loweho. Opravdu, opravdu.

7
Jack V.

Jednoduchá odpověď zní: dědičnost má větší propojení než složení. S ohledem na dvě možnosti, jinak rovnocenné kvality, vyberte tu, která je méně vázaná.

7
Jeffrey Faust

Dědičnost není ze své podstaty špatná a složení není ze své podstaty dobré. Jsou to pouze nástroje, které může programátor OO) použít k návrhu softwaru.

Když se podíváte na třídu, dělá to víc, než by absolutně mělo ( SRP )? Zbytečně duplikuje funkčnost ( DRY ), nebo se příliš zajímá o vlastnosti nebo metody jiných tříd ( Envy Envy ) ? Pokud třída porušuje všechny tyto pojmy (a možná i více), pokouší se být třída Boha . Jedná se o řadu problémů, které se mohou objevit při navrhování softwaru, z nichž žádný není nutně problém dědičnosti, ale který může rychle vytvořit velké bolesti hlavy a křehké závislosti, kde byl také použit polymorfismus.

Kořenem problému bude pravděpodobně nedostatek porozumění dědičnosti a další z chudých možností, pokud jde o design, nebo neuznávající „vůně kódu“ týkající se tříd, které nedodržují Princip jediné odpovědnosti. Polymorfismus a Liskovská substituce nemusí být vyřazena ve prospěch kompozice. Polymorfismus sám o sobě může být použit, aniž by se spoléhal na dědičnost, to vše jsou zcela doplňkové koncepty. Pokud se použije zamyšleně. Trik je zaměřit se na to, aby váš kód byl jednoduchý a čistý a nepodléhal přílišnému znepokojení nad počtem tříd, které musíte vytvořit, abyste vytvořili robustní návrh systému.

Pokud jde o otázku upřednostňování kompozice před dědičností, jedná se skutečně pouze o další případ promyšlené aplikace konstrukčních prvků, které mají největší smysl, pokud jde o řešený problém. Pokud nepotřebujete zdědit chování, pravděpodobně byste neměli jako složení pomáhat vyhnout se neslučitelnosti a většímu úsilí refaktoringu později. Pokud na druhé straně zjistíte, že opakujete mnoho kódu tak, že veškerá duplikace je soustředěna na skupinu podobných tříd, pak je možné, že vytvoření společného předka by pomohlo snížit počet identických hovorů a tříd. budete možná muset opakovat mezi každou třídou. Proto dáváte přednost kompozici, ale nepředpokládáte, že dědičnost není nikdy použitelná.

2
S.Robins