Složení nad dědičností - Composition over inheritance
tento článek možná matoucí nebo nejasné čtenářům.Říjen 2015) (Zjistěte, jak a kdy odstranit tuto zprávu šablony) ( |
Složení nad dědičností (nebo složený princip opětovného použití) v objektově orientované programování (OOP) je princip, kterého by třídy měly dosáhnout polymorfní chování a opětovné použití kódu od jejich složení (tím, že obsahuje instance jiných tříd, které implementují požadovanou funkčnost) spíše než dědictví ze základní nebo nadřazené třídy.[2] Toto je často uváděný princip OOP, jako například ve vlivné knize Designové vzory (1994).[3]
Základy
Implementace složení nad dědičnost obvykle začíná vytvořením různých rozhraní představující chování, které musí systém vykazovat. Rozhraní povolena polymorfní chování. Třídy implementující identifikovaná rozhraní jsou sestaveny a přidány obchodní doména třídy podle potřeby. Chování systému je tedy realizováno bez dědičnosti.
Ve skutečnosti mohou být třídy obchodních domén všechny základní třídy bez jakéhokoli dědičnosti. Alternativní implementace chování systému se provádí poskytnutím jiné třídy, která implementuje požadované rozhraní chování. Třída, která obsahuje odkaz na rozhraní, může podporovat implementace rozhraní - volba, kterou lze odložit až do doby spuštění.
Příklad
Dědictví
Příklad v C ++ následuje:
třída Objekt{veřejnost: virtuální prázdnota Aktualizace() { // no-op } virtuální prázdnota kreslit() { // no-op } virtuální prázdnota kolidovat(Objekt předměty[]) { // no-op }};třída Viditelné : veřejnost Objekt{ Modelka* Modelka;veřejnost: virtuální prázdnota kreslit() přepsat { // kód k nakreslení modelu na pozici tohoto objektu }};třída Pevný : veřejnost Objekt{veřejnost: virtuální prázdnota kolidovat(Objekt předměty[]) přepsat { // kód pro kontrolu a reakci na kolize s jinými objekty }};třída Pohyblivý : veřejnost Objekt{veřejnost: virtuální prázdnota Aktualizace() přepsat { // kód pro aktualizaci polohy tohoto objektu }};
Předpokládejme tedy, že máme také tyto konkrétní třídy:
- třída
Hráč
- který jePevný
,Pohyblivý
aViditelné
- třída
Mrak
- který jePohyblivý
aViditelné
, ale nePevný
- třída
Budova
- který jePevný
aViditelné
, ale nePohyblivý
- třída
Past
- který jePevný
, ale ani jedenViditelné
aniPohyblivý
Všimněte si, že vícenásobné dědictví je nebezpečné, pokud není implementováno pečlivě, protože může vést k diamantový problém. Jedním z řešení, jak se tomu vyhnout, je vytvořit třídy, jako je Viditelné a pevné
, VisibleAndMovable
, VisibleAndSolidAndMovable
atd. pro každou potřebnou kombinaci, i když to vede k velkému množství opakujících se kódů. Mějte na paměti, že C ++ řeší diamantový problém vícenásobné dědičnosti povolením virtuální dědičnost.
Složení a rozhraní
Příklady C ++ v této části demonstrují princip použití kompozice a rozhraní k dosažení opětovného použití kódu a polymorfismu. Vzhledem k tomu, že jazyk C ++ nemá vyhrazené klíčové slovo pro deklaraci rozhraní, používá následující příklad C ++ „dědičnost z čistě abstraktní základní třídy“. Pro většinu účelů je to funkčně ekvivalentní rozhraním poskytovaným v jiných jazycích, jako je Java a C #.
Představte abstraktní třídu s názvem VisibilityDelegate
, s podtřídami Neviditelný
a Viditelné
, který poskytuje prostředky k nakreslení objektu:
třída VisibilityDelegate{veřejnost: virtuální prázdnota kreslit() = 0;};třída Neviditelný : veřejnost VisibilityDelegate{veřejnost: virtuální prázdnota kreslit() přepsat { // no-op }};třída Viditelné : veřejnost VisibilityDelegate{veřejnost: virtuální prázdnota kreslit() přepsat { // kód k nakreslení modelu na pozici tohoto objektu }};
Představte abstraktní třídu s názvem UpdateDelegate
, s podtřídami Nepohyblivý
a Pohyblivý
, který poskytuje prostředky k pohybu objektu:
třída UpdateDelegate{veřejnost: virtuální prázdnota Aktualizace() = 0;};třída Nepohyblivý : veřejnost UpdateDelegate{veřejnost: virtuální prázdnota Aktualizace() přepsat { // no-op }};třída Pohyblivý : veřejnost UpdateDelegate{veřejnost: virtuální prázdnota Aktualizace() přepsat { // kód pro aktualizaci polohy tohoto objektu }};
Představte abstraktní třídu s názvem CollisionDelegate
, s podtřídami Není pevné
a Pevný
, který poskytuje prostředek ke srážce s objektem:
třída CollisionDelegate{veřejnost: virtuální prázdnota kolidovat(Objekt předměty[]) = 0;};třída Není pevné : veřejnost CollisionDelegate{veřejnost: virtuální prázdnota kolidovat(Objekt předměty[]) přepsat { // no-op }};třída Pevný : veřejnost CollisionDelegate{veřejnost: virtuální prázdnota kolidovat(Objekt předměty[]) přepsat { // kód pro kontrolu a reakci na kolize s jinými objekty }};
Nakonec představte třídu s názvem Objekt
se členy ke kontrole jeho viditelnosti (pomocí a VisibilityDelegate
), pohyblivost (pomocí UpdateDelegate
) a pevnost (pomocí a CollisionDelegate
). Tato třída má metody, které delegují na své členy, např. Aktualizace()
jednoduše zavolá metodu na UpdateDelegate
:
třída Objekt{ VisibilityDelegate* _proti; UpdateDelegate* _u; CollisionDelegate* _C;veřejnost: Objekt(VisibilityDelegate* proti, UpdateDelegate* u, CollisionDelegate* C) : _proti(proti) , _u(u) , _C(C) {} prázdnota Aktualizace() { _u->Aktualizace(); } prázdnota kreslit() { _proti->kreslit(); } prázdnota kolidovat(Objekt předměty[]) { _C->kolidovat(předměty); }};
Konkrétní třídy by pak vypadaly takto:
třída Hráč : veřejnost Objekt{veřejnost: Hráč() : Objekt(Nový Viditelné(), Nový Pohyblivý(), Nový Pevný()) {} // ...};třída Kouř : veřejnost Objekt{veřejnost: Kouř() : Objekt(Nový Viditelné(), Nový Pohyblivý(), Nový Není pevné()) {} // ...};
Výhody
Upřednostňovat kompozici před dědičností je princip designu, který dává designu vyšší flexibilitu. Je přirozenější vytvářet třídy obchodních domén z různých komponent, než se snažit najít mezi nimi společné rysy a vytvořit rodokmen. Například plynový pedál a volant sdílejí velmi málo společných rysů, přesto jsou oba důležité součásti automobilu. Co mohou dělat a jak z nich mohou mít prospěch auto, je snadno definováno. Složení také z dlouhodobého hlediska poskytuje stabilnější obchodní doménu, protože je méně náchylné k podivům členů rodiny. Jinými slovy, je lepší sestavit, co objekt dokáže (MÁ ) než rozšířit, co to je (JE ).[1]
Počáteční návrh je zjednodušen identifikací chování systémových objektů v samostatných rozhraních namísto vytváření hierarchického vztahu k distribuci chování mezi třídami obchodních domén prostřednictvím dědičnosti. Tento přístup snáze přizpůsobuje budoucí změny požadavků, které by jinak vyžadovaly úplnou restrukturalizaci tříd obchodních domén v modelu dědičnosti. Kromě toho se vyhýbá problémům často spojeným s relativně malými změnami modelu založeného na dědičnosti, který zahrnuje několik generací tříd.
Některé jazyky, zejména Jít, použijte výhradně typové složení.[4]
Nevýhody
Jednou společnou nevýhodou použití složení namísto dědičnosti je, že metody poskytované jednotlivými komponentami bude možná nutné implementovat v odvozeném typu, i když jsou pouze metody předávání (to platí ve většině programovacích jazyků, ale ne ve všech; viz Vyhýbání se nevýhodám.) Naproti tomu dědičnost nevyžaduje, aby byly všechny metody základní třídy znovu implementovány v odvozené třídě. Odvozená třída spíše potřebuje implementovat (přepsat) metody, které mají jiné chování než metody základní třídy. To může vyžadovat podstatně menší úsilí při programování, pokud základní třída obsahuje mnoho metod poskytujících výchozí chování a pouze několik z nich musí být přepsáno v rámci odvozené třídy.
Například v kódu C # níže proměnné a metody Zaměstnanec
základní třída dědí Zaměstnanec za hodinu
a Zaměstnanec s platem
odvozené podtřídy. Pouze Platit()
metoda musí být implementována (specializována) každou odvozenou podtřídou. Ostatní metody jsou implementovány samotnou základní třídou a jsou sdíleny všemi jejími odvozenými podtřídami; nemusí být znovu implementovány (přepsány) nebo dokonce uvedeny v definicích podtříd.
// Základní třídaveřejnost abstraktní třída Zaměstnanec{ // Vlastnosti chráněný tětiva název { dostat; soubor; } chráněný int ID { dostat; soubor; } chráněný desetinný Platební sazba { dostat; soubor; } chráněný int Hodiny { dostat; } // Získejte výplatu za aktuální výplatní období veřejnost abstraktní desetinný Platit();}// Odvozená podtřídaveřejnost třída Zaměstnanec za hodinu : Zaměstnanec{ // Získejte výplatu za aktuální výplatní období veřejnost přepsat desetinný Platit() { // Odpracovaný čas je v hodinách vrátit se Hodiny * Platební sazba; }}// Odvozená podtřídaveřejnost třída Zaměstnanec s platem : Zaměstnanec{ // Získejte výplatu za aktuální výplatní období veřejnost přepsat desetinný Platit() { // Mzda je roční plat místo hodinové sazby vrátit se Hodiny * Platební sazba / 2087; }}
Vyhýbání se nevýhodám
Této nevýhodě se lze vyhnout použitím rysy, mixiny, (typ) vkládání nebo protokol rozšíření.
Některé jazyky poskytují konkrétní prostředky, jak to zmírnit:
- C# poskytuje výchozí metody rozhraní od verze 8.0, která umožňuje definovat tělo pro člena rozhraní. [5]
- D poskytuje explicitní deklaraci "alias tohoto" v rámci typu, který do ní může předávat všechny metody a členy jiného obsaženého typu. [6]
- Jít vkládání typu se vyhne potřebě metod předávání.[7]
- Jáva poskytuje projekt Lombok[8] což umožňuje implementaci delegování pomocí jediného
@Delegát
anotace v poli, místo kopírování a udržování názvů a typů všech metod z delegovaného pole.[9] Java 8 umožňuje výchozí metody v rozhraní, podobně jako v C # atd. - Julie makra lze použít ke generování metod předávání. Existuje několik implementací, jako např Lazy.jl a TypedDelegation.jl.
- Kotlin zahrnuje vzor delegování do syntaxe jazyka.[10]
- Raku poskytuje a
rukojeti
klíčové slovo pro usnadnění předávání metod. - Rez poskytuje vlastnosti s výchozí implementací.
- Rychlý rozšíření lze použít k definování výchozí implementace protokolu na samotném protokolu, nikoli v rámci implementace jednotlivého typu.[11]
Empirické studie
Studie z roku 2013 týkající se 93 open source programů Java (různé velikosti) zjistila, že:
I když neexistuje obrovská [sic] příležitost nahradit dědictví složením (...), příležitost je významná (medián 2% použití [dědictví] je pouze interní opětovné použití a dalších 22% je pouze externí nebo interní Naše výsledky naznačují, že není třeba se obávat zneužití dědičnosti (alespoň v softwaru Java s otevřeným zdrojovým kódem), ale zdůrazňují otázku týkající se použití složení v porovnání s dědičností. Pokud existují značné náklady spojené s používáním dědičnosti, když lze použít složení, pak naše výsledky naznačují, že existuje nějaký důvod k obavám.
— Tempero et al.„Co programátoři dělají s dědičností v Javě“[12]
Viz také
- Vzor delegování
- Princip substituce Liskov
- Objektově orientovaný design
- Role-orientované programování
- Stavový vzor
- Strategický vzor
Reference
- ^ A b Freeman, Eric; Robson, Elisabeth; Sierra, Kathy; Bates, Bert (2004). Hlava první návrhové vzory. O'Reilly. str.23. ISBN 978-0-596-00712-6.
- ^ Knoernschild, Kirk (2002). Java Design - objekty, UML a proces: 1.1.5 Princip kompozitního opětovného použití (CRP). Addison-Wesley Inc. ISBN 9780201750447. Citováno 2012-05-29.
- ^ Gamma, Erichu; Helm, Richard; Johnson, Ralph; Vlissides, Johne (1994). Návrhové vzory: Prvky opakovaně použitelného objektově orientovaného softwaru. Addison-Wesley. str.20. ISBN 0-201-63361-2. OCLC 31171684.
- ^ Pike, Rob (2012-06-25). „Méně je exponenciálně více“. Citováno 2016-10-01.
- ^ „Co je nového v C # 8.0“. Dokumenty Microsoftu. Microsoft. Citováno 2019-02-20.
- ^ „Alias toto“. D Jazyková reference. Citováno 2019-06-15.
- ^ "(Typ) Vkládání “. Dokumentace Go Programovací jazyk. Citováno 2019-05-10.
- ^ https://projectlombok.org
- ^ "@Delegát". Projekt Lombok. Citováno 2018-07-11.
- ^ „Delegované vlastnosti“. Odkaz na Kotlin. JetBrains. Citováno 2018-07-11.
- ^ „Protokoly“. Programovací jazyk Swift. Apple Inc.. Citováno 2018-07-11.
- ^ Tempero, Ewan; Yang, Hong Yul; Noble, James (2013). Co programátoři dělají s dědičností v Javě (PDF). ECOOP 2013 - objektově orientované programování. 577–601.