Dědičnost a polymorfismus

Dědičnost aneb jeden ze základních principů OOP

Dědičnost je jedna ze základních vlastností OOP a slouží k tvoření nových datových struktur na základě starých. Inspirace je (jako u všech aspektů OOP) ve skutečném světě. Pro implementování dědičnosti potřebujeme rodiče (předka) a dítě (potomka). Potomek přebírá atributy (proměnné) předka i jeho schopnosti (metody). Potomek tedy "vylepšuje" předka, neboť má vše, co předek, + své věci navíc.

V okruhu 2.4.4 se bavíme o modifikátorech přístupu. Víme, že prvek (proměnná, či metoda) může být public (všude viditelná), private (viditelná pouze v rámci třídy ke které náleží) a protected. protected se používá právě v rámci dědičnosti a říká, že prvek je viditelný ve třídě, které náleží a ve všech potomcích této třídy. Pokud je modifikátor přístupu prvku jiný než public, nelze k němu přistupovat zvenčí (Nelze k němu přistupovat skrze instanci).

Na následujících obrázcích se nachází příklad dědičnosti. Všimněme si aplikování modifikátorů přístupu.

Rodičovská třída
obr. 1: Rodičovská třída
Vlastní zdroj

Třída Shape je rodič. Obsahuje privátní atribut color. K tomuto atributu nelze přistupovat jinde, než v rámci této třídy.

Znamená to, že následující kus kódu vyhodí chybu.

Shape s = new Shape();
s.color = "black"; // Nemůžeme přistoupit k proměnné color skrze instanci, protože je private.

Skrze instanci můžeme přistupovat pouze k veřejným členům třídy.

Shape s = new Shape();
s.setColor("black") // Toto je správně, metoda setColor je public.

Speciálním případem je modifikátor protected. Při přístupu skrze instanci se chová stejně, jako private. protected string type; Rozdíl je, že k tomuto prvku můžeme přistupovat v potomcích třídy Shape. Viz metoda init() ve třídě Triangle.

Potomek třídy Shape
obr. 2: Potomek třídy Shape
Vlastní zdroj

Ač atributy perimeter a area nikde ve třídě Triangle nevidíme, v metodě init() k nim přistupujeme. Je tomu tak proto, že třída Triangle rozšiřuje svého rodiče, třídu Shape. A jak jsme řekli výše, potomek obsahuje to samé, co rodič + něco navíc. (Atributy a, b, c).

Pozor: Pokud je atribut rodičovské třídy private a potomek ho nevidí, neznamená to, že potomek tento atribut neobsahuje. V našem příkladu potomek nemůže přímo přistupovat k atributu color, ale může k němu přistupovat skrze metody setColor() a getColor(). Říkáme, že potomek zdědil atribut color jako private (privátní).

Ve výsledném programu bude využití našich dvou tříd následující.

Využití dědičnosti v praxi
obr. 3: Využití dědičnosti v praxi
Vlastní zdroj

Za zmínku stojí řádek Console.WriteLine(t.getType()). Proč nemůžeme napsat Console.WriteLine(t.type)?. Protože atribut type je protected. Zde bychom se snažili přistoupit k protected atributu zvenčí (skrze instanci), což nelze.

Celému tomuto principu, jež jde ruku v ruce s dědičností se říká enkapsulace (viz okruh 2.4.4).

Přiřazení hodnoty mezi předkem a potomkem

Poněvadž platí, že potomek obsahuje to samé co předek, + něco navíc, tak můžeme předkovi přiřadit potomka. Naopak to nejde, protože potomek má některé atributy navíc, které by zůstaly "nevyplněny"

Shape s = new Triangle(a,b,c); // Tato je to správně. Deklarujeme nový objekt typu Shape. A tomuto objektu říkáme, budeš trojúhelník.
Triangle T = new Shape(); // Toto je špatně. A ani to z logického hlediska nedává smysl.

Předek může samozřejmě mít potomků více.

Triangle t = new Triangle(a,b,c,);
Square s = new Square(a);
Circle c = new Circle(r);

List<Shape> shapes = new List<Shape>();
//Stále platí to samé, předkovi lze přiřadit potomka, nikoliv naopak.
shapes.add(t);
shapes.add(s);
shapes.add(c);

Hierarchie volání konstruktoru

Při vytváření potomka platí, že první se zavolá konstruktor předka a poté se volá konstruktor odpovídajícího potomka. Konstruktory se volají tzv. zdola nahoru. Pokud má tedy předek také svého předka, volá se první konstruktor tohoto předka.

Hierarchie dědění
obr.4: Hierarchie dědění
Vlastní zdroj

Vícenásobná dědičnost

Stejně jako může předek mít potomka, tak může i potomek mít svého potomka. Aplikují se stejná pravidla při dědění, jako v předchozím případě. Všechno co má rodič, má i potomek.

Dědění z potomka
obr.5: Dědění z potomka
Vlastní zdroj

V některých jazycích, jako je například C++, je možné dědit z více předků zároveň. Potomek v takovémto případě dědi atributy i metody ze dvou tříd. Stejně tak se volají konstruktory těchto tříd před zavoláním třídy potomka. Dědičnost v C++ zde.

Virtuální metody, přepisování metod

Výše uvedených vlastností dosáhneme implementací tzv. virtuálních metod. Jedná se o metody předka, které může potomek přímo přepsat. Pokud poté přiřadíme předkovi potomka a na tohoto potomka zavoláme metodu, kterou přepsal, vykoná se metoda potomka. Pokud bychom metodu v předkovi jako virtual neoznačili a poté ji v potomkovi nepřepsali, vykonala by se metoda předka. (Viz příklad, obrázek č. 6).

Implementace těchto metod se liší v závislosti na jazyku. v C# a C++ k tomu slouží prefix virtual v rodičovské třídě a prefix override v potomkovi. V Javě jsou všechny metody přirozeně virtuální, není tedy třeba specifikovat v rodiči, které lze přepsat, neboť jdou pomocí anotace @Override přepsat všechny (pokud jejich hlavička neobsahuje klíčové slovo final).

Následuje příklad jednoduché dědičnosti bez virtuálních metod.

"Nevyužití virtuálních metod: předek
obr.6: Nevyužití virtuálních metod: předek
Vlastní zdroj
"Nevyužití virtuálních metod: potomek
obr.7: Nevyužití virtuálních metod: potomek
Vlastní zdroj
Volání stejně pojmenované metody v předkovi i v potomkovi
obr.8: Volání stejně pojmenované metody v předkovi i v potomkovi
Vlastní zdroj
Výstup
obr.9: Výstup
Vlastní zdroj

Můžeme vidět, že k přepsání nedošlo (Kdybychom použili Dog d = new Dog();, tak by nebyl problém). Ale logicky, by k přepsání dojít mělo. Máme zvíře ( Animal a), zvíře je pes(a = new Dog();). A my se zvířete (ne přímo psa) ptáme "co jsi zač?". Důvodem, proč se tak nestalo je fakt, že ač jsme do instance třídy Animal uložili objekt typu Dog, tak ukazatel tohoto objektu d stále ukazuje na typ Animal. Což by se nestalo při Dog d = new Dog();.

Pojďme to zkusit znovu, tentokrát s využitím virtuálních metod.

"Využití virtuálních metod: předek
obr.10: Využití virtuálních metod: předek
Vlastní zdroj
"Využití virtuálních metod: potomek
obr.11: Využití virtuálních metod: potomek
Vlastní zdroj
Volání stejně pojmenované metody v předkovi i v potomkovi
obr.12: Volání stejně pojmenované metody v předkovi i v potomkovi
Vlastní zdroj
Výstup
obr.13: Výstup
Vlastní zdroj

Nyní dostáváme chtěný výstup. Důvodem je tzv. Tabulka virtuálních metod. Jedná se o vnitřní strukturu, která si drží informace o tom, jaký potomek přepisuje jakou virtuální metodu v jakém předkovi. Virtuální metody jsou základním způsobem dosažení polymorfismu.

Čistě virtuální metody

Jedná se o metody, které nemají tělo a pokud je potomek zdědí, MUSÍ je implementovat. Pokud třída obsahuje jenom čistě virtuální metody, nazývá se Interface. Viz. okruh 2.4.6

Polymorfismus aneb jeden ze základních principů OOP

Polymorfismus je vlastnost programovacího jazyka, speciálně v objektově orientovaném programování, která umožňuje objektům volání jedné metody se stejným jménem, ale s jinou implementací.

Polymorfismus umožňuje:

  • jednomu objektu volat jednu metodu s různými parametry (ad-hoc polymorfismus);
  • objektům odvozeným z různých tříd volat tutéž metodu se stejným významem v kontextu jejich třídy, často pomocí rozhraní;
  • přetěžování operátorů neboli provedení rozdílné operace v závislosti na typu operandů;
  • jedné funkci dovolit pracovat s argumenty různých typů (parametrický polymorfismus, ne ve všech programovacích jazycích).

Implementace polymorfismu se různí, nejčastěji ji lze dosáhnout za pomocí abstraktních tříd a interface Viz. okruh 2.4.6 .Implementací polymorfismu dosáhneme abstrakce.

Citace