IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

8. diel - Aréna s mágom (dedičnosť a polymorfizmus)

V minulej lekcii, Dedičnosť a polymorfizmus v Dart , sme si vysvetlili dedičnosť a polymorfizmus. Pre túto lekciu máme sľúbené, že si ich vyskúšame v praxi. Bude to opäť na našej aréne, kde z bojovníka oddědíme mága. Tento Dart tutoriál už patrí k tým náročnejším a bude tomu tak aj u ďalších. Preto si priebežne precvičujte prácu s objektmi, skúšajte si naše cvičenia a tiež vymýšľajte nejaké svoje aplikácie, aby ste si zažili základné veci. To, že je tu prítomný celý on-line kurz neznamená, že ho celý naraz prečítate a pochopíte:) Snažte sa programovať priebežne.

Objektovo orientované programovanie v Dart
Než začneme niečo písať, zhodneme sa na tom, čo by mal mág vedieť. Mág bude fungovať rovnako, ako bojovník. Okrem života bude mať však aj manu. Spočiatku bude mana plná. V prípade plnej many môže mág vykonať magický útok, ktorý bude mať pravdepodobne vyššie damage, ako útok normálne (ale samozrejme záleží na tom, ako si ho nastavíme). Tento útok manu vybije na 0. Každé kolo sa bude mana zvyšovať o 10 a mág bude podnikať len bežný útok. Akonáhle sa mana úplne doplní, opäť bude môcť magický útok použiť. Mana bude zobrazená grafickým ukazovateľom, rovnako ako život.

Vytvoríme teda triedu Mag, zdedíme ju z Bojovnik a dodáme ju vlastnosti, ktoré chceme oproti bojovníkovi navyše. Bude teda vyzerať takto (opäť si ju okomentujte):

class Mag extends Bojovnik {
    int _mana;
    int _maxMana;
    int _magickyUtok;
}

V mágovi nemáme zatiaľ prístup ku všetkým premenným, pretože sú v bojovníkovi nastavené ako privátne. Musíme triedu Bojovnik ľahko upraviť. Vytvoríme si metódy (tzv. Getter), ktoré nám umožnia naše vlastnosti čítať aj zvonku. Neskôr si v lekciách ukážeme aj Getter, ktoré má Dart natívne zabudované. Budeme potrebovať len _jmeno, _utok a _kostka. Naopak vlastnosť _zprava nie je vhodné zverejňovať ani pre čítanie, pretože nesúvisí s bojovníkom, ale s nejakou vnútornou logikou triedy. Trieda teda bude vyzerať nejako takto:

String _jmeno;
String vratJmeno() {
    return _jmeno;
}
int _zivot;
int _maxZivot;
int _utok;
int vratUtok() {
    return _utok;
}
int _obrana;
Kostka _kostka;
Kostka vratKostka() {
    return _kostka;
}
String _zprava;

// ...

Prejdime ku konstruktoru.

Konštruktor potomka

Dart nededia konstruktory! Je to pravdepodobne z toho dôvodu, že predpokladá, že potomok bude mať navyše nejaké vlastnosti a pôvodné konštruktor by u neho bol na škodu. To je aj náš prípad, pretože konštruktor mága bude brať oproti tomu z bojovníka navyše 2 parametre (maximálna manu a magický útok).

Definujeme si teda konštruktor v potomkovi, ktorý berie parametre potrebné pre vytvorenie bojovníka a niekoľko parametrov navyše pre mága.

V konštruktor potomkov je nutné vždy volať konštruktor predka. Je to z toho dôvodu, že bez volania konstruktoru nemusí byť inštancie správne inicializovaná. Konštruktor predka nevoláme iba v prípade, že žiadny nemá. Náš konštruktor musia mať samozrejme všetky parametre potrebné pre predka plus tie nové, čo má navyše potomok. Niektoré potom odovzdáme predkovi a niektoré si spracujeme sami. Konštruktor predka sa vykoná pred naším konstruktoru.

V Dart existuje kľúčové slovo super, ktoré je podobné nami už známemu this. Na rozdiel od this, ktoré odkazuje na konkrétnu inštanciu triedy, super odkazuje na predka. My teda môžeme zavolať konštruktor predka s danými parametrami a potom vykonať navyše inicializáciu pre mága. V Dart sa volanie konstruktoru predka píše do hlavičky metódy za dvojbodku :.

Konštruktor mága bude teda vyzerať takto:

Mag(String jmeno, int maxZivot, int utok, int obrana, Kostka kostka, this._maxMana, this._magickyUtok)
    : super(jmeno, maxZivot, utok, obrana, kostka) {
    _mana = _maxMana;
}

Pozn .: Rovnako môžeme volať aj iný konštruktor v tej istej triede (nie predka), len miesto super použijeme this.

Presuňme sa teraz do main.dart a druhého bojovníka (Shadow) zmeňme na mága, napr. Takto:

Bojovnik gandalf = new Mag('Gandalf', 60, 15, 12, kostka, 30, 45);

Zmenu samozrejme musíme urobiť aj v riadku, kde bojovníka do arény vkladáme. Všimnite si, že mága ukladáme do premennej typu Bojovnik. Nič nám v tom nebráni, pretože bojovník je jeho predok. Rovnako tak si môžeme typ premennej zmeniť na Mag. Keď aplikáciu teraz spustíme, bude fungovať úplne rovnako, ako predtým. Mág všetko dedí z bojovníka a zatiaľ teda funguje ako bojovník.

Polymorfizmus a prepisovanie metód

Bolo by výhodné, keby objekt Arena mohol s mágom pracovať rovnako ako s bojovníkom. My už vieme, že takémuto mechanizmu hovoríme polymorfizmus. Aréna zavolá na objekte metódu utoc() so súperom v parametri. Nestará sa o to, či bude útok vykonávať bojovník alebo mág, bude s nimi pracovať rovnako. U mága si teda prepíšeme metódu utoc() z predka. Prepíšeme zdedenú metódu tak, aby útok pracoval s mannou, hlavička metódy však zostane rovnaká.

Budeme ešte určite používať metódu _nastavZpravu(), tá je však privátne. Označme ju ako public:

Teraz sa vráťme do potomka a poďme ju prepísať. Metódu normálne definujeme v mag.dart tak, ako sme zvyknutí. Použijeme však ešte anotáciu @override, ktorá značí, že si sme vedomí toho, že sa metóda zdedila, ale prajeme si zmeniť jej správanie.

@override
void utoc(Bojovnik souper)

Podobne sme prepisovali metódu toString() u našich objektov. Každý objekt v Dart je totiž odděděný od triedy Object, ktorá obsahuje metódu toString(). Pri jej implementácii teda musíme použiť anotáciu @override.

Správanie metódy utoc() nebude nijako zložité. Podľa hodnoty many buď vykonáme bežný útok alebo útok magický. Hodnotu many potom buď zvýšime o 10 alebo naopak znížime na 0 v prípade magického útoku.

@override
void utoc(Bojovnik souper) {
    int uder = 0;
    // Mana není naplněna
    if (_mana < _maxMana) {
        _mana += 10;

        if (_mana > _maxMana) {
            _mana = _maxMana;
        }

        uder = vratUtok() + vratKostka().hod();
        nastavZpravu('${vratJmeno()} použil magii za $uder hp');
    } else {
        uder = _magickyUtok + vratKostka().hod();
        nastavZpravu('${vratJmeno()} použil magii za $uder hp');
        _mana = 0;
    }
    souper.branSe(uder);
}

Kód je asi zrozumiteľný. Všimnite si obmedzenia many na _maxMana, môže sa nám totiž stať, že túto hodnotu presiahne, keď ju zvyšujeme o 10. Keď sa nad kódom zamyslíme, tak útok vyššie v podstate vykonáva pôvodnej metóda utoc(). Iste by bolo prínosné zavolať podobu metódy na predkovi namiesto toho, aby sme správanie odpisovali. K tomu opäť použijeme super:

@override
void utoc(Bojovnik souper) {
    // Mana není naplněna
    if (_mana < _maxMana) {
        _mana += 10;

        if (_mana > _maxMana) {
            _mana = _maxMana;
        }

        super.utoc(souper);
    } else {
        int uder = _magickyUtok + vratKostka().hod();
        nastavZpravu('${vratJmeno()} použil magii za $uder hp');
        _mana = 0;
        souper.branSe(uder)
    }
}

Opäť vidíme, ako môžeme znovupoužívat kód. S dedičnosťou je spojené naozaj mnoho techník, ako si ušetriť prácu. V našom prípade to ušetrí niekoľko riadkov, ale u väčšieho projektu by to mohlo mať obrovský význam.

Aplikácia teraz funguje tak, ako má:

Konzolová aplikácia
-------------- Aréna --------------

Zdraví bojovníků:

Zalgoren [#############       ]
Gandalf [#################   ]
Gandalf použil magii za 52 hp
Zalgoren utrpěl poškození 36 hp

Aréna nás však neinformuje o mane mága, poďme to napraviť. Pridáme mágovi verejnú metódu grafickaMana(), ktorá bude obdobne ako u života vracať String s grafickým ukazovateľom many.

Aby sme nemuseli logiku so zložením ukazovatele písať dvakrát, upravíme metódu grafickyZivot() v bojovnik.dart. Pripomeňme si, ako vyzerá:

String grafickyZivot() {
    int celkem = 20;
    int pocet = ((_zivot / _maxZivot) * celkem).round();
    if (pocet == 0 && nazivu())
        pocet = 1;
    return '[' + '#' * pocet + ' ' * (celkem - pocet) + ']';
}

Vidíme, že nie je výnimkou premenných _zivot a _maxZivot na živote nijako závisí. Metódu premenujeme na grafickyUkazatel() a dáme ju 2 parametre: aktuálnu hodnotu a maximálnu hodnotu. _zivot a _maxZivot v tele metódy potom nahradíme za aktualni a maximalni:

String grafickyUkazatel(int aktualni, int maximalni) {
    int celkem = 20;
    int pocet = ((aktualni / maximalni) * celkem).round();
    if (pocet == 0 && nazivu()) pocet = 1;
    return '[' + '#' * pocet + ' ' * (celkem - pocet) + ']';
}

Metódu grafickyZivot() v bojovnik.dart naimplementujeme znovu, bude nám v nej stačiť jediný riadok a to zavolanie metódy grafickyUkazatel() s príslušnými parametrami:

String grafickyZivot() {
    return grafickyUkazatel(_zivot, _maxZivot);
}

Určite som mohol v tutoriálu s bojovníkom urobiť metódu grafickyUkazatel() rovno. Chcel som však, aby sme si ukázali, ako sa rieši prípady, keď potrebujeme vykonať podobnú funkčnosť viackrát. S takouto parametrizáciou sa v praxi budete stretávať často, pretože nikdy presne nevieme, čo budeme v budúcnosti od nášho programu požadovať.

Teraz môžeme vykresľovať ukazovateľ tak, ako sa nám to hodí. Presuňme sa do mag.dart a naimplementujme metódu grafickaMana():

String grafickaMana() {
    return grafickyUkazatel(_mana, _maxMana);
}

Jednoduché, že? Teraz je mág hotový, zostáva len naučiť arénu zobrazovať manu v prípade, že je bojovník mág. Presuňme sa teda do arena.dart.

Rozpoznanie typu objektu

Keďže sa nám teraz vykreslenie bojovníka skomplikovalo, urobíme si na neho samostatnú metódu vypisBojovnika(), jej parametrom bude daná inštancie bojovníka:

void _vypisBojovnika(Bojovnik b) {
    print(b);
    print('Zivot: ${b.grafickyZivot()}');
}

Teraz poďme reagovať na to, či je bojovník mág. Minule sme si povedali, že k tomu slúži operátor is:

void _vypisBojovnika(Bojovnik b) {
    print(b);
    print('Zivot: ${b.grafickyZivot()}');
    if (b is Mag) {
        print('Mana: ${b.grafickaMana()}');
    }
}

Bojovníka sme nemuseli pretypovať na mága (b as Mag), keďže Dart podľa podmienky s is sám spozná, že je bojovník daného typu a tak sa môžeme k metóde grafickaMana() dostať bez problémov. Samotný Bojovnik ju totiž nemá. To by sme mali, _vypisBojovnika() budeme volať v metóde _vykresli(), ktorá bude vyzerať takto:

void _vykresli() {
    print('-------------- Aréna --------------\n');
    print('Bojovníci:\n');
    _vypisBojovnika(_bojovnik1);
    print('');
    _vypisBojovnika(_bojovnik2);
    print('');
}

Hotovo.:-)

Konzolová aplikácia
-------------- Aréna --------------

Bojovníci:

Zalgoren
Život: [##########          ]

Gandalf
Život: [#####               ]
Mana: [#############       ]

Zalgoren útočí s úderem za 28 hp

Kód máte v prílohe. Ak ste niečomu nerozumeli, skúste si článok prečítať viackrát alebo pomalšie, sú to dôležité praktiky. V budúcej lekcii, Statika v Dart , si vysvetlíme pojem statika.


 

Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.

Stiahnuť

Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami

Stiahnuté 9x (4.53 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Dart

 

Predchádzajúci článok
Dedičnosť a polymorfizmus v Dart
Všetky články v sekcii
Objektovo orientované programovanie v Dart
Preskočiť článok
(neodporúčame)
Statika v Dart
Článok pre vás napísal Honza Bittner
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
FIT ČVUT alumnus :-) Sleduj mě na https://twitter.com/tenhobi a ptej se na cokoli na https://github.com/tenhobi/ama.
Aktivity