Lekce 14 - Dědičnost a polymorfismus v JavaScriptu
V předchozím kvízu, Kvíz - JSON a AJAX v JavaScriptu, jsme si ověřili nabyté zkušenosti z předchozích lekcí.
Dnes se v tutoriálu OOP v JavaScriptu podíváme na dědičnost a polymorfismus.
Dědičnost v JavaScriptu
Dědičnost je jedna ze základních technik OOP a slouží k
tvoření nových datových struktur na základě starých. Pojďme si
to ukázat na příkladu. Vytvoříme si obecnou třídu Clovek
,
které přiřadíme vlastnosti jméno a věk a ještě metodu na
představení:
class Clovek { jmeno; #vek; constructor(jmeno, vek) { this.jmeno = jmeno; this.#vek = vek; } predstavSe() { return `Jmenuji se ${this.jmeno} a je mi ${this.#vek} let.`; } }
Kód by nás neměl ničím překvapit. Všimněme si akorát, že vlastnost pro věk jsem definovali jako privátní, aby nebyla přístupná zvenčí.
Rozšíření třídy
Řekněme, že si s touto třídou v aplikaci chvíli vystačíme. Co když pak ale potřebujeme lidi, kteří budou mít nějaké vlastnosti navíc? Například takový programátor je také člověk se jménem, věkem a schopností se představit. Navíc by ale měl umět programovací jazyk, ve kterém programuje.
Naivní řešení
Možná by nás napadlo vytvořit třídu Programator
,
zkopírovat do ní kód z třídy Clovek
a přidat jen navíc
vlastnost jazyk
a například také metodu
programuj()
:
class Programator { jmeno; #vek; jazyk; constructor(jmeno, vek, jazyk) { this.jmeno = jmeno; this.#vek = vek; this.jazyk = jazyk; } predstavSe() { return `Jmenuji se ${this.jmeno} a je mi ${this.#vek} let.`; } programuj() { return `Programuji v ${this.jazyk}...`; } }
Toto řešení je sice funkční, ale porušuje jednu z nejzákladnějších
dobrých praktik všech programátorů – princip DRY (Don't
Repeat Yourself, tedy neopakuje se). Více o něm najdeme v článku Best
practices pro vývoj softwaru - Základní praktiky Představme si, že se v
budoucnu rozhodneme třídu Clovek
upravit a zapomeneme, že je
rozkopírovaná do několika dalších souborů. Takové chyby se poté špatně
hledají a samozřejmě způsobí nefunkčnost aplikace. Takto tedy určitě ne
Řešení pomocí dědičnosti
Novou třídu Programator
opravdu vytvoříme, ale z třídy
Clovek
ji oddědíme. V JavaScriptu se pro
dědění používá v definici nové třídy klíčové slovo
extends
, takto:
class Potomek extends Predek
Konstruktory a dědičnost
Je tu ovšem malý háček. Jakmile potřebujeme ve třídě potomka
konstruktor, musíme v něm nejprve zavolat konstruktor předka pomocí funkce
super()
, případně mu předat potřebné parametry. Toto
volání musíme provést před použitím klíčového slova this
.
Je to i logické. Konstruktor objekt připravuje na použití a pokud má
předek nějaký konstruktor, potomek by jej měl ve svém konstruktoru zavolat,
aby vše inicializoval. Konstruktor předka musíme zavolat v konstruktoru
potomka i v případě, kdyby předek neměl definovaný svůj vlastní
konstruktor (i v tomto případě totiž konstruktor má, jen se mu vygeneroval
automaticky).
Pojďme tedy od naší třídy Clovek
oddědit novou třídu
Programator
:
class Programator extends Clovek { jazyk; constructor(jmeno, vek, jazyk) { super(jmeno, vek); this.jazyk = jazyk; } programuj() { return `Programuji v ${this.jazyk}...`; } }
Jak vidíme, máme zde konstruktor se třemi parametry. Dva z těchto
parametrů předáváme při zavolání super()
konstruktoru třídy Clovek
, tedy jméno a věk. Dále zde máme
již zmiňovanou vlastnost jazyk
, do které zde přiřazujeme
hodnotu ze stejnojmenného parametru. Třída programátor má tedy nyní tři
vlastnosti: jmeno
, #vek
a jazyk
a dvě
metody – odděděnou predstavSe()
a novou
programuj()
.
Nyní si v obsluze vytvoříme instanci třídy Programator
a
vyzkoušíme si zavolat obě metody:
const programator = new Programator("Šimon", 19, "JS"); document.write(` ${programator.predstavSe()}<br> ${programator.programuj()}<br> `);
Nyní se můžeme podívat na výsledek:
Výhody dědičnosti
Výhody dědění jsou jasné, nemusíme opisovat oběma třídám ty samé vlastnosti. 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 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ě.
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. JavaScript podporuje pouze jednoduchou dědičnost, s vícenásobnou dědičností se můžete setkat například v C++. Potomek samozřejmě může mít dalšího potomka a tak dále.
Polymorfismus v JavaScriptu
Nenechme se vystrašit 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. Vyzkoušejme si to na našich lidech.
Představení se
Člověk má definovanou metodu predstavSe()
, která pozdraví
tímto způsobem:
Přepsání metody
Nyní aplikujeme polymorfismus tak, že ve třídě Programator
metodu predstavSe()
z předka upravíme. Přepsání metody v
JavaScriptu je jednoduché – stačí ji znovu definovat. Naučme tedy
programátora představovat se nějakým programátorským způsobem,
například, že použije pozdrav "Hello world!":
class Programator extends Clovek { jazyk; constructor(jmeno, vek, jazyk) { super(jmeno, vek); this.jazyk = jazyk; } programuj() { return `Programuji v ${this.jazyk}...`; } predstavSe() { return `Hello world! Jmenuji se ${this.jmeno}.`; } }
Výsledek:
Pokud metodu predstavSe()
zavoláme na instanci třídy
Clovek
, vypíše původní text pozdravu. Získali jsme tedy
shodné rozhraní a jinou funkcionalitu podle konkrétního objektu.
Problém s privátními vlastnostmi
V přepsané metodě predstavSe()
záměrně nevypisujeme věk
programátora a nepřistupujeme tak k vlastnosti #vek
. Privátní
vlastnosti a metody třídy Clovek
totiž nejsou v
jejím potomkovi Programator
přístupné. Privátní 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á, a nemůže ji měnit.
Abychom mohli vlastnost #vek
použít ve třídě
Programator
, musíme ji ve třídě Clovek
označit
jako veřejnou. Tím ji ale učiníme přístupnou odkudkoliv a přijdeme tak o
výhody zapouzdření. Aby bylo alespoň z názvu vlastnosti jasné, že se k
ní zvenčí nemá přistupovat, pojmenujeme ji s podtržítkem _
na začátku, tedy _vek
:
class Clovek { jmeno; _vek; constructor(jmeno, vek) { this.jmeno = jmeno; this._vek = vek; } predstavSe() { return `Jmenuji se ${this.jmeno} a je mi ${this._vek} let.`; } }
Důležité je pamatovat na to, že tento způsob pojmenování
vlastností a metod je pouhou konvencí. Pokud se pokusíme
přistoupit k vlastnosti _vek
mimo třídu Clovek
nebo
Programator
, JavaScript nevyvolá žádnou
chybu.
Ve třídě Programator
nyní můžeme přistoupit k vlastnosti
pro věk a vypsat tak celé představení:
class Programator extends Clovek { jazyk; constructor(jmeno, vek, jazyk) { super(jmeno, vek); this.jazyk = jazyk; } programuj() { return `Programuji v ${this.jazyk}...`; } predstavSe() { return `Hello world! Jmenuji se ${this.jmeno} a je mi ${this._vek} let.`; } }
Výsledek:
V jiných jazycích, například v Javě nebo C#, se vlastnosti
a metody, které jsou přístupné pouze ve třídě a v jejich potomcích,
označují jako protected
.
Vylepšení
Všimněme si, že jsme použili základní představení z třídy
Clovek
, jen jsme před něj přidali text "Hello world!". Naše
řešení výše není ideální, pro více typů lidí bychom museli stále
kopírovat do každé třídy ten samý výchozí pozdrav a před něj teprve
dávat novou hlášku. A již víme, že podle DRY je jakékoli
kopírování stejného kódu špatně.
Základní představení může vracet metoda predstavSe()
třídy Clovek
a my ji můžeme zavolat z potomků stejně jako
předtím konstruktor člověka pomocí klíčového slova super
.
Její výstup pak jednoduše přidáme k textovému řetězci, který se bude
vracet:
class Programator extends Clovek { jazyk; constructor(jmeno, vek, jazyk) { super(jmeno, vek); this.jazyk = jazyk; } programuj() { return `Programuji v ${this.jazyk}...`; } predstavSe() { return `Hello world! ${super.predstavSe()}`; } }
Zkusme si vytvořit v obsluze instanci třídy Clovek
a zavolejme
metodu predstavSe()
, ať vidíme rozdíl:
const programator = new Programator("Šimon", 19, "JS"); document.write(` ${programator.predstavSe()}<br> ${programator.programuj()}<br> `); const clovek = new Clovek("Marek", 26); document.write(clovek.predstavSe());
Výpis bude vypadat takto:
Tak to je pro dnešek vše
Dole jsou opět k dispozici zdrojové kódy ke stažení.
V následujícím cvičení, Řešené úlohy k 14. lekci OOP v JavaScriptu, si procvičíme nabyté zkušenosti z předchozích lekcí.
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 175x (2.53 kB)
Aplikace je včetně zdrojových kódů v jazyce JavaScript