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