7. diel - Dedičnosť a polymorfizmus vo Swift
V minulej lekcii, Aréna s bojovníkmi vo Swift , sme dokončili našu arénu, simulujúce
zápas dvoch bojovníkov. Dnes si opäť rozšírime znalosti o objektovo
orientovanom programovaní. V úvodnej lekcii do OOP sme si hovorili, že OOP
stojí na troch základných pilieroch: zapuzdrenie,
dedičnosti a polymorfizmu. Zapuzdrenie a
používanie modifikátora private
nám je už dobre známe. Dnes
sa pozrieme na zvyšné dva piliere.
Dedičnosť
Dedičnosť je jedna zo základných vlastností OOP a slúži k tvoreniu nových dátových štruktúr na základe starých. Vysvetlime si to na jednoduchom príklade:
Budeme programovať informačný systém. To je celkom reálny príklad, aby
sme si však učenie spríjemnili, bude to informačný systém pre správu
zvierat v ZOO Náš systém
budú používať dva typy užívateľov: užívateľ a administrátor.
Užívateľ je bežný ošetrovateľ zvierat, ktorý bude môcť upravovať
informácie o zvieratách, napr. Ich váhy alebo rozpätie krídel.
Administrátor bude môcť tiež upravovať údaje o zvieratách a navyše
zvieratá pridávať a mazať z databázy. Z vlastností bude mať navyše
telefónne číslo, aby ho bolo možné kontaktovať v prípade výpadku
systému. Bolo by určite zbytočné a neprehľadné, keby sme si museli
definovať obe triedy úplne celé, pretože mnoho vlastností týchto 2
objektov je spoločných. Užívateľ aj administrátor budú mať určite meno,
vek a budú sa môcť prihlásiť a odhlásiť. Nadefinujeme si teda iba triedu
Uzivatel
(nepôjde o funkčné ukážku, dnes to bude len teória,
programovať budeme nabudúce):
class Uzivatel { private var jmeno : String private var heslo : String private var vek : Int func prihlasit(heslo: String) -> Bool { // ... } func odhlasit() -> Bool { // ... } func nastavVahu(zvire: Zvire) { // ... } // ... }
Triedu som len naznačil, ale iste si ju dokážeme dobre predstaviť. Bez
znalosti dedičnosti by sme triedu Administrator
definovali asi
takto:
class Administrator { private var jmeno : String private var heslo : String private var vek : Int private var telefonniCislo : String func prihlasit(heslo: String) -> Bool { // ... } func odhlasit() -> Bool { // ... } func nastavVahu(zvire: Zvire) { // ... } func pridejZvire(zvire: Zvire) { } func vymazZvire(zvire: Zvire) { } // ... }
Vidíme, že máme v triede veľa redundantného (duplikovaného) kódu.
Akékoľvek zmeny musíme teraz vykonávať v oboch triedach, kód sa nám
veľmi komplikuje. Teraz použijeme dedičnosť, definujeme teda triedu
Administrator
tak, aby z triedy Uzivatel
dedila.
Vlastnosti a metódy užívateľa teda už nemusíme znovu definovať, Swift
nám ich do triedy sám dodá:
class Administrator: Uzivatel { private var telefonniCislo : String func pridejZvire(zvire: Zvire) { } func vymazZvire(zvire: Zvire) { } // ... }
Vidíme, že ku zdedenie sme použili operátor :
. V anglickej
literatúre nájdete dedičnosť pod slovom inheritance. Vo Swift sa často
označuje ako subclassing.
Viditeľnosť a modifikátory prístupu pre dedičnosť
V príklade vyššie nebudú v potomkovi prístupné privátnej vlastnosti,
ale len vlastnosti a metódy s modifikátorom public
či bez
modifikátora (čo je defaultne internal
). Vlastnosti a metódy s
modifikátorom private
sú chápané ako špeciálne logika
konkrétnej triedy, ktorá je potomkovi utajená, aj keď ju vlastne používa,
nemôže ju meniť.
Niektoré programovacie jazyky ponúkajú špeciálne modifikátor prístupu
protected
, ktorý označuje metódy a vlastnosti, ktoré sú
dostupné iba triede a jej potomkom, ktorí z nej dedia. Swift tento
modifikátor nepodporuje a oficiálne vysvetlenie jeho absencia je, že dedičom
trieda rovnako môže tieto metódy a vlastnosti poskytnú pomocou nových
public
/ internal
metód a vlastností. A tiež by
protected
dobre nefungovalo s extension
s, čo je
veľká kapitola vo Swift a neskôr si ich vysvetlíme. Pretože by sme však
radi docielili takého zapuzdrenie, aby sme boli schopní niektoré vlastnosti
alebo metódy skryť zvonku, ale sprístupniť pre potomkov danej triedy,
pomôžeme si inak.
Fileprivate
Najbližšie sa požadovanému výsledku priblížime pomocou modifikátora
fileprivate
. Ako ste asi odvodili z názvu, metódy a funkcie s
týmto modifikátorom sú obmedzené len na daný swift súbor. Vo Swiftu totiž
môžeme mať v raz súbore viac tried a ak sú dostatočne malé a nejako spolu
súvisia, tak to dáva z hľadiska objektového návrhu aj zmysel Ak triedu s jej potomkom
umiestnime do rovnakého súboru a použijeme modifikátor
fileprivate
, docielime to, že potomok tieto prvky uvidí, ale z
iných tried, presnejšie z iných súborov, viditeľné nebudú.
Triedu Uzivatel
by sme tak upravili nasledovne a triedu
Administrator
presunuli do rovnakého súboru ako trieda
Uzivatel
:
class Uzivatel { fileprivate var jmeno : String fileprivate var heslo : String fileprivate var vek : Int // ... } class Administrator: Uzivatel { // vlastnosti jako jmeno, heslo a další zde máme přístupné... }
Keď si teraz vytvoríme inštanciu užívateľa a administrátora, obaja
budú mať napr. Vlastnosť jmeno
a metódu
prihlasit()
. Swift triedu Uzivatel
zdedí a doplní
nám automaticky všetky jej vlastnosti.
Výhody dedenie sú jasné, nemusíme opisovať obom triedam tie isté vlastnosti, ale stačí dopísať len to, v čom sa líšia. Zvyšok sa zdedí. Prínos je obrovský, môžeme rozširovať existujúce komponenty o nové metódy a tým je znovu využívať. Nemusíme písať hŕbu redundantného (duplikovaného) kódu. A hlavne - keď zmeníme jedinú vlastnosť v materskej triede, automaticky sa táto zmena všade zdedí. Nedôjde teda k tomu, že by sme to museli meniť ručne u 20 tried a niekde na to zabudli a spôsobili chybu. Sme ľudia a chybovať budeme vždy, musíme teda používať také programátorské postupy, aby sme mali možnosť chybovať čo najmenej.
O materskej triede sa niekedy hovorí ako o predkovi (tu
Uzivatel
) ao triede, ktorá z nej dedí, ako o potomkovi (tu
Administrator
). Potomok môže pridávať nové metódy alebo si
prispôsobovať metódy z materskej triedy (viď ďalej). Môžete sa stretnúť
aj s pojmami nadtřída a podtrieda.
Hierarchie tried
Ďalšou možnosťou, ako objektový model navrhnúť, by
bolo zaviesť materskú triedu Uzivatel
, ktorá by
slúžila len k dedenie. Z Uzivatel
by potom totiž dedili
Osetrovatel
az neho Administrator
. To by sa však
oplatilo pri väčšom počte typov používateľov. V takomto prípade
hovoríme o hierarchii tried, budeme sa tým zaoberať ku koncu tohto kurzu.
Náš príklad bol jednoduchý a preto nám stačili iba 2 triedy. Existujú
tzv. Návrhové vzory, ktoré obsahujú osvedčená schémy
objektových štruktúr pre známe prípady použitia. Záujemcovia je nájdu
popísané v sekcii Návrhové vzory, je to
však už pokročilejšie problematika a tiež veľmi zaujímavá.
V objektovom modelovania sa dedičnosť znázorňuje graficky ako prázdna šípka smerujúca k predkovi. V našom prípade by grafická notácie vyzerala takto:
Dátový typ pri dedičnosti
Obrovskou výhodou dedičnosti je, že keď si vytvoríme premennú s
dátovým typom materskej triedy, môžeme do nej bez problémov ukladať aj jej
potomkov. Je to dané tým, že potomok obsahuje všetko, čo obsahuje
materská trieda, spĺňa teda "požiadavky" (presnejšie obsahuje rozhranie)
dátového typu. A k tomu má oproti materskej triede niečo navyše. Môžeme
si teda urobiť pole typu Uzivatel
a v ňom mať ako užívateľa,
tak administrátorov. S premennou to teda funguje takto:
var u = Uzivatel("Jan Novák", 33) var 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
Vo Swift je veľa konštrukcií, ako operovať s typmi inštanciou pri dedičnosti. Podrobne sa na ne pozrieme počas kurzu, teraz si ukážme len to, ako môžeme overiť typ inštancie v premennej:
let u : Uzivatel = Administrator("Josef Nový", 25) if u is Administrator { print("Je to administrátor") } else { print("Je to uživatel") }
Pomocou operátora is
sa môžeme spýtať, či
je objekt daného typu. Kód vyššie otestuje, či je v premennej
u
užívateľ alebo jeho potomok administrátor.
Jazyky, ktoré dedičnosť podporujú, buď vie dedičnosť jednoduchú, kde trieda dedí len z jednej triedy, alebo viacnásobnú, kde trieda dedí hneď z niekoľkých tried naraz. Viacnásobná dedičnosť sa v praxi príliš neosvedčila, časom si povieme prečo a ukážeme si aj ako ju obísť. Swift podporuje len jednoduchú dedičnosť, s viacnásobnou dedičnosťou sa môžete stretnúť napr. V C ++.
Polymorfizmus
Nenechajte sa vystrašiť príšerným názvom tejto techniky, pretože je v
jadre veľmi jednoduchá. Polymorfizmus umožňuje používať jednotné
rozhranie pre prácu s rôznymi typmi objektov. Majme napríklad veľa objektov,
ktoré reprezentujú nejaké geometrické útvary (kruh, štvorec,
trojuholník). Bolo by určite prínosné a prehľadné, keby sme s nimi mohli
komunikovať jednotne, hoci sa líšia. Môžeme zaviesť triedu
GeometrickyUtvar
, ktorá by obsahovala vlastnosť
barva
a metódu vykresli()
. Všetky geometrické tvary
by potom dedili z tejto triedy jej interface (rozhranie). Objekty kruh a
štvorec sa ale iste vykresľujú inak. Polymorfizmus nám umožňuje
prepísať si metódu vykresli()
pri
každej podtriedy tak, aby robila, čo chceme. Rozhranie tak
zostane zachované a my nebudeme musieť premýšľať, ako sa to u onoho
objekte volá.
Polymorfizmus býva často vysvetľovaný na obrázku so zvieratami, ktoré
majú všetky v rozhraní metódu speak()
, ale každé si ju
vykonáva po svojom.
Podstatou polymorfizmu je teda metóda alebo metódy, ktoré majú všetci
potomkovia definované s rovnakou hlavičkou, ale iným telom. Polymorfizmus si
spolu s dedičnosťou vyskúšame v nasledujúcej lekcii, Aréna s mágom (dedičnosť a polymorfizmus) vo Swift , na
bojovníkoch v našej aréne. Pridáme mága, ktorý si bude metódu
utoc()
vykonávať po svojom pomocou many, ale inak zdedí
správanie a vlastnosti bojovníka. Zvonku teda vôbec nespoznáme, že to nie
je bojovník, pretože bude mať rovnaké rozhranie. Bude to zábava