Lekce 7 - Dědičnost a polymorfismus v Dartu
V minulé lekci, Aréna s bojovníky v Dartu, jsme dokončili naši arénu, simulující zápas dvou bojovníků. Opět si 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: zapouzdření, dědičnosti a polymorfismu. Zapouzdření a používání modifikátoru private (podtržítko) nám je již dobře známé. V této lekci 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ží k 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 to 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 a mazat z databáze. Z vlastností
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 2 objektů je společných. Uživatel i administrátor budou
mít jistě jméno, 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, teď
to bude jen teorie, programovat budeme příště):
class Uzivatel { String _jmeno; String _heslo; int _vek; bool prihlasit(String heslo) { // ... } bool odhlasit() { // ... } void nastavVahu(Zvire zvire) { // ... } // ... }
Třídu jsem jen naznačil, ale jistě si ji dokážeme dobře představit.
Bez znalosti dědičnosti bychom třídu Administrator
definovali
asi takto:
class Administrator { String _jmeno; String _heslo; int _vek; String _telefonniCislo; bool prihlasit(String heslo) { // ... } bool odhlasit() { // ... } void nastavVahu(Zvire zvire) { // ... } void pridejZvire(Zvire zvire) { // ... } void vymazZvire(Zvire zvire) { // ... } // ... }
Vidíme, že máme ve třídě spoustu redundantního (duplikovaného) kódu.
Jakékoli změny musíme nyní provádět v obou třídách, kód se nám velmi
komplikuje. Nyní použijeme dědičnost, definujeme tedy třídu
Administrator
tak, aby z třídy Uzivatel
dědila.
Vlastnosti a metody uživatele tedy již nemusíme znovu definovat, Dart nám je
do třídy sám dodá:
class Administrator extends Uzivatel { String _telefonniCislo; void nastavVahu(Zvire zvire) { // ... } void pridejZvire(Zvire zvire) { // ... } void vymazZvire(Zvire zvire) { // ... } // ... }
Vidíme, že ke zdědění jsme použili klíčové slovo extends. V anglické literatuře najdete dědičnost pod slovem inheritance.
V příkladu výše nebudou v potomkovi přístupné privátní vlastnosti, ale pouze vlastnosti a metody s modifikátorem public. Private vlastnosti a metody jsou chápány jako speciální logika konkrétní třídy, která je potomkovi utajena, i když ji vlastně používá, nemůže ji měnit. Různé programovací jazyky, jako např. C# nebo Java, mají navíc i modifikátor protected (private pro venek, public pro potomky). Dart tento modifikátor nemá a rozlišuje pouze mezi veřejnými (public) a neveřejnými (private) věcmi. Okolo protected v OOP můžete najít hromadu diskusí pro i proti používání, protected totiž může rozporovat s principy YAGNI, LSP, OCP či třeba SRP. My se tedy budeme při návrzích více zaměřovat na přidávání samostatných částí, než-li na úpravou již existujících částí.
Když si nyní vytvoříme instance uživatele a administrátora, oba budou
mít např. vlastnost jmeno
a metodu prihlasit()
. Dart
třídu Uzivatel
zdědí a doplní nám automaticky všechny její
vlastnosti. Ty však nebudeme moci používat přímo, jelikož jsme je
nastavili ve třídě Uzivatel
jako private.
Výhody dědění jsou jasné, nemusíme opisovat oběma třídám ty samé vlastnosti, 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 jedinou vlastnost v mateřské třídě, automaticky se tato změna všude podědí. Nedojde tedy k tomu, že bychom to museli měnit ručně u 20 tříd a někde na to zapomněli a způsobili 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ůžete 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í. Z Uzivatel
by potom dědili Osetrovatel
a z
něj Administrator
. To by se však vyplatilo při větším počtu
typů uživatelů. V takovém případě hovoříme o hierarchii tříd, budeme
se tím zabývat ke konci této sekce. Náš příklad byl jednoduchý a proto
nám stačily pouze 2 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 však již
pokročilejší problematika a také velmi zajímavá. 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 ni 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 udělat seznam 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;
V Dartu je mnoho konstrukcí, jak operovat s typy instancí při dědičnosti. 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) print('Je to administrátor'); else print('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í, buď umí dědičnost 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 jak ji obejít. Dart podporuje pouze jednoduchou dědičnost, s vícenásobnou dědičností se můžete setkat např. v jazyce C++.
Polymorfismus
Nenechte se vystrašit příšerný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 liší. Můžeme zavést třídu
GeometrickyUtvar
, která by obsahovala vlastnost 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ý si bude metodu utoc()
vykonávat po svém pomocí many, ale jinak zdědí chování a vlastnosti
bojovníka. Zvenčí tedy vůbec nepoznáme, že to není bojovník, protože
bude mít stejné rozhraní. Bude to zábava.