Vícenásobná dědičnost
Vícenásobná dědičnost (někdy také označována jako mnohonásobná dědičnost) patří do funkcionality některých objektově orientovaných programovacích jazyků, kde třídy mohou dědit atributy a metody z více než jednoho předka. To představuje rozdíl od jednonásobné dědičnosti, kde třídy mohou mít pouze jednoho předka. Mnohonásobná dědičnost umožňuje třídě dědit datové atributy a implementaci z více rodičovských tříd. To však současně přináší určité negativní jevy. Nejznámějším příkladem takového problému je diamantový problém popsaný níže.
Podrobnější pohled
V objektově orientovaném programování dědičnost popisuje vztah mezi dvěma třídami objektů, kde jedna je potomkem té druhé. Potomek zdědí všechny schopnosti (implementované metody) a vlastnosti (datové atributy) svého předka.
Mnohonásobná dědičnost umožňuje programátorovi využívat schopnosti ze dvou nebo více tříd a sloučit je do jediné. Vhodný příklad můžeme najít v C++, kde existuje třída InputStream
a třída OutputStream
a jejich společný potomek InputOutputStream
.
Dědičnost bychom však měli používat s rozmyslem a mít na paměti, že potomek vždy musí být schopen reprezentovat svého předka. Například, když máme třídu Kočka
definovanou jako potomka třídy Savec
, je všechno v pořádku, savec může být reprezentován kočkou. Pokud se ale například pokusíme vytvořit KruhovouVýseč
(má tvar trojúhelníčku pizzy) tak, že ji definujeme jako potomka třídy Kolo
, kterou rozšíříme o úhel, nebude to fungovat. Jen si zkuste na motorku namontovat místo kola kruhovou výseč, daleko byste asi nedojeli.
Protože základem OOP je mimo jiné zapouzdření a dědičnost ji z principu porušuje, vícenásobná dědičnost ji tedy porušuje vícenásobně. Je tedy vhodné při návrhu architektury zvážit, zda by místo dědičnosti nebylo vhodnější použít alternativní postupy. Například návrhové vzory jako skládání či dekorování, a pro mnohonásobnou dědičnost lze použít princip mixin a trait (Scala).
Diamantový problém
Diamantový problém představuje nejasnost, která vznikne, když třída D
dědí ze dvou tříd B
a C
, které obě dědí ze třídy A
. Pokud metoda ze třídy A
je překryta ve třídě B
nebo C
nebo v obou a ve třídě D
překryta není, nastává problém, kterou metodu vlastně třída D
používá. Například, když třída Tlačítko
bude dědit ze třídy Obdélník
a ze třídy GrafickýPrvek
a obě tyto třídy budou dědit ze třídy Objekt
. V tomto případě, pokud zavoláme metodu equals
na Tlačítko
nebude jasné, jestli máme použít metodu ze třídy Obdélník
, GrafickýPrvek
nebo Objekt
.
Diamantový problém získal svoje označení podle tvaru, který v této situaci zaujímají třídy v diagramu tříd.
Řešení v různých jazycích
Různé programovací jazyky mají různé způsoby jak se vypořádat s problémy souvisejícími s mnohonásobnou dědičností.
- C++ defaultně pohlíží na společného předka
A
jako na dva rozdílné. Tedy předek třídyB
je z tohoto pohledu úplně jiná třídaA
než třídaA
jakožto předek třídyC
. Pokud dědičnost ze třídyA
do obou třídB
iC
je deklarována jako virtuální pak C++ vytvoří pouze jeden objektA
a zajistí jeho správné používání. Pokud jsou virtuální a nevirtuální dědičnosti namíchány, pak C++ vytvoří společný jeden objektA
pro virtuální dědičnost a jeden pro každou nevirtuální. Při volání metod musíme v C++ explicitně určit, která rodičovská třída bude při volání použita.
- Java od verze Java 8 zavedla výchozí metodu pro interface (rozhraní). Když tedy interface
B
aC
jsou potomky interfaceA
a oba implementují jeho abstraktní metodu. TřídaD
, která implementuje („je potomkem“)B
aC
musí poskytovat vlastní implementaci dané metody, jinak vznikne chyba při překladu. Implementovat takovou metodu také může znamenat pohopouhé zavolání super metody. Tedy jedné z výchozích metod z interfaceB
neboC
nebo obě dvě. Předchozí verze Javy nebyly vystaveny diamantovému problému, protože Java podporovala pouze jednonásobnou dědičnost. Se zavedením výchozích metod do interfejsů ve verzi 8 tak nově v Javě může vzniknout kolize implementace. Java překladač však obsahuje určitá pravidla, která brání vzniku diamantového problému. Například, pokud dědíte implementaci ze třídy a zároveň z interface, má přednost ta ze třídy. Pokud třída implementuje interfaceB
, který překrývá metodu z interfaceA
, pro třídu se použije ta bližší implementace, tedy z interfaceB
.
- JavaFX Script od verzi 1.2 podporuje mnohonásobnou dědičnost prostřednictvím koncepce mixin. Pokud se objeví konflikt, překladač nepovolí použití přímého zdědění proměnné nebo metody. Na každého „předka“ takové třídy se můžeme odkázat pomocí přetypování objektu na daný typ. Například
(individual as Person).printInfo();
- Scala podporuje princip vícenásobné dědičnosti pomocí trait, které umožňují mnohonásobnou dědičnost přidáním odlišností mezi hierarchií tříd a hierarchií traitů. Třída může dědit pouze z jedné třídy, ale může provést mixin traitů podle potřeby. Scala řeší kolizi jmen metod pravidlem – upřednostňuje dědění zprava a z hlubších (bližších) tříd přičemž redukuje nadbytečnosti. Takže v našem příkladu by výsledkem bylo pořadí: [
D
,C
,A
,B
,A
], které se posléze zredukuje na [D
,C
,B
,A
].
- Ruby třídy mají právě jednoho rodiče, ale mohou zároveň dědit z více modulů. To znamená, že mnohonásobná dědičnost jako taková sice v Ruby podporována není, ale používá mixin, čímž velmi elegantně obchází diamantový problém.
- Common Lisp nabízí implicitní řešení, ale ponechává programátorovi možnost, toto chování přepsat podle individuálních potřeb. Ve výchozím stavu se volají metody podle toho, v jakém pořadí jsou rodičovské třídy napsány v definici potomka. Programátor však může toto chování přepsat, tím, že určí přesné pořadí pro jednotlivé metody nebo stanoví pravidla pro kombinování metod. Pro řízení dědičnosti lze rovněž použít prostředky jako MOP (metaobject protocol), dynamický výběr a další vnitřní mechanizmy bez ohrožení stability systému.
- Perl používá seznam, podle kterého určuje, jakým způsobem bude dědění probíhat. Překladač upřednostňuje dědění z hlubších (bližších) tříd a použije první metodu, kterou najde. Pořadí, ve kterém jsou rodičovské třídy zapsány, tedy ovlivňuje sémantiku třídy potomka. Změníme-li tedy pořadí zápisu rodičovských tříd, změní se nám struktura schématu rodičovských tříd. V ukázkovém příkladu by třída
B
byla prohlížena před třídouC
což by fakticky znamenalo, že metody a atributy ze třídyA
jsou děděny prostřednictvímB
. Pro Perl platí, že můžeme toto výchozí nastavení přepsat pomocí C3 linearizace od verze 5. A od verze 6 má již C3 linearizace přednost před těmito pravidly.
- Python má stejnou strukturu jako Perl. Sémantika třídy je rovněž ovlivněna pořadím zápisu rodičovských tříd. Na rozdíl od něj však řešení problémů vícenásobné dědičnosti podporuje přímo v syntaxi jazyka. Python se s tímto vypořádal pomocí tříd nového typu, které všechny mají shodného předka třídu
Object
. Python vytváří seznam tříd za použití C3 linearizačního algoritmu. Tento algoritmus zajistí dvě podmínky: potomek uvádí svého předka a když třída dědí z více tříd je dbáno na pořadí. V našem případě by tedy pořadí tříd bylo:D
,B
,C
,A
.
V jednonásobné dědičnosti
Jazyky, které používají pouze jednoduchou dědičnost, tedy takové, kde třída může dědit pouze z jednoho předka, nemusí diamantový problém vůbec řešit. Důvodem pro to je, že takové jazyky mají nejvýše jedinou implementaci jakékoliv metody na jakékoliv úrovni dědičnosti bez ohledu na opakování nebo umístění metod. Je běžné, že tyto jazyky dovolují, aby třídy byly implementovány rozhraními (interface). Interface obsahuje deklaraci metod, ale již ne jejich implementaci. Tento přístup používá například ActionScript, C#, D, Java, Nemerle, Object Pascal (Delphi), Objective-C, Smalltalk, Swift a další. Kromě Smalltalku dovolují všechny zmíněné jazyky mnohonásobnou implementaci rozhraní.
C3 linearizace
Jedná se o algoritmus používaný k určení, která metoda by měla být zděděna při používání vícenásobné dědičnosti. Někdy bývá označována jako MRO neboli Method Resolution Order (určení pořadí metod). Je implemtována například v jazyce Perl od verze 6 a v jazyce Python od verze 2.3.