Motivácia
Kadet! Aby si obstál v boji s objektmi v nehostinnom svete Objektového programovania, potrebuješ sa zoznámiť s dôležitou vlastnosťou, ktorú niektoré objekty používajú na svoje maskovanie a oklamanie nepriateľa formou polymorfizmu. Touto vlastnosťou nie je nič iné, ako dedičnosť!
Náš taktický a strategický tím pre zdokonalenie tvojich schopností obstáť v tomto boji pripravil cvičnú misiu s kódovým označením Mission: Inheritance. Sú pre teba pripravené ciele, ktoré ti pomôžu porozumieť objektom využívajúcim túto vlastnosť. Po ich dosiahnutí sa tvoja šanca prežiť vo svete Objektového programovania dramaticky zvýši! Preto postupuj podľa pripraveného plánu misie.
Veľa šťastia pri zdolávaní cieľov! Z veliteľského mostíka zdraví Manager.
Ciele
- Precvičiť si dedičnosť.
- Porozumieť prekrývaniu metód.
- Porozumieť princípu polymorfizmu.
- Naučiť sa používať kľúčové slovo
super
. - Osvojiť si použitie referencie na metódu.
Postup
Krok 1: Similarity (not) entirely coincidental
Kadet! Nepochybuj o tom, že pôvod kladiva Mjolnir
je jednoznačne extraterestriálny, a hoci oplýva nadľudskou mocou, chtiac-nechtiac má nejaké spoločné črty s hasiacim prístrojom: dá sa použiť a časom sa zničí. Túto podobnosť môžeš v implementácii premietnuť do využitia dedičnosti, čo ti aj pomôže zredukovať opakujúci sa kód.
Úloha 1.1
Vytvorte abstraktnú triedu BreakableTool
, ktorá predstavuje zovšeobecnenie pre akýkoľvek pracovný nástroj, ktorý má obmedzený počet použití.
Zovšeobecnená implementácia nástroja, ktorý sa môže opotrebovať, zahŕňa sledovanie počtu ostávajúcich použití (stav) a aktualizovanie tohto počtu pri samotnom použití nástroja (správanie). Preto v triede BreakableTool
vytvorte:
- Členskú premennú
remainingUses
pre počet zostávajúcich použití nástroja, ktorú je potrebné sprístupniť zvonku triedy len na čítanie a inicializovať použitím parametrického konštruktora. - Metódu
use()
na odrátavanie použití nástroja a na jeho odstránenie zo scény, keď počet ostávajúcich použití klesne na 0 (nástroj sa opotreboval a zlomil).
Poznámka
Pomôžte si kódom, ktorý ste implementovali už pri kladive alebo hasiacom prístroji.
Úloha 1.2
Upravte triedy Hammer
a FireExtinguisher
, aby boli potomkom triedy BreakableTool
.
Upravte konštruktory týchto tried. Teraz, keď rodičovská trieda obsahuje len parametrický konštruktor, musíte ho vhodne zavolať pomocou kľúčového slova super
.
Nezabudnite na zámer, kvôli ktorému sme vytvorili zovšeobecnenie v podobe rodičovskej triedy BreakableTool
a upravte implementácie tried Hammer
a FireExtinguisher
tak, aby čo najviac využívali zdedenú funkcionalitu.
Úloha 1.3
Vytvorte nový balík sk.tuke.kpi.oop.game.tools
a presuňte do neho triedy týkajúce sa nástrojov.
V projekte sa už zväčšuje počet tried. Aby ste si v nich zachovali poriadok a zoskupili spolu súvisiace triedy, vytvorte pre nástroje nový balík tools
v balíku sk.tuke.kpi.oop.game
a presuňte tam triedu BreakableTool
, ako aj Hammer
, Mjolnir
a FireExtinguisher
.
Poznámka
Pri tejto úlohe viete využiť možnosti refaktorizácie, ktoré ponúka vývojové prostredie. Triedy môžete presunúť do nového balíka napr. pomocou drag&drop funkcionality v paneli so štruktúrou projektu. Vyhnete sa tak manuálnemu prepisovaniu package
deklarácie na začiatku súborov a upravovaniu import
-ov, keďže to dokáže prostredie vykonať automaticky.
Krok 2: Self-exploding reactor
Doteraz ste mohli s reaktorom pracovať výlučne manuálne - vedeli ste ho zapnúť a vypnúť a vedeli ste mu zvýšiť a znížiť teplotu volaním vhodných metód pomocou Inšpektora. V tomto kroku však vykonáte potrebné úpravy na to, aby vedel reaktor pracovať automatizovane. Využijete na to systém akcií, ktoré budú reprezentovať činnosť alebo správanie aktéra v hernej scéne.
Gamelib
Akcia, reprezentovaná rozhraním Action
a jeho všeobecnou implementáciou AbstractAction
, slúži v hernej knižnici GameLib na zapuzdrenie činností aktérov do samostatných objektov. Naplánovaním akcie na scéne, do ktorej aktér patrí, sa začne jej (postupné) vykonávanie. Každá akcia naplánovaná na scéne, ktorá ešte nebola dokončená, sa vykoná volaním jej metódy execute()
vždy pred vykreslením nového snímku scény. Pomocou akcií je možné implementovať správanie aktéra vzhľadom na plynúci čas, resp. reakcie aktéra na aktuálne dianie v scéne.
Popis priebehu hernej slučky v knižnici Gamelib nájdete aj na stránke jej dokumentácie.
Vo všeobecnosti, každá akcia má
- stav reprezentovaný dvoma členskými premennými:
actor
(getter getActor()) - aktér, s ktorým sa akcia vykonáva (referenciu nastaví scéna pri naplánovaní akcie, môže byť ajnull
)isDone
(getter isDone()) - vyjadruje, či sa akcia už ukončila (kontrolované scénou vždy pred vykonaním akcie).
- dve podstatné metódy:
- metóda execute(float deltaTime) - implementuje logiku akcie, je volaná vždy pred vykreslením nového snímku scény (max. 60x za sekundu) v prípade, že akcia bola naplánovaná a nie je ešte ukončená. Parametrom metódy je čas (v sekundách) od posledného vykresľovania scény, ktorý dodá knižnica pri volaní tejto metódy.
- metóda reset() - umožňuje resetovať stav akcie.
Plánovanie akcie sa vykonáva volaním metódy scheduleAction(Action action, Actor actor)
nad objektom scény, alebo volaním pomocnej metódy scheduleFor(Actor actor)
nad objektom akcie. To všetko si postupne ukážeme v nasledujúcich úlohách.
Úloha 2.1
V novom balíku sk.tuke.kpi.oop.game.actions
vytvorte triedu PerpetualReactorHeating
, ktorá bude reprezentovať akciu postupne zvyšujúcu teplotu reaktora. Konkrétnu hodnotu kroku zvyšovania teploty umožnite špecifikovať pri vytváraní inštancie triedy v konštruktore.
Ako je možné vidieť aj z diagramu tried uvedenom vyššie, nová trieda akcie PerpetualReactorHeating
má dediť od triedy AbstractAction
. Trieda AbstractAction
je dostupná v balíku knižnice sk.tuke.kpi.gamelib.framework.actions
.
Trieda AbstractAction
používa typový parameter A
, ktorým je definovaný typ aktéra, s ktorým sa môže akcia vykonávať. Keďže má akcia zvyšovať teplotu reaktora, dosaďte jej typový argument Reactor
:
public class PerpetualReactorHeating extends AbstractAction<Reactor> {
}
Poznámka
Časť implementácie triedy AbstractAction
z knižnice vyzerá nasledovne:
public abstract class AbstractAction<A extends Actor> implements Action<A> {
private A actor;
@Override
public A getActor() { return actor; }
@Override
public void setActor(A actor) { this.actor = actor; }
/* ... zbytok triedy ... */
}
Všimnite si typový parameter **A
**, ktorý
- je ohraničený na typy odvodené od typu
Actor
(A extends Actor
), - definuje typ členskej premennej
actor
ako aj typ návratovej hodnoty metódygetActor()
a typ parametra metódysetActor()
.
Ak pri implementácii našej triedy PerpetualReactorHeating
použijeme typ Reactor
ako typový argument namiesto A
(čo spravíme zápisom AbstractAction<Reactor>
, ako je uvedené vyššie), dosiahneme to, že z pohľadu triedy PerpetualReactorHeating
bude typ premennej actor
, typ návratovej hodnoty metódy getActor()
a typ parametra metódy setActor()
zmenený na typ Reactor
. Tým dosiahneme, že:
- Akcia bude použiteľná len pre aktérov typu
Reactor
, keďže metódasetActor()
bude akceptovať len argumenty typuReactor
- jej signatúra z pohľadu triedyPerpetualReactorHeating
budesetActor(Reactor actor)
. - Volaním metódy
getActor()
dostaneme objekt typuReactor
a nie len všeobecného typuActor
. To nám umožní na tomto objekte volať metódy špecifické pre reaktor, ako napr.increaseTemperature()
, čo budeme potrebovať v rámci ďalšej úlohy.
Ak to zhrnieme, tak podobne ako nám bežné parametre metód umžňujú zovšeobecniť implementáciu pre rôzne hodnoty, typové parametre nám umožňujú zovšeobecniť implementáciu pre rôzne typy.
Úloha 2.2
V triede PerpetualReactorHeating
prekryte abstraktnú metódu execute(float deltaTime)
, v ktorej implementujete činnosť akcie.
Pri implementovaní metódy execute()
získajte referenciu na aktéra, s ktorým sa akcia vykonáva (bude to nejaký konkrétny reaktor) a zvýšte jeho teplotu o hodnotu danú parametrom konštruktora akcie.
Úloha 2.3
Naplánujte vykonávanie akcie PerpetualReactorHeating
na reaktore tak, aby sa teplota zvyšovala vždy o 1 stupeň.
Keďže pre naplánovanie akcie potrebuje aktér poznať scénu, do ktorej patrí, vhodným miestom pre naplánovanie akcie môže byť metóda addedToScene()
zdedená od triedy AbstractActor
, ktorá v parametri dostane referenciu na scénu (typ Scene
) a je zavolaná po tom, čo bol aktér do danej scény pridaný. Metódu addedToScene()
je teda potrebné prekryť. Nezabudnite však na to, že stále potrebujeme, aby sa pôvodná implementácia metódy addedToScene()
z triedy AbstractActor
vykonala, a teda využite kľúčové slovo super
na zavolanie zdedenej implementácie!
Scéna definuje metódu scheduleAction()
, ktorá akceptuje akciu a aktéra, s ktorým má akciu neskôr vykonávať. Naplánovanie vykonávania akcie v metóde reaktora môže preto vyzerať nasledovne:
// v metode addedToScene triedy Reactor
scene.scheduleAction(new PerpetualReactorHeating(1), this);
Náš taktický a strategický tím však navrhol aj doplnkovú metódu scheduleFor()
dostupnú priamo na akcii, ktorá akceptuje aktéra a zabezpečí to isté, ako metóda scheduleAction()
na scéne, len s o niečo prehľadnejším zápisom:
// v metode addedToScene triedy Reactor
new PerpetualReactorHeating(1).scheduleFor(this);
Gamelib
Implementácia metódy scheduleFor()
každej akcie je (zjednodušene) nasledovná:
Disposable scheduleFor(A actor) {
return actor.getScene().scheduleAction(this, actor);
}
Úloha 2.4
Overte správnosť svojej implementácie spustením reaktora.
Ak ste postupovali správne, zapnutému reaktoru sa bude postupom času zvyšovať teplota, čo si viete overiť pomocou inšpektora.
Krok 3: How cool is cool enough?
Teraz, keď máme prvý krok k vytvoreniu samočinného aktéra za sebou, určite neuniklo vašej pozornosti, že v podstate len na volanie jednej metódy reaktora bola vytvorená celá nová trieda. Tento nedostatok prvotného riešenia sa pokúsime postupne riešiť.
Úprava reaktora z predchádzajúceho kroku má však jeden háčik - tým, že necháte reaktor pracovať samostatne, bude sa systematicky prehrievať. A prehrievanie, ako vieme už od roku 1986, bude nevyhnutne viesť k jeho trvalému poškodeniu. Aby sme sa vyhli takejto situácii, namiesto kladív vytvoríte chladič, ktorý bude reaktor systematicky ochladzovať.
Úloha 3.1
V balíku sk.tuke.kpi.oop.game
vytvorte triedu Cooler
pre chladič, ktorý bude zabezpečovať dostatočné chladenie pre pripojený reaktor.
Správanie chladiča a jeho vzťah s reaktorom opisuje nasledujúci diagram tried:
Ako animáciu chladiča použite súbor fan. Keď bude chladič vypnutý, tak prehrávanie jeho animácie pozastavte. Keď chladič naopak zapnete, jeho animáciu spustite. Na objekte animácie nájdete vhodné metódy na jej ovládanie.
Úloha 3.2
Do triedy Cooler
pridajte privátnu metódu, v ktorej implementujete činnosť chladiča.
Metódu pomenujte napríklad coolReactor()
. Pri jej zavolaní sa vždy (teda - ak je chladič zapnutý) zníži teplota pripojeného reaktora o jeden stupeň.
Úloha 3.3
Prekryte metódu addedToScene()
, v ktorej naplánujete akciu pre činnosť chladiča.
Pre zavolanie metódy v rámci akcie nie je potrebné vytvárať celú triedu akcie, ktorá potom slúži len na jednu, veľmi špecifickú funkciu. Namiesto implementácie novej triedy využijeme akciu Invoke (dostupnú v knižnici GameLib), ktorou môžeme definovať činnosť akcie (to, čo sa vykoná v metóde execute()
) aj referenciou na metódu implementujúcu požadované správanie.
Poznámka
Referencia na metódu method
konkrétneho objektu object
má v jazyku Java zápis v tvare object::method
, používa sa teda operátor dvojitej dvojbodky. Keďže ide len o referenciu na metódu, túto metódu nevoláme, neodovzdávame jej žiadne argumenty, a preto ani nepíšeme zátvorky za názvom metódy. Prípadné argumenty sú metóde dodané až pri použití referencie.
Akciu a jej naplánovanie teda zapíšeme takto:
// v metode addedToScene triedy Cooler
new Invoke<>(this::coolReactor).scheduleFor(this);
Poznámka
Trieda akcie Invoke
je univerzálna a využíva typový parameter pre typ aktéra, na ktorom môže byť konkrétna inštancia akcie naplánovaná. Pri použití triedy musíme teda buď
- uviesť typový argument explicitne - vo vyššie uvedenom fragmente kódu by to bolo
new Invoke<Reactor>(...)
, alebo - nechať kompilátor tento argument odvodiť z kontextu (ak je to monžé).
Odvodenie typového argumentu dosiahneme použitím diamantového operátora a zápis bude vyzerať new Invoke<>(...)
. Vynechanie tohto operátora na type s typovým parametrom kompilátor spracuje v režime spätnej kompatibility so starými verziami jazyka Java a označí taký typ ako raw typ.
Takáto akcia však celkom nesplní to, čo od nej čakáme: metódu coolReactor()
zavolá iba raz a potom je považovaná za skončenú (isDone()
vráti true
). Opakované vykonávanie akcie vieme docieliť kompozíciou s akciou Loop
(dostupná v GameLib), ktorá volá inú akciu danú ako argument jej konštruktora a nikdy nie je považovaná za skončenú (isDone()
akcie Loop
stále vráti false
):
// v metode addedToScene triedy Cooler
new Loop<>(new Invoke<>(this::coolReactor)).scheduleFor(this);
Poznámka
Akcia Loop
implementuje návrhový vzor Decorator.
Úloha 3.4
Overte správnosť svojej implementácie spustením reaktora a pripojením najprv jedného a potom dvoch chladičov naraz.
Čím viac chladičov pripojíte, tým skôr bude reaktor ochladený. Pozor však na to, že reaktor nie je možné chladiť do mínusových teplôt.
Krok 4: Defective Light
Náš analytický tím zadefinoval pokazené svetlo, ako svetlo, ktoré je pokazené. A keďže môžeme predpokladať, že pokazené svetlo bolo najprv v poriadku, jeho správanie odvodíme od dobrého svetla. V našom prípade sa bude pokazenosť prejavovať blikaním svetla v nepravidelných intervaloch.
Úloha 4.1
V balíku sk.tuke.kpi.oop.game
vytvorte triedu DefectiveLight
, ktorá bude potomkom triedy Light
.
Úloha 4.2
V triede DefectiveLight
vytvorte metódu, ktorá bude definovať správanie pokazeného svetla.
Pre dosiahnutie efektu pokazeného svetla môžete použiť ľubovoľný postup. Jeden z nich môže vyzerať napr. tak, že vygenerujete náhodné číslo od 0 do 20 a ak bude vygenerované číslo 1, zmeníte stav svetla (zo zhasnutého na zasvietené a opačne).
Poznámka
Náhodné číslo môžte v Jave získať pomocou inštancie triedy Random
alebo jednoducho volaním statickej metódy Math.random()
.
Úloha 4.3
Naplánujte správanie pokazeného svetla pomocou akcií Loop
a Invoke
.
Úloha 4.4
Overte správnosť svojej implementácie vytvorením pokazeného svetla a jeho pripojením k reaktoru.
Nezabudnite, že typ parametra metódy addLight()
v triede Reactor
netreba meniť. Je tu využitá vlastnosť polymorfizmu: DefectiveLight
, ako potomok triedy Light
, sa dá použiť všade tam, kde je akceptovaný typ Light
.
Krok 5: Repository
Úloha 5.1
Nahrajte (cez commit a push) váš zdrojový kód do repozitára na GitLab-e. Spravte tak do nasledujúceho cvičenia. Zároveň si pripravte otázky, ktoré by ste na cvičení chceli vyriešiť.
Zdrojový kód nahrajte aj v prípade, ak ste nestihli dokončiť všetky úlohy. Rozpracované časti, ktoré by mohli spôsobiť kompilačné chyby, odporúčame zakomentovať.
Doplňujúce úlohy
Úloha A.1
Vytvorte triedu SmartCooler
predstavujúcu upravený Cooler
, ktorý sa automaticky zapína a vypína v závislosti od teploty reaktora.
Pomocou smart chladiča zabezpečte udržiavanie pracovnej teploty reaktora v rozsahu od 1500 do 2500 stupňov. To znamená, že chladič bude chladiť len vtedy, ak aktuálna teplota stúpne nad 2500 stupňov. Ak však teplota klesne pod 1500 stupňov, chladič prestane reaktor chladiť.
V tomto prípade bude tiež zaujímavé sledovať, ako sa chladič bude zapínať a vypínať podľa toho, kedy bude naozaj pracovať.
Poznámka
Nezabudnite na to, že časť funkcionality chladiča je už riešená v triede Cooler
. Vyhnite sa duplikovaniu kódu a využite možnosť volať metódy predka.
Úloha A.2
Vytvorte triedu Helicopter
pre bojovú helikoptéru, ktorá bude vedieť prenasledovať hráča a zaútočiť na neho, keď ho dostihne.
V triede implementujte verejnú metódu searchAndDestroy()
, ktorá spustí prenasledovanie hráča. Pohyb helikoptéry smerom k hráčovi pri pustenom prenasledovaní realizujte v metóde vytvorenej pre akciu Invoke
.
Zabezpečte, aby sa energia hráča znížila o 1, keď sa s helikoptérou stretne. Pre animáciu helikoptéry použite obrázok heli (stiahnite si ho do projektu do adresára src/main/resources/sprites
).
Poznámka
Pri tejto úlohe neprekrývajte metódu addedToScene()
v triede Helicopter
! Porozmýšľajte, kde je vhodnejšie miesto pre naplánovanie akcie.
Poznámka
Pre získanie referencie na objekt hráča si prezrite dostupné metódy na objekte hernej scény. Hráč je typu Player
a má meno Player. Pre overenie, či je helikoptéra v kolízii (dotýka sa) s hráčom, použite vhodnú metódu dostupnú na objekte aktéra.
Doplňujúce zdroje
- Java Tutoriál: Dedičnosť
- Java Tutoriál: Generické typy
- Java Tutoriál: Ohraničené typové parametre
- Java Tutoriál: Referencie na metódy