Lekce 7 - Dědičnost a polymorfismus
V minulé lekci, C# - Aréna s bojovníky, jsme dokončili naši arénu simulující zápas dvou bojovníků.
Dnes si opět rozšíříme znalosti o objektově orientovaném
programování. V úvodní lekci do OOP jsme si říkali, že OOP stojí na
třech základních pilířích: na zapouzdření,
dědičnosti a polymorfismu. Zapouzdření i
používání modifikátoru private
jsou nám již dobře známé.
Dnes se podíváme na zbylé dva pilíře.
Dědičnost
Dědičnost je jedna ze základních vlastností OOP a slouží ke tvoření nových datových struktur na základě starých. Vysvětleme si to na jednoduchém příkladu:
Budeme programovat informační systém. To je docela reálný příklad.
Abychom si však učení zpříjemnili, bude se jednat o informační systém
pro správu zvířat v zoo
Náš systém budou používat dva typy uživatelů: uživatel a administrátor.
Uživatel je běžný ošetřovatel zvířat, který bude moci upravovat
informace o zvířatech, např. jejich váhu nebo rozpětí křídel.
Administrátor bude moci také upravovat údaje o zvířatech, a navíc
zvířata přidávat či mazat z databáze. Z atributů bude mít navíc
telefonní číslo, aby ho bylo možné kontaktovat v případě výpadku
systému. Bylo by jistě zbytečné a nepřehledné, kdybychom si museli
definovat obě třídy úplně celé, protože mnoho vlastností těchto dvou
objektů bude společných. Uživatel i administrátor budou mít jistě jméno
i věk a budou se moci přihlásit a odhlásit. Nadefinujeme si tedy pouze
třídu Uzivatel
(nepůjde o funkční ukázku, dnes nás zajímá
pouze teorie, programovat budeme příště):
class Uzivatel { private string jmeno; private string heslo; private int vek; public bool Prihlasit(string heslo) { // ... } public bool Odhlasit() { // ... } public void NastavVahu(Zvire zvire) { // ... } // ... }
Třídu jsme pouze naznačili, ale jistě si ji dokážeme dobře
představit. Bez znalosti dědičnosti bychom třídu Administrator
definovali asi takto:
class Administrator { private string jmeno; private string heslo; private int vek; private string telefonniCislo; public bool Prihlasit(string heslo) { // ... } public bool Odhlasit() { // ... } public void NastavVahu(Zvire zvire) { // ... } public void PridejZvire(Zvire zvire) { } public void VymazZvire(Zvire zvire) { } // ... }
Vidíme, že máme ve třídě spoustu redundantního (duplikovaného) kódu.
Jakékoli změny nyní musíme provádět v obou třídách, kód se nám velmi
komplikuje. Namísto toho použijeme dědičnost. Definujeme tedy třídu
Administrator
tak, aby dědila z třídy Uzivatel
.
Atributy a metody uživatele tedy již nemusíme znovu definovat, jazyk C# nám
je do třídy sám dodá:
class Administrator: Uzivatel { private string telefonniCislo; public void PridejZvire(Zvire zvire) { } public void VymazZvire(Zvire zvire) { } // ... }
Vidíme, že ke zdědění jsme použili operátor :
. V
anglické literatuře najdete dědičnost pod slovem inheritance.
V příkladu výše nebudou v potomkovi přístupné privátní atributy, ale
pouze atributy a metody s modifikátorem public
. Atributy a metody
s modifikátorem private
jsou chápány jako speciální logika
konkrétní třídy. Ta je potomkovi utajena, a i když ji vlastně používá,
nemůže ji měnit. Abychom dosáhli požadovaného výsledku, použijeme
nový modifikátor přístupu protected
, který
funguje stejně jako private
, ale dovoluje tyto atributy dědit.
Začátek třídy Uzivatel
by tedy vypadal takto:
class Uzivatel { protected string jmeno; protected string heslo; protected int vek; // ...
Když si nyní vytvoříme instance uživatele a administrátora, oba budou
mít např. atribut jmeno
a metodu Prihlasit()
. C#
třídu Uzivatel
zdědí a doplní nám automaticky všechny její
atributy.
Výhody dědění jsou jasné, nemusíme opisovat oběma třídám tytéž atributy, ale stačí dopsat jen to, v čem se liší. Zbytek se podědí. Přínos je obrovský. Můžeme rozšiřovat existující komponenty o nové metody a tím je znovu využívat. Nemusíme psát spousty redundantního (duplikovaného) kódu. A hlavně – když změníme jediný atribut v mateřské třídě, automaticky se tato změna podědí všude. Nedojde tedy k tomu, že bychom museli atribut měnit ručně u dvaceti tříd a někde na něj zapomněli a způsobili tím chybu. Jsme lidé a chybovat budeme vždy, musíme tedy používat takové programátorské postupy, abychom měli možností chybovat co nejméně.
O mateřské třídě se někdy hovoří jako o předkovi (zde
Uzivatel
) a o třídě, která z ní dědí, jako o potomkovi (zde
Administrator
). Potomek může přidávat nové metody nebo si
uzpůsobovat metody z mateřské třídy (viz dále). Můžeme se setkat i s
pojmy nadtřída a podtřída.
Další možností, jak objektový model navrhnout, by bylo
zavést mateřskou třídu Uzivatel
, která by sloužila pouze k
dědění. Ze třídy Uzivatel
by potom dědila jak třída
Osetrovatel
, tak Administrator
. To by se však
vyplatilo až při větším počtu typů uživatelů. V takovém případě
hovoříme o hierarchii tříd, čímž se budeme zabývat ke konci této sekce.
Náš příklad byl jednoduchý, a proto nám stačily pouze dvě třídy.
Existují tzv. návrhové vzory, které obsahují osvědčená
schémata objektových struktur pro známé případy užití. Zájemci je
naleznou popsané v sekci Návrhové vzory. Je to
velmi zajímavá, avšak již pokročilejší problematika. V objektovém modelování se dědičnost znázorňuje
graficky jako prázdná šipka směřující k předkovi. V našem případě by
grafická notace vypadala takto:
Datový typ při dědičnosti
Obrovskou výhodou dědičnosti je, že když si vytvoříme
proměnnou s datovým typem mateřské třídy, můžeme do ní bez problému
ukládat i její potomky. Je to dané tím, že potomek obsahuje vše,
co obsahuje mateřská třída, splňuje tedy "požadavky" (přesněji obsahuje
rozhraní) datového typu. A k tomu má oproti mateřské třídě něco navíc.
Můžeme si tedy vytvořit pole typu Uzivatel
a v něm mít jak
uživatele, tak administrátory. S proměnnou to tedy funguje takto:
Uzivatel u = new Uzivatel("Jan Novák", 33); Administrator a = new Administrator("Josef Nový", 25); // Nyní do uživatele uložíme administrátora: u = a; // Vše je v pořádku, protože uživatel je předek // Zkusíme to opačně a dostaneme chybu: a = u;
Jazyk C# nabízí mnoho konstrukcí, jak s typy instancí při dědičnosti operovat. Podrobně se na ně podíváme během kurzu, nyní si ukažme jen to, jak můžeme ověřit typ instance v proměnné:
Uzivatel u = new Administrator("Josef Nový", 25); if (u is Administrator) Console.WriteLine("Je to administrátor"); else Console.WriteLine("Je to uživatel");
Pomocí operátoru is
se můžeme zeptat, zda
je objekt daného typu. Kód výše otestuje, zda je v proměnné u
uživatel, nebo jeho potomek administrátor.
Jazyky, které dědičnost podporují, umí dědičnost buď jednoduchou, kde třída dědí jen z jedné třídy, nebo vícenásobnou, kde třída dědí hned z několika tříd najednou. Vícenásobná dědičnost se v praxi příliš neosvědčila. Časem si řekneme proč a ukážeme si i to, jak ji obejít. C# podporuje pouze jednoduchou dědičnost, s vícenásobnou se můžete setkat např. v C++.
Polymorfismus
Nenechme se odradit hrozivým názvem této techniky, protože je v jádru
velmi jednoduchá. Polymorfismus umožňuje používat jednotné rozhraní pro
práci s různými typy objektů. Mějme například mnoho objektů, které
reprezentují nějaké geometrické útvary (kruh, čtverec, trojúhelník).
Bylo by jistě přínosné a přehledné, kdybychom s nimi mohli komunikovat
jednotně, ačkoli se objekty liší. Můžeme zavést třídu
GeometrickyUtvar
, která by obsahovala atribut barva
a
metodu Vykresli()
. Všechny geometrické tvary by potom dědily z
této třídy její interface (rozhraní). Objekty kruh a čtverec se ale jistě
vykreslují jinak. Polymorfismus nám umožňuje přepsat si
metodu Vykresli()
u každé podtřídy tak, aby
dělala, co chceme. Rozhraní tak zůstane zachováno a my nebudeme
muset přemýšlet, jak se to u onoho objektu volá.
Polymorfismus bývá často vysvětlován na obrázku se zvířaty, která
mají všechna v rozhraní metodu Speak()
, ale každé si ji
vykonává po svém.
Podstatou polymorfismu je tedy metoda nebo metody, které mají všichni potomci definované se stejnou hlavičkou, ale jiným tělem.
Polymorfismus si spolu s dědičností vyzkoušíme v příští lekci,
Aréna s mágem (dědičnost a polymorfismus), na bojovnících v naší aréně. Přidáme mága, který bude metodu
Utoc()
vykonávat po svém pomocí many, ale jinak zdědí
chování a atributy bojovníka. Zvenčí tedy vůbec nepoznáme, že mág není
bojovník, protože bude mít stejné rozhraní. Bude to zábava