IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 8 - Aréna s mágem (dědičnost a polymorfismus)

V minulé lekci, Dědičnost a polymorfismus v Dartu, jsme si vysvětlili dědičnost a polymorfismus. Pro tuto lekci máme slíbeno, že si je vyzkoušíme v praxi. Bude to opět na naší aréně, kde z bojovníka oddědíme mága. Tento Dart tutoriál již patří k těm náročnějším a bude tomu tak i u dalších. Proto si průběžně procvičujte práci s objekty, zkoušejte si naše cvičení a také vymýšlejte nějaké své aplikace, abyste si zažili základní věci. To, že je tu přítomen celý on-line kurz neznamená, že ho celý najednou přečtete a pochopíte :) Snažte se programovat průběžně.

Objektově orientované programování v Dartu

Než začneme něco psát, shodněme se na tom, co by měl mág umět. Mág bude fungovat stejně, jako bojovník. Kromě života bude mít však i manu. Zpočátku bude mana plná. V případě plné many může mág vykonat magický útok, který bude mít pravděpodobně vyšší damage, než útok normální (ale samozřejmě záleží na tom, jak si ho nastavíme). Tento útok manu vybije na 0. Každé kolo se bude mana zvyšovat o 10 a mág bude podnikat jen běžný útok. Jakmile se mana zcela doplní, opět bude moci magický útok použít. Mana bude zobrazena grafickým ukazatelem, stejně jako život.

Vytvoříme tedy třídu Mag, zdědíme ji z Bojovnik a dodáme ji vlastnosti, které chceme oproti bojovníkovi navíc. Bude tedy vypadat takto (opět si ji okomentujte):

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

V mágovi nemáme zatím přístup ke všem proměnným, protože jsou v bojovníkovi nastavené jako privátní. Musíme třídu Bojovnik lehce upravit. Vytvoříme si metody (tzv. gettery), které nám umožní naše vlastnosti číst i zvenčí. Později si v lekcích ukážeme i gettery, které má Dart nativně zabudované. Budeme potřebovat jen _jmeno, _utok a _kostka. Naopak vlastnost _zprava není vhodné zveřejňovat ani pro čtení, protože nesouvisí s bojovníkem, ale s nějakou vnitřní logikou třídy. Třída tedy bude vypadat nějak 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;

// ...

Přejděme ke konstruktoru.

Konstruktor potomka

Dart nedědí konstruktory! Je to pravděpodobně z toho důvodu, že předpokládá, že potomek bude mít navíc nějaké vlastnosti a původní konstruktor by u něj byl na škodu. To je i náš případ, protože konstruktor mága bude brát oproti tomu z bojovníka navíc 2 parametry (maximální manu a magický útok).

Definujeme si tedy konstruktor v potomkovi, který bere parametry potřebné pro vytvoření bojovníka a několik parametrů navíc pro mága.

V konstruktorech potomků je nutné vždy volat konstruktor předka. Je to z toho důvodu, že bez volání konstruktoru nemusí být instance správně inicializovaná. Konstruktor předka nevoláme pouze v případě, že žádný nemá. Náš konstruktor musí mít samozřejmě všechny parametry potřebné pro předka plus ty nové, co má navíc potomek. Některé potom předáme předkovi a některé si zpracujeme sami. Konstruktor předka se vykoná před naším konstruktorem.

V Dartu existuje klíčové slovo super, které je podobné námi již známému this. Na rozdíl od this, které odkazuje na konkrétní instanci třídy, super odkazuje na předka. My tedy můžeme zavolat konstruktor předka s danými parametry a poté vykonat navíc inicializaci pro mága. V Dartu se volání konstruktoru předka píše do hlavičky metody za dvojtečku :.

Konstruktor mága bude tedy vypadat 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.: Stejně můžeme volat i jiný konstruktor v té samé třídě (ne předka), jen místo super použijeme this.

Přesuňme se nyní do main.dart a druhého bojovníka (Shadow) změňme na mága, např. takto:

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

Změnu samozřejmě musíme udělat i v řádku, kde bojovníka do arény vkládáme. Všimněte si, že mága ukládáme do proměnné typu Bojovnik. Nic nám v tom nebrání, protože bojovník je jeho předek. Stejně tak si můžeme typ proměnné změnit na Mag. Když aplikaci nyní spustíme, bude fungovat úplně stejně, jako předtím. Mág vše dědí z bojovníka a zatím tedy funguje jako bojovník.

Polymorfismus a přepisování metod

Bylo by výhodné, kdyby objekt Arena mohl s mágem pracovat stejným způsobem jako s bojovníkem. My již víme, že takovémuto mechanismu říkáme polymorfismus. Aréna zavolá na objektu metodu utoc() se soupeřem v parametru. Nestará se o to, jestli bude útok vykonávat bojovník nebo mág, bude s nimi pracovat stejně. U mága si tedy přepíšeme metodu utoc() z předka. Přepíšeme zděděnou metodu tak, aby útok pracoval s manou, hlavička metody však zůstane stejná.

Budeme ještě jistě používat metodu _nastavZpravu(), ta je však privátní. Označme ji jako public:

Nyní se vraťme do potomka a pojďme ji přepsat. Metodu normálně definujeme v mag.dart tak, jak jsme zvyklí. Použijeme však ještě anotaci @override, která značí, že si jsme vědomi toho, že se metoda zdědila, ale přejeme si změnit její chování.

@override
void utoc(Bojovnik souper)

Podobně jsme přepisovali metodu toString() u našich objektů. Každý objekt v Dartu je totiž odděděný od třídy Object, která obsahuje metodu toString(). Při její implementaci tedy musíme použít anotaci @override.

Chování metody utoc() nebude nijak složité. Podle hodnoty many buď provedeme běžný útok nebo útok magický. Hodnotu many potom buď zvýšíme o 10 nebo naopak snížíme na 0 v případě 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 srozumitelný. Všimněte si omezení many na _maxMana, může se nám totiž stát, že tuto hodnotu přesáhneme, když ji zvyšujeme o 10. Když se nad kódem zamyslíme, tak útok výše v podstatě vykonává původní metoda utoc(). Jistě by bylo přínosné zavolat podobu metody na předkovi místo toho, abychom chování opisovali. K tomu opět 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ět vidíme, jak můžeme znovupoužívat kód. S dědičností je spojeno opravdu mnoho technik, jak si ušetřit práci. V našem případě to ušetří několik řádků, ale u většího projektu by to mohlo mít obrovský význam.

Aplikace nyní funguje tak, jak má:

Konzolová aplikace
-------------- 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 maně mága, pojďme to napravit. Přidáme mágovi veřejnou metodu grafickaMana(), která bude obdobně jako u života vracet String s grafickým ukazatelem many.

Abychom nemuseli logiku se složením ukazatele psát dvakrát, upravíme metodu grafickyZivot() v bojovnik.dart. Připomeňme si, jak vypadá:

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

Vidíme, že není kromě proměnných _zivot a _maxZivot na životě nijak závisí. Metodu přejmenujeme na grafickyUkazatel() a dáme ji 2 parametry: aktuální hodnotu a maximální hodnotu. _zivot a _maxZivot v těle metody poté 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) + ']';
}

Metodu grafickyZivot() v bojovnik.dart naimplementujeme znovu, bude nám v ní stačit jediný řádek a to zavolání metody grafickyUkazatel() s příslušnými parametry:

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

Určitě jsem mohl v tutoriálu s bojovníkem udělat metodu grafickyUkazatel() rovnou. Chtěl jsem však, abychom si ukázali, jak se řeší případy, kdy potřebujeme vykonat podobnou funkčnost vícekrát. S takovouto parametrizací se v praxi budete setkávat často, protože nikdy přesně nevíme, co budeme v budoucnu od našeho programu požadovat.

Nyní můžeme vykreslovat ukazatel tak, jak se nám to hodí. Přesuňme se do mag.dart a naimplementujme metodu grafickaMana():

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

Jednoduché, že? Nyní je mág hotový, zbývá jen naučit arénu zobrazovat manu v případě, že je bojovník mág. Přesuňme se tedy do arena.dart.

Rozpoznání typu objektu

Jelikož se nám nyní vykreslení bojovníka zkomplikovalo, uděláme si na něj samostatnou metodu vypisBojovnika(), jejím parametrem bude daná instance bojovníka:

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

Nyní pojďme reagovat na to, jestli je bojovník mág. Minule jsme si řekli, že k tomu slouží operátor is:

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

Bojovníka jsme nemuseli přetypovat na mága (b as Mag), jelikož Dart podle podmínky s is sám pozná, že je bojovník daného typu a tak se můžeme k metodě grafickaMana() dostat bez problémů. Samotný Bojovnik ji totiž nemá. To bychom měli, _vypisBojovnika() budeme volat v metodě _vykresli(), která bude vypadat takto:

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

Hotovo. :-)

Konzolová aplikace
-------------- Aréna --------------

Bojovníci:

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

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

Zalgoren útočí s úderem za 28 hp

Kód máte v příloze. Pokud jste něčemu nerozuměli, zkuste si článek přečíst vícekrát nebo pomaleji, jsou to důležité praktiky. V příští lekci, Statika v Dartu, si vysvětlíme pojem statika.


 

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 9x (4.53 kB)
Aplikace je včetně zdrojových kódů v jazyce Dart

 

Předchozí článek
Dědičnost a polymorfismus v Dartu
Všechny články v sekci
Objektově orientované programování v Dartu
Přeskočit článek
(nedoporučujeme)
Statika v Dartu
Článek pro vás napsal Honza Bittner
Avatar
Uživatelské hodnocení:
2 hlasů
FIT ČVUT alumnus :-) Sleduj mě na https://twitter.com/tenhobi a ptej se na cokoli na https://github.com/tenhobi/ama.
Aktivity