Lekce 5 - Tvorba OOP diáře v JavaScriptu
V minulé lekci, Referenční a hodnotové datové typy v JavaScriptu, jsme si vysvětlili rozdíly mezi hodnotovými a referenčními datovými typy. Již víme, že když uložíme instanci třídy do nějaké proměnné, je v ní ve skutečnosti uložena reference (odkaz) na tuto instanci. Můžeme tak používat jednu instanci z několika proměnných nebo ji jednoduše předávat, aniž by se zkopírovala.
V dnešním tutoriálu objektově orientovaného programování v JavaScriptu začneme programovat elektronický diář. Do diáře budeme schopni zadávat úkoly, mazat je, vypisovat a označovat jako splněné. Naším cílem bude tento výsledek:
Opakování OOP
Při tvorbě diáře využijeme také své dosavadní znalosti objektově orientovaného programování (OOP). Připomeňme si v rychlosti, co o OOP již víme. Základní jednotkou je objekt tvořený vlastnostmi a metodami. Abychom mohli vytvořit objekt, musíme si nejprve vytvořit třídu. Třída je vzor, podle kterého se objekty vytváří a který definuje, jaké vlastnosti a metody dané objekty mají mít.
Například v námi vytvářeném elektronickém diáři budeme mít třídu
Zaznam
definující vlastnosti jako název, datum a stav splnění.
Tato třída bude šablonou, podle které se vytvoří každý záznam (objekt)
s požadovanými vlastnostmi.
Základy jsme si osvěžili a můžeme se podívat na něco nového
Základní pilíře OOP
OOP stojí na třech základních pilířích:
- zapouzdření,
- dědičnost,
- polymorfismus.
Tyto pilíře si v kurzu postupně představíme. Dnes použijeme první z nich.
Příprava projektu
Nejdříve se ale zamyslíme nad tím, co vše budeme potřebovat. Co se
týče JavaScriptu, vytvoříme si složku js/
a v ní tři
skripty:
Diar.js
,Zaznam.js
,obsluha.js
.
Dále si v kořenové složce vytvoříme jednoduchou HTML stránku
index.html
s:
- formulářem na přidání úkolu (záznamu) nahoře,
- výpisem úkolů (záznamů) dole.
Soubor index.html
bude vypadat takto:
<!DOCTYPE html> <html lang="cs-cz"> <head> <meta charset="UTF-8"> <title>Diář</title> </head> <body> <h1>Diář</h1> <div> <input type="text" id="nazev" placeholder="Vyplňte název úkolu"><br> <input type="date" id="datum" placeholder="Vyplňte datum"><br> <button id="potvrdit">Uložit úkol</button> </div> <div id="seznam-ukolu"> </div> <script src="js/Diar.js"></script> <script src="js/Zaznam.js"></script> <script src="js/obsluha.js"></script> </body> </html>
Do elementu <div>
jsme vložili dva elementy
<input>
typu text
a date
na název
úkolu a jeho datum. Do stejného elementu <div>
jsme
přidali ještě i tlačítko na potvrzení. Níže jsme pak vložili druhý
<div>
pro seznam úkolů a rovnou i naodkazovali naše
skripty.
Stránka vypadá v prohlížeči následovně:
Vzhled nyní nebudeme příliš řešit.
Záznam
Začneme se souborem Zaznam.js
. Jak asi tušíme, soubor bude
obsahovat třídu Zaznam
reprezentující jeden záznam v našem
diáři. Dáme jí různé vlastnosti, které při vytvoření a po vytvoření
záznamu budeme moci měnit. Zatím to budou:
nazev
– název,datum
– datum,splneno
– informace, zda byl úkol splněn.
První dvě vlastnosti nastavíme konstruktorem (inicializujeme nový záznam s daným názvem a datem). Co se týká splnění úkolu, tak budeme předpokládat, že je po vytvoření vždy nesplněný:
class Zaznam { nazev; datum; splneno = false; constructor(nazev, datum) { this.nazev = nazev; this.datum = datum; } }
Připomeňme si, že klíčovým slovem this
zde
odkazujeme na aktuálně vytvářenou instanci třídy
Zaznam
.
Diář
Přesuňme se nyní k samotnému diáři. V souboru Diar.js
si
vytvoříme třídu Diar
.
Základní atributy
Třídě zatím přidáme dva třídní atributy:
zaznamy
– Záznamy diáře uložené v poli.jazyk
– Jazyk výpisu data záznamů, který se nám bude hodit v budoucnu, jelikož různé jazyky mají různé formáty. Například české datum vypadá jinak než anglické.
Třída Diar
bude nyní vypadat následovně:
class Diar { zaznamy = []; jazyk; }
Z minulých lekcí víme, že takto definované vlastnosti pomocí třídních atributů můžeme při vytváření a po vytvoření instance libovolně měnit, a to i zvenčí. To však v mnoha případech není žádoucí. Proč si nyní vysvětlíme spolu s pojmem zapouzdření.
Zapouzdření
Zapouzdření umožňuje skrýt některé metody a vlastnosti tak, aby zůstaly použitelné jen pro třídu zevnitř. Objekt si můžeme představit jako černou skřínku (anglicky blackbox), která má určité rozhraní (interface), přes které jí předáváme instrukce/data a ona je zpracovává.
Nevíme, jak to uvnitř funguje, ale víme, jak se navenek chová a používá. Nemůžeme tedy způsobit nějakou chybu, protože využíváme a vidíme jen to, co tvůrce třídy zpřístupnil.
Příkladem může být třída Clovek
, která bude mít
vlastnost datumNarozeni
a na jeho základě další vlastnosti jako
plnolety
a vek
. Kdyby někdo objektu zvenčí změnil
datumNarozeni
, přestaly by platit vlastnosti plnolety
a vek
. Říkáme, že vnitřní stav objektu by byl
nekonzistentní. Toto se nám ve strukturovaném programování
může klidně stát. V OOP však objekt zapouzdříme. Třídní atribut
odpovídající vlastnosti datumNarozeni
označíme jako
privátní a tím pádem bude jasné, že nechceme, aby nám
danou vlastnost někdo jen tak měnil. Naopak ven vystavíme metodu
zmenDatumNarozeni()
, která dosadí nové datum narození do
vlastnosti datumNarozeni
a zároveň provede potřebný přepočet
věku a přehodnocení plnoletosti. Použití objektu je bezpečné a aplikace
stabilní.
Zapouzdření tedy tlačí programátory používat objekt jen tím správným způsobem. Rozhraní (interface) třídy rozdělí na veřejně přístupné a vnitřní (privátní) strukturu.
Zapouzdření atributů
Vraťme se k naší třídě Diar
. Nyní jsou všechny atributy
třídy veřejně přístupné. My však nechceme a ani nepotřebujeme, aby se
daly odpovídající vlastnosti zvenčí modifikovat a hrozilo tak, že se
vnitřní stav objektu stane nekonzistentní. Chceme je tedy označit jako
privátní.
V takovém případě musíme atribut pojmenovat se znakem
mřížky #
na začátku, například
#zaznamy
. Vlastnost odpovídající danému atributu je poté
viditelná jen uvnitř třídy a zvenčí se JavaScript tváří, že vůbec
neexistuje. Pokud se pokusíme přistoupit k privátní vlastnosti zvenčí,
bude vyvolána chyba.
Při návrhu třídy všechny atributy i metody definujeme jako
privátní s mřížkou #
na začátku. Až v případě, že je
něco opravdu potřeba vystavit, mřížku nepoužijeme.
Přejmenujme tedy naše dosavadní atributy tak, aby byly privátní:
class Diar { #zaznamy = []; #jazyk; }
Atributy třídy Zaznam
však ponecháme veřejné.
Budeme k nim totiž chtít přistupovat ve třídě Diar
, tedy
zvenčí.
Inicializace jazyka
Dále si do třídy Diar
přidáme konstruktor, ve kterém
inicializujeme vlastnost #jazyk
na hodnotu z parametru:
class Diar { #zaznamy = []; #jazyk; constructor(jazyk = "cs-CZ") { this.#jazyk = jazyk; } }
Protože budeme chtít většinou české prostředí, definujeme parametru
konstruktoru výchozí hodnotu "cs-CZ"
. Tato hodnota se použije,
pokud parametr při vytváření instance nezadáme.
Vybírání elementů na stránce
Ve třídě budeme potřebovat pracovat s DOM elementy na stránce. Existuje pravidlo, které říká, že je dobrým principem oddělení práce s logikou od práce s uživatelským rozhraním. Tohoto základního programátorského pravidla využívá například architektura MVC.
V javascriptových frameworcích, ke kterým se dostaneme po tomto kurzu, je architektura aplikace postavená tak, aby se nemíchal kód vybírající elementy na stránce s dalším kódem aplikace. Bylo by to totiž velmi nepřehledné.
My si v základním OOP kurzu nebudeme vytvářet žádnou komplikovanou architekturu, ale budeme se snažit umístit vybírání elementů ze stránky na jedno jediné místo ve třídě.
Do třídy si tedy přidáme několik dalších privátních atributů pro elementy ze stránky, které budeme ve třídě dále potřebovat:
#nazevInput
– element<input>
s názvem nově přidávaného záznamu,#datumInput
– element<input>
s datem nově přidávaného záznamu,#potvrditButton
– ukládací tlačítko,#vypisElement
– element pro výpis záznamů uložených v diáři.
Nové atributy umístíme pod ty existující a rovnou je napojíme na odpovídající DOM prvky ze stránky:
class Diar { #zaznamy = []; #jazyk; #nazevInput = document.getElementById("nazev"); #datumInput = document.getElementById("datum"); #potvrditButton = document.getElementById("potvrdit"); #vypisElement = document.getElementById("seznam-ukolu"); ... }
Elementy vybíráme pomocí metody getElementById()
.
Nikam jinam ve třídě nebudeme dále vkládat žádný další výběr elementů, protože by to bylo velmi nepřehledné.
Metody
Přejděme k metodám diáře.
Metoda #nastavUdalosti()
Aby náš konstruktor nebyl příliš dlouhý, vyčleníme nastavení
obslužných událostí elementům na stránce do oddělené privátní metody
#nastavUdalosti()
. V našem případě jde jen o obsluhu kliknutí
na tlačítko potvrzení.
Zatím si přidejme chybnou naivní implementaci metody pro obsluhu tlačítka, která nebude fungovat. Proč si vysvětlíme za okamžik:
#nastavUdalosti() { this.#potvrditButton.onclick = function() { // tento kód nebude fungovat const zaznam = new Zaznam(this.#nazevInput.value, this.#datumInput.value); this.#zaznamy.push(zaznam); this.vypisZaznamy(); }; }
Metoda na událost onclick
tlačítka
this.#potvrditButton
naváže obslužnou funkci. Zde je ještě
vše v pořádku. Uvnitř se vezmou hodnoty z inputů a na jejich základě se
vytvoří nová instance záznamu. Tuto novou instanci vložíme do pole
záznamů #zaznamy
. Všechny záznamy poté vypíšeme na stránku
pomocí metody vypisZaznamy()
, kterou si implementujeme za
chvíli.
Co je tedy špatně? Pokud jste dávali v Základních konstrukcích JavaScriptu pozor, víte, že:
Při použití function
pro
obsluhu událostí elementů se mění kontext a klíčové
slovo this
následně ukazuje na element, který událost
způsobil. this
tedy přestane obsahovat instanci naší
třídy. Toto chování je chyba v návrhu jazyka JavaScript a
zabraňuje nám pracovat s instančními proměnnými a metodami v obsluze
událostí.
Arrow functions
Způsobů, jak tento problém obejít, je hned několik. My si zmíníme to
nejjednodušší řešení. K obsluze události použijeme takzvanou
arrow function, což je "zkrácený" zápis funkce. Název
vychází ze znaku šipky (anglicky arrow), kterým se tyto funkce zapisují.
Arrow functions byly do JavaScriptu přidány až později, a proto
chybou změny kontextu již netrpí. Přesněji ani žádný svůj
kontext nemají a klíčové slovo this
v nich
obsahuje to, co v něm bylo předtím, beze změny.
Pokud tedy vytvoříme arrow function uvnitř třídy,
klíčové slovo this
bude v této funkci obsahovat stejnou hodnotu
jako this
dané třídy, to znamená aktuální instanci
dané třídy. Vše tak bude fungovat dle očekávání.
Arrow function do proměnné uložíme následujícím způsobem:
nazevFunkce = () => { // tělo funkce }
Pokud bychom chtěli funkci poslat i parametry, můžeme je psát do závorek, jak jsme zvyklí:
nazevFunkce = (parametr) => { // tělo funkce }
Pokud bychom chtěli poslat pouze jeden, můžeme závorky dokonce i vynechat:
nazevFunkce = parametr => { // tělo funkce }
Nyní tedy metodu #nastavUdalosti()
opravíme, aby v obslužné
funkci fungovalo klíčové slovo this
. To totiž používáme k
přístupu k vlastnostem a metodám naší třídy:
#nastavUdalosti() { this.#potvrditButton.onclick = () => { const zaznam = new Zaznam(this.#nazevInput.value, this.#datumInput.value); this.#zaznamy.push(zaznam); this.vypisZaznamy(); }; }
Metodu #nastavUdalosti()
zavoláme na konci konstruktoru:
constructor(jazyk = "cs-CZ") { this.#jazyk = jazyk; this.#nastavUdalosti(); }
Metoda vypisZaznamy()
V metodě vypisZaznamy()
pro výpis záznamů nás asi nic
nepřekvapí. Funguje velmi podobně jako výpis zaměstnanců, který jsme
vytvářeli v předešlých lekcích:
vypisZaznamy() { this.#vypisElement.innerHTML = ""; for (const zaznam of this.#zaznamy) { this.#vypisElement.innerHTML += `<h3>${zaznam.nazev}</h3>kdy: ${zaznam.datum}<br>splněno: ${zaznam.splneno}`; } }
Metoda maže veškerý obsah z našeho elementu pro výpis a vypisuje do něj postupně záznamy pomocí cyklu. Za zmínku ještě stojí, že jsme metodu ponechali veřejnou. Budeme ji totiž volat i zvenčí.
Výpis záznamů
Nakonec už jen musíme v souboru obsluha.js
vytvořit instanci
třídy Diar
a vypsat uložené záznamy pomocí metody
vypisZaznamy()
:
const diar = new Diar(); diar.vypisZaznamy();
Diář zatím ihned po svém vytvoření neobsahuje žádné záznamy. Dále v kurzu je ale budeme načítat z lokálního úložiště.
Pokud nyní naši aplikaci spustíme v prohlížeči, bude vypadat takto:
V následujícím cvičení, Řešené úlohy k 4.-5. 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 805x (2.69 kB)
Aplikace je včetně zdrojových kódů v jazyce JavaScript