Lekce 7 - Dědičnost a polymorfismus v Kotlin
V minulé lekci, Aréna s bojovníky v Kotlin, 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ím tutoriálu 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 nám je 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ží 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 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 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, dnes to bude jen teorie, programovat budeme příště):
class Uzivatel { private val jmeno: String = "" private val heslo: String = "" private val vek: Int = 0 fun prihlasit(heslo: String): Boolean { // ... } fun odhlasit(): Boolean { // ... } fun nastavVahu() { // ... } // ... }
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 { private val jmeno: String = "" private val heslo: String = "" private val vek: Int = 0 private val telefonniCislo: String = "" fun prihlasit(heslo: String): Boolean { // ... } fun odhlasit(): Boolean { // ... } fun nastavVahu(zvire: Zvire) { // ... } fun pridejZvire(zvire: Zvire) { // ... } fun 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, nejprve řekneme Kotlinu, že třída
Uzivatel může být děděná. Dále definujeme třídu
Administrator tak, aby z třídy Uzivatel dědila.
Atributy a metody uživatele tedy již nemusíme v administrátorovi znovu
definovat, Kotlin nám je do třídy sám dodá.
V Kotlinu jsou třídy ve výchozím stavu tzv. zapečetěné,
nelze z nich tedy dědit. Pokud přecházíte z Javy, můžeme situaci
přirovnat k tomu, jako by u tříd byl napsaný modifikátor
final.
Třídu Uzivatel tzv. otevřeme a tím zajistíme, že z ní
půjde dědit. To provedeme klíčovým slovem open:
open class Uzivatel { // ... zbytek metod a atributů ...
Dále definujeme třídu Administrator tak, že bude dědit z
třídy Uzivatel:
class Administrator: Uzivatel { private val telefonniCislo: String = ""; fun pridejZvire(zvire: Zvire) { // ... } fun 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.
private atributy 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. 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 nebo metody dědit.
Začátek třídy Uzivatel by tedy vypadal takto:
class Uzivatel { protected val jmeno: String = "" protected val heslo: String = "" protected val vek: Int = 0 // ...
Když si nyní vytvoříme instance uživatele a administrátora, oba budou
mít např. atribut jmeno a metodu prihlasit(). Kotlin
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 ty samé 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 všude podědí. Nedojde tedy k tomu, že bychom to museli měnit ručně u 20ti 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 tohoto kurzu.
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 vytvořit pole typu Uzivatel a v něm mít jak
uživatele, tak administrátory. S proměnnou to tedy funguje takto:
val u = Uzivatel("Jan Novák", 33); val a = 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 Kotlinu 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é:
val u = Administrator("Josef Nový", 25) if (u is Administrator) { println("Je to administrátor") } else { println("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. Kotlin podporuje pouze jednoduchou dědičnost, s vícenásobnou dědičností se můžete setkat např. v 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 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.
V příští lekci, Aréna s mágem - Dědičnost a polymorfismus v Kotlin, si polymorfismus spolu s dědičností
vyzkoušíme 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 atributy bojovníka. Zvenčí tedy vůbec nepoznáme, že to není
bojovník, protože bude mít stejné rozhraní. Bude to zábava 

