OOP. Verze : 365 NS, odstavců, slov, znaků, bajtů. a Java 8

Rozměr: px
Začít zobrazení ze stránky:

Download "OOP. Verze : 365 NS, odstavců, slov, znaků, bajtů. a Java 8"

Transkript

1

2 Verze : 365 NS, odstavců, slov, znaků, bajtů OOP a Java 8 Návrh a vývoj složitějšího projektu vyhovujícího zadanému rámci Rudolf Pecinovský 49R_Adventura_TXT.doc verze , uloženo: so :16 Strana 2 z 351

3 Rudolf Pecinovský OOP a Java 8 Návrh složitějšího projektu vyhovujícího zadanému rámci Copyright Rudolf Pecinovský, 2015 Vydalo nakladatelství Tomáš Bruckner, První vydání, Řepín-Živonín 2015 V knize použité názvy mohou být ochrannými známkami nebo registrovanými ochrannými známkami příslušných vlastníků. Návrh vnitřního layoutu Rudolf Pecinovský Zlom Rudolf Pecinovský Počet stran 351 ISBN (PDF) Strana 3 z 351

4 Všem, kteří se chtějí něco naučiti Strana 4 z 351

5 Stručný obsah 5 Stručný obsah Stručný obsah Stručný obsah... 5 Podrobný obsah... 6 Seznam programů Seznam obrázků Seznam odboček podšeděných bloků Úvod Zadání projektu Podpůrný framework Koncepce testů scénáře Správce scénářů Návrh správce scénářů konkrétní hry Sjednocený správce scénářů Návrh rámce pro hru Začínáme tvořit vlastní hru Vytváříme svět hry Dotahujeme svět hry Vytvoření prvních definic povinných akcí Definice společného rodiče kontejnerů h-objektů Definice nestandardních akcí Realizace rozhovoru Zprovoznění zbylých testů Definice uživatelského rozhraní A Používané termíny B Šablony zdrojových kódů Literatura Rejstřík Strana 5 z 351

6 Podrobný obsah 6 Podrobný obsah Podrobný obsah Stručný obsah... 5 Podrobný obsah... 6 Seznam programů Seznam obrázků Seznam odboček podšeděných bloků Úvod Účel této publikace Komu je kniha určena Rozdělení textu Koncepce výkladu Terminologie Použitý jazyk Potřebné vybavení Doprovodné programy Formátování doprovodných programů Použité typografické konvence Odbočka podšeděný blok Zpětná vazba Zadání projektu Koncepce vyvíjené hry Co to je h-objekt Příklady her Shrnutí co jsme se dozvěděli Podpůrný framework Koncepce frameworku Vymezení mantinelů Doména pedu.eu a balíček eu.pedu Předpřipravené datové typy a soubory Usnadnění testů Propojení frameworku s vyvíjeným programem Umístění zdrojových kódů do balíčku Rodičovský balíček Dvě konvence Konvence respektující školu studenta Konvence nerespektující školu studenta Balíček studenta Balíček řadového čtenáře Balíček doprovodných programů Pojmenování projektů Vytváříte svůj projekt Strana 6 z 351

7 Podrobný obsah Jméno a ID autora Problémy klasického řešení Řešení v Javě Definice vlastního autorského interfejsu Interfejs IGSMFactory Definice vlastní tovární třídy Program Zipper Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Koncepce testů scénáře Jak testovat Programování řízené testy Jednotkové a integrační testy Možnosti testování naší hry Třída Scenario Kroky scénáře třída ScenarioStep Jednoslovnost názvů Typ kroků scénáře třída TypeOfStep Třída ScenarioStep podrobněji Typ scénáře třída TypeOfScenario Simulace průběhu hry Minimální požadavky na hru a její scénáře Diagram tříd balíčku scenario Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Správce scénářů Interfejs IScenarioManager Správce by měl být jedináček Spravované scénáře Jak získat hru k testování Jak získat objekt třídy Jak získat instanci tovární třídy Testování korektnosti správce scénářů Testování korektnosti hry Definice interfejsu IScenarioManager Společný rodič správců třída AScenarioManager Vytvoření scénářů Návrhový vzor Stavitel (Builder) Předání podkladů pro vytvoření scénářů společnému rodiči Shrnutí požadavků Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Návrh správce scénářů konkrétní hry Zadání, které budeme řešit Prázdný správce scénářů Import k odstranění Implementace interfejsu IAuthorPrototype Potomek třídy AScenarioManager Class-objekt sdružené tovární třídy Úprava sdružené tovární třídy Jednoduchý správce využívající literály Počáteční krok Strana 7 z 351

8 Podrobný obsah 8 Pokračovací kroky Přechod mezi scénáři Základní chybový scénář Umístění statických inicializačních bloků Konstruktor Hlavní metoda Trochu chytřejší správce Definice třídy Texts Definice správce scénářů využívajícího konstanty Prověrka ekvivalence obou správců Tovární třída v doprovodném projektu Úkol Dodatečný požadavek k usnadnění testů Náměty Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Sjednocený správce scénářů Řešený problém Návrhový vzor Strategie (Strategy) Použití návrhového vzoru Dekorátor Vytvoření dekorující třídy Přizpůsobení základním požadavkům Probírka definovaných instančních metod Definice konstruktoru Definice metody main(string[]) Použití dědění Vytvoření společné rodičovské třídy Úprava konstruktoru Definice nastavení požadovaného potomka Definice tovární metody Definice metody main(string[]) Úprava tovární třídy Bez propagace do dalších kapitol Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Návrh rámce pro hru Koncepce rámce hry Objekty vystupující ve hře Zadání Definice použitých interfejsů Hra IGame Svět IWorld Prostor IArea Příkaz Akce IAction Přechod H-objekt IItem Hráč Batoh IBag Úkol, cíl Množství, kapacita Upřesnění návrhu rámce IAction Název getname() Popis getdescription() Reakce na zadání příkazu execute(string[]) Strana 8 z 351

9 Podrobný obsah 9 IItem Název getname() Váha getweight() IBag Kapacita getcapacity() Obsah getitems() IArea Název getname() Sousedé prostoru getneighbors() H-objekty v prostoru getitems() IWorld Kolekce prostorů hry getallareas() Aktuální prostor getcurrentarea() IGame Identifikace autora getauthorname(), getauthorid() Tovární třída getfactoryclass() Název getname() Kolekce přípustných akcí getallactions() Získání světa hry getworld() Získání obsahu batohu getbag() Informace o živosti hry isalive() Povinné akce getbasicactions() Externí ukončení hry stop() Provedení příkazu executecommand(string) Doplnění společných rodičů Pojmenované objekty interfejs INamed Pomocné statické metody geto(string,?) Datový typ Optional<E> Kontejnery h-objektů interfejs IItemContainer Doplnění uživatelského rozhraní Diagram tříd balíčku game_txt Diagram tříd v balíčku empty_classes K čemu je dobrý rámec hry Shrnutí co jsme se v kapitole dozvěděli Začínáme tvořit vlastní hru Výběr metody návrhu Začínáme s třídou hry Předek ANamed Interfejs IAuthorPrototype Class-objekt tovární třídy Aktivace tovární metody Vyvíjíme podle TDD Kdo má mít na starosti zpracování příkazů Tři skupiny objektů Jak jsou na tom příkazy/akce? Dva druhy správců Správce jako instance samostatné třídy Správce jako objekt třídy Definice správce akcí třídy AAction Modifikátor přístupu Přístupová práva a jejich vlastnosti Konstruktor Neimplementovaná abstraktní metoda Příprava definice správce Strana 9 z 351

10 Podrobný obsah 10 Úprava definice konstruktoru akcí Konstruktor správce a vytvoření jednotlivých akcí Kde inicializovat Úprava metody execute(string) ve třídě hry Definice metody pro provádění příkazů Dva režimy zpracování příkazů Metoda isalive() ve třídě hry Metoda isalive() ve správci akcí AAction Definice metody executecommand(string) Metoda pro spuštění hry startgame(string) Metoda pro zpracování běžného příkazu executecommoncomand(string) Rozdělení příkazového řádku na slova Převedení počátečního slova na akci Prozatímní podoba definice třídy AAction Test Aktualizace informace o živosti hry Opakování testu Co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Vytváříme svět hry Analýza chybového hlášení Svět hry jako správce prostor hry Vytvoření třídy světa hry (Apartment) Jedináček Přístupová práva Definice metody getallareas() Třída prostorů hry (Room) Začínáme s úpravami Konstruktor prostoru Metoda getneighbors() Změna typu návratové hodnoty Metoda getitems() Zabezpečení nezměnitelnosti kontejneru Třída h-objektů v prostorech (Item) Kdo si má co pamatovat Mají-li h-objekty několik různých vlastností ovlivňujících jejich chování Název a váha/přenositelnost jako parametry konstruktoru Alternativní možnosti předání údajů Proč kódovat dodatečné údaje do prefixu Zveřejnění prefixů Vlastní definice Předběžné vytvoření třídy batohu Dokončení konstruktoru Přístupové metody Zdrojový kód ukázkové hry v doprovodném projektu Dokončení definice třídy prostorů (Room) Dokončení definice třídy světa hry (Apartment) Metoda getallareas() Doplnění konstruktoru Metoda getcurrentarea() Úprava metody getworld() ve třídě hry Test Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Dotahujeme svět hry Strana 10 z 351

11 Podrobný obsah Úvahy o inicializaci Inicializace světa hry a správce prostorů Nastavení výchozího prostoru Vyvolání inicializace jednotlivých prostorů Inicializace jednotlivých prostorů Nastavení h-objektů uložených v místnosti na počátku hry Příprava na nastavení počátečních sousedů Nastavení počátečních sousedů Test Oprava definice metody getbag() ve třídě hry Test Dotažení definice třídy batohu Metoda getitems() Metoda initialize() Začlenění inicializace batohu mezi ostatní Metoda getcapacity() Test Co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Vytvoření prvních definic povinných akcí Jak sjednotit postup pro většinu her Definice nového scénáře Úprava definice konstruktoru Úprava definice metody main(string[]) Definice akce pro nápovědu Zkopírování kostry třídy Rodičovská třída Metoda execute(string...) Doplnění těla metody getallactions() ve třídě hry Doplnění těla metody getallactions() ve třídě správce akcí Dokončení definice metody execute(string...) Proč to nechodí Vytvoření objektu definované akce Test Definice akce pro přesun z aktuálního prostoru do zadaného sousedního prostoru Metoda execute(string...) Existence parametru Korektnost parametru Realizace přesunu do cílového prostoru Test Definice akce pro zvednutí h-objektu v aktuálním prostoru a jeho přemístění do batohu Metoda execute(string...) Existence parametru Název zvedaného h-objektu Korektnost parametru Přítomnost h-objektu v aktuálním prostoru Přenositelnost požadovaného h-objektu Změna obsahu kolekce h-objektů v prostoru Úpravy definice třídy batohu Dokončení definice metody execute(string...) Test Co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Definice společného rodiče kontejnerů h-objektů Strana 11 z 351

12 Podrobný obsah Definice akce pro položení h-objektu Úvahy o výhodnosti společného předka LSP (Liskov Substitution Principle) Substituční princip Liskové Více předků prostoru Definice společného předka AItemContainer Kontrolní test Vlastní definice předka První úprava jeho budoucích potomků: tříd prostorů a batohu Dotváříme společného předka Úpravy odvozené od třídy prostorů Vytknutí konstant týkajících se h-objektů Úprava konstruktoru Přidání parametru rodičovskému konstruktoru Metoda getitems() Doplnění implementace interfejsu IItemContainer Metoda removeitem(item) Metoda initializeitems() Úpravy odvozené od třídy batohu Atributy items a exporteditems Konstruktor Proměnný počet parametrů a jeho specifika Metoda getitems() Metoda initialize() Metoda tryadditem(item) Přidání dalších metod Shrnutí Kontrolní test Pokračujeme v definici akce pro pokládání h-objektů ActionPutDown Test Příkaz k předčasnému ukončení hry ActionExit Metoda execute(string...) Test Aktualizace informace o živosti hry Dokončení definice metody execute(string...) Test Prověření programu chybovým scénářem Doplnění reakce na prázdný příkaz Test Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Definice nestandardních akcí Změna testu Definice akce pro otevření ledničky Definice třídy State Vytvoření třídy ActionOpen Definice metody execute(string...) Test existence parametru Test přítomnost otevíraného h-objektu Test otevíratelnosti zadaného h-objektu Dokončení definice Doplnění testu podloženost Test Oprava definice akce pro položení h-objektu Odhalování chyby krokováním programu Test Strana 12 z 351

13 Podrobný obsah Definice akce pro přečtení papíru Test existence parametru Test přítomnosti daného h-objektu v batohu Test, zda je požadováno přečtení h-objektu, který lze opravdu přečíst Test nasazených brýlí Výsledná podoba metody Test Definice akce pro nasazení brýlí Test Definice akce pro podložení ledničky Test plnosti batohu Definice metody execute(string...) Test Co jsme prozatím přeskočili Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Realizace rozhovoru Specifika rozhovoru Přepnutí do konverzačního režimu Zavedení příznaku prověřenosti Úprava metody execute(string...) ve třídě ActionPickUp Třída Conversation Metoda start(item) Úprava metody executecommand(string) ve třídě AAction Definice metody answer(string) ve třídě Conversation Stavy rozhovoru Reakce v závislosti na stavu Vlastní definice metody answer(string) Test Definice metody waitingfortheage(string) Test Definice metody waitingfortheyear(string) Test Příkaz pro zavření ledničky ActionCLose Test Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Zprovoznění zbylých testů Metoda stop() ve třídě RUPApartmentGame Test Metoda getbasicactions() ve třídě RUPApartmentGame Test Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Definice uživatelského rozhraní Co je třeba navrhnout Úprava rozdělení do balíčků Uživatelské rozhraní využívající služeb třídy javax.swing.joptionpane Definice metody startgame() Metody třídy JOptionPane Definice metody startgame(igame) Test Strana 13 z 351

14 Podrobný obsah Uživatelské rozhraní komunikující prostřednictvím standardního vstupu a výstupu Nevýhody používání standardního vstupu a výstupu Třída java.util.scanner Vytvoření třídy pro konzolový vstup Test Zobecnění uvedených řešení Interfejs IGamePlayer Hlavní metoda umožňující volbu použitého rozhraní Výsledná definice třídy UIC_GamePlayer Další zobecnění uživatelského rozhraní Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali A Používané termíny Rozhraní (interface) Signatura (signature) Kontrakt (contract) Interfejs (interface type) Rodičovský podobjekt Metoda (method) Konstruktor třídy (class constructor) Konstruktor instancí (instance constructor) Instance typu Xxx (Xxx instance) Mateřská třída objektu (object s mother class) Přetížení metody (method overloading) Zakrytí metody (method hiding) Přebití metody (method overriding) Předefinování metody (method redefining) Přepsání metody (method rewriting) Instance versus objekt Class-objekt datového typu Xxx (class-object of the Xxx data type) Kontejner B Šablony zdrojových kódů Šablona interfejsu Šablona třídy Literatura Rejstřík Strana 14 z 351

15 Seznam programů 15 Seznam programů Seznam programů Výpis 2.1: Definice interfejsu IAuthorPrototype, tentokrát i s deklarací balíčku a s importy (ty budou v příštích výpisech chybět) Výpis 2.2: Definice interfejsu IGSMFactory Výpis 3.1: Výčtový typ TypeOfStep Výpis 3.2: Výchozí podoba třídy ScenarioStep Výpis 3.3: Definice třídy TypeOfScenario Výpis 4.1: Definice metody getinstanceoffactory(class<t>) v interfejsu IGSMFactory Výpis 4.2: Definice interfejsu IScenarioManager Výpis 5.1: Výpis první části zdrojového kódu třídy RUPScenarioManagerLit až po definici prvních dvou kroků úspěšného (šťastného) scénáře Výpis 5.2: Výpis druhé části zdrojového kódu třídy RUPScenarioManagerLit od posledního kroku úspěšného (šťastného) scénáře až po definici prvních čtyř kroků chybového scénáře Výpis 5.3: Výpis třetí části zdrojového kódu třídy RUPScenarioManagerLit začínající za definicí kroků chybového scénáře (tj. definicí atributu s odkazem na jedináčka) a pokračující až do konce definice třídy Výpis 5.4: Výpis autotestu ukázkového správce scénářů z výpisů 5.1 až 5.3 se zakomentovanými kroky testujícími v chybovém scénáři reakci za zadání příkazů bez parametrů Výpis 5.5: Definice třídy Texts definující jednotlivé textové konstanty Výpis 5.6: Definice prvních tří kroků úspěšného scénáře ve třídě ManagerWithConstants Výpis 5.7: Definice metody getscenariomanager() ve třídě RUPGSMFactory Výpis 6.1: Definice statické nastavovací metody setdelegate(ascenariomanager) ve Výpis 6.2: třídě RUPScenarioManagerD Definice statické tovární metody getinstance() ve třídě RUPScenarioManagerD Výpis 6.3: Definice bezparametrického konstruktoru ve třídě RUPScenarioManagerD Výpis 6.4: Definice metody main(string[]) ve třídě RUPScenarioManagerD Výpis 6.5: Definice instančního konstruktoru ve třídě RUPScenarioManagerH Výpis 6.6: Výpis 6.7: Definice statické nastavovací metody setchild(rupscenariomanagerh) ve třídě RUPScenarioManagerH Definice statické tovární metody getinstance() ve třídě RUPScenarioManagerH Výpis 6.8: Definice statické tovární třídy RUPGSMFactory Výpis 7.1: Popis rozhraní konstruktoru instancí třídy BasicActions Výpis 8.1: Definice konstruktoru instancí třídy RUPApartmentGame Výpis 8.2: Definice metody getgame() ve třídě RUPGSMFactory Strana 15 z 351

16 Seznam programů 16 Výpis 8.3: Začátek chybového hlášení při prvním spuštění hry oznamující nedotaženou definici metody isalive() ve třídě RUPAppartmentGame Výpis 8.4: Definice metod isalive() ve třídě RUPAppartmentGame Výpis 8.5: Klíčová část hlášení o průběhu testu po dokončení definice metody isalive() ve třídě RUPAppartmentGame oznamující nedotaženou definici metody executecommand(?) ve třídě RUPAppartmentGame Výpis 8.6: Upravená definice konstruktoru instancí třídy AAction Výpis 8.7: Výchozí podoba statického inicializačního bloku ve třídě AAction spolu s jeho umístěním do správné sekce Výpis 8.8: Definice metody executecommand(string) ve třídě RUPAppartmentGame Výpis 8.9: Upravená definice metody isalive() ve třídě RUPAppartmentGame Výpis 8.10: Prozatímní podoba definice třídy AAction Výpis 8.11: Výpis 8.12: Klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy AAction oznamující obdržení špatné informace o živosti hry Klíčová část hlášení o průběhu testu po úpravě metody startgame(string) ve třídě AAction oznamující nedokončenou metodu getworld(?) ve třídě RUPAppartmentGame Výpis 9.1: Definice konstruktoru instancí třídy Room a jím inicializovaných atributů Výpis 9.2: Definice třídy Item, jejíž instance představují h-objekty Výpis 9.3: Upravená definice konstruktoru instancí třídy Apartment Výpis 9.4: Upravená definice metody getcurrentarea() ve třídě Apartment Výpis 9.5: Upravená definice metody getworld() ve třídě RUPApartmentGame Výpis 9.6: Výpis 10.1: Klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy Appartment oznamující nevrácení aktuálního prostoru Upravená definice metody startgame(string) ve třídě AAction spolu s nově definovanou metodou initialize() Výpis 10.2: Definice metody initialize() ve třídě Apartment Výpis 10.3: Definice metody initialize() ve třídě Room Výpis 10.4: Definice metody initializeitems() ve třídě Room Výpis 10.5: Definice metody getoroom(string)ve třídě Apartment Výpis 10.6: Definice metody initializeneighbors() ve třídě Room Výpis 10.7: Klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy Room oznamující nedotažení definice metody getbag() ve třídě RUPApartmentGame Výpis 10.8: Definice metody getbag() ve třídě RUPApartmentGame Výpis 10.9: Klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy batohu; zpráva oznamuje neschopnost hry reagovat na příkaz Jdi Koupelna Výpis 11.1: Definice pole REQUIRED_STEPS ve třídě RUPScenarioManagerCon Výpis 11.2: Upravená definice konstruktoru třídy RUPApartmentManager Výpis 11.3: Upravená definice metody main(string[]) ve třídě RUPScenarioManagerCon Výpis 11.4: Definice konstruktoru instancí třídy ActionHelp Výpis 11.5: Definice metody getallactions() ve třídě RUPApartmentGame Výpis 11.6: Definice metody getallactions() ve třídě AAction Výpis 11.7: Definice metody execute(string...) ve třídě ActionHelp Strana 16 z 351

17 Seznam programů 17 Výpis 11.8: Upravená definice konstruktoru třídy AAction, která nyní zahrnuje i vytvoření první akce Výpis 11.9: Klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy ActionHelp; zpráva oznamuje neschopnost hry reagovat na příkaz Jdi Koupelna Výpis 11.10: Upravená definice konstruktoru ve třídě ActionMove Výpis 11.11: Definice metody execute(string...) ve třídě ActionMove Výpis 11.12: Klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy ActionMove; zpráva oznamuje neschopnost hry reagovat na příkaz Vezmi Brýle Výpis 11.13: Definice konstruktoru instancí třídy ActionPickUp Výpis 11.14: Definice metody removeitem(item) ve třídě Room Výpis 11.15: Definice metody tryadditem(item) ve třídě Hands Výpis 11.16: Definice metody execute(string...) ve třídě ActionPickUp Výpis 11.17: Klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy ActionPickUp; zpráva oznamuje neschopnost hry reagovat na příkaz Polož Brýle Výpis 12.1: Definice konstruktoru instancí třídy ActionPutDown Výpis 12.2: Zopakovaná klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy ActionPickUp; zpráva oznamuje neschopnost hry reagovat na příkaz Polož Brýle Výpis 12.3: Upravená definice konstruktoru ve třídě Room Výpis 12.4: Definice třídy AItemContainer Výpis 12.5: Upravená definice třídy Room Výpis 12.6: Upravená definice Hands Výpis 12.7: Definice metody execute(string...) ve třídě ActionPutDown Výpis 12.8: Klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy ActionPutDown; zpráva oznamuje neschopnost hry reagovat na příkaz Konec Výpis 12.9: Definice konstruktoru instancí třídy ActionExit Výpis 12.10: Klíčová část hlášení o průběhu testu po dokončení předběžné definice metody execute(string...) ve třídě ActionPutDown oznamující nekorektní informaci o živosti hry Výpis 12.11: Definice metody stopgame() ve třídě AAction Výpis 12.12: Definice metody execute(string...) ve třídě ActionExit Výpis 12.13: Klíčová část hlášení o průběhu testu po dokončení výsledné podoby definice metody execute(string...) ve třídě ActionPutDown oznamující úspěšný průchod testem scénáře pojmenovaného REQUIRED Výpis 12.14: Výpis 12.15: Výpis 12.16: Výpis 13.1: Klíčová část hlášení o průběhu testu po přepnutí na prověření chodu aplikace prostřednictvím chybového scénáře Upravená definice metody executecommoncomand(string) ve třídě AAction Upravená definice metody executecommoncomand(string) ve třídě AAction Klíčová část hlášení o průběhu testu po startu celkového testu hry v projektu A112z_RequiredActionsCompleted oznamující, že hra neumí zpracovat příkaz Otevři Lednička Strana 17 z 351

18 Seznam programů 18 Výpis 13.2: Upravená definice statické metody initialize() třídy AAction Výpis 13.3: Předběžné podoba definice třídy State Výpis 13.4: Definice konstruktoru instancí třídy ActionOpen Výpis 13.5: Definice chybových textů ve třídě Texts definujících reakce programu na chybná zadání uživatele pokoušejícího se otevřít ledničku Výpis 13.6: Klíčová část hlášení o průběhu testu po dokončení předběžné definice metody execute(string...) ve třídě ActionOpen oznamující špatnou reakci na zadání příkazu Otevři Lednička Výpis 13.7: Definice metody execute(string...) ve třídě ActionOpen Výpis 13.8: Klíčová část hlášení o průběhu testu po doplnění definice metody execute(string...) ve třídě ActionOpen; zpráva oznamuje špatnou reakci na zadání příkazu Vezmi Papír Výpis 13.9: Definice metody removeitem(item) ve třídě Hands Výpis 13.10: Klíčová část hlášení o průběhu testu po dokončení definice metody removeitem(item) ve třídy Hands; zpráva oznamuje neschopnost hry reagovat na příkaz Přečti Papír Výpis 13.11: Definice metody execute(string...) ve třídě ActionRead Výpis 13.12: Klíčová část hlášení o průběhu testu po dokončení definice třídy ActionRead; zpráva oznamuje neschopnost hry reagovat na příkaz Polož Brýle Výpis 13.13: Definice metody execute(string...) ve třídě ActionPutOnGlasses Výpis 13.14: Klíčová část hlášení o průběhu testu po dokončení definice třídy ActionPutOnGlasses; zpráva oznamuje neschopnost hry reagovat na příkaz Podlož Lednička Časopis Výpis 13.15: Definice metody isfull() instancí třídy ItemContainer Výpis 13.16: Definice metody execute(string...) instancí třídy ActionSupportIcebox Výpis 13.17: Klíčová část hlášení o průběhu testu po dokončení definice třídy ActionPutOnGlasses; zpráva oznamuje neschopnost hry reagovat na příkaz Podlož Lednička Časopis Výpis 14.1: Upravená definice metody execute(string...) ve třídě ActionPickUp Výpis 14.2: Počáteční verze definice metody start(item) ve třídě Conversation Výpis 14.3: Upravená definice metody executecommand ve třídě AAction Výpis 14.4: Definice metody answer(string) ve třídě Conversation Výpis 14.5: Klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy Conversation; zpráva oznamuje nedokončenost definice metody waitingfortheage(string) Výpis 14.6: Definice metody waitingfortheage(string) ve třídě Conversation Výpis 14.7: Definice dalších konstant ve třídě Texts Výpis 14.8: Klíčová část hlášení o průběhu testu po dokončení definice metody waitingfortheage(string) ve třídě Conversation; zpráva oznamuje nedokončenost definice metody waitingfortheyear(string) Výpis 14.9: Definice metody waitingfortheyear(string) ve třídě Conversation Výpis 14.10: Klíčová část hlášení o průběhu testu po dokončení definice metody waitingfortheyear(string) ve třídě Conversation; zpráva oznamuje neschopnost hry reagovat na příkaz Zavři Lednička Výpis 14.11: Definice metody execute(string...) instancí třídy ActionCLose Strana 18 z 351

19 Seznam programů 19 Výpis 14.12: Výpis 15.1: Výpis 15.2: Klíčová část hlášení o průběhu testu po dokončení definice třídy ActionCLose; zpráva oznamuje nedotaženost metody stop() ve třídě RUPApartmentGame Klíčová část hlášení o průběhu testu po úpravě definice metody stop() ve třídě RUPApartmentGame; zpráva oznamuje neplnohodnotnou definici metody getbasicactions() Definice konstanty basicactions a metody getbasicactions() ve třídě RUPApartmentGame Výpis 16.1: Definice metody startgame() ve třídě UIA_JOptionPane Výpis 16.2: Definice metody startgame(igame) ve třídě UIA_JOptionPane Výpis 16.3: Definice metody startgame(igame) ve třídě UIB_Scanner Výpis 16.4: Definice metody startgame(igame) ve třídě UIB_Scanner Výpis 16.5: Definice interfejsu IGamePlayer Výpis 16.6: Definice třídy UIC_GamePlayer Výpis 16.7: Definice třídy UID_Multiplayer Výpis B.1: Výpis B.2: Definice (polo)prázdného interfejsu s řádkovými komentáři vyznačujícími jednotlivé sekce kódu Definice (polo)prázdné třídy s řádkovými komentáři vyznačujícími jednotlivé sekce kódu Strana 19 z 351

20 Seznam obrázků 20 Seznam obrázků Seznam obrázků Obrázek 3.1 Jednoduchý diagram tříd balíčku scenario Obrázek 5.1 Diagram tříd projektu A105z_ScenarioManager, zachycujícího stav po definici dvou verzí správců scénářů Obrázek 6.1: Dialogové okno Introduce Local Extension Obrázek 7.1 Diagram tříd balíčku game_txt se zobrazenými vzájemnými závislostmi Obrázek 7.2 Zjednodušený diagram tříd balíčku game_txt zobrazující pouze vztahy rodič potomek Obrázek 7.3 Jednoduchý diagram tříd balíčku empty_classes Obrázek 8.1 Diagram tříd projektu A108z_ActionManager zachycujícího stav poté, co byly definovány kostry tříd reprezentujících hru a správce akcí Obrázek 9.1 Diagram tříd projektu A109z_Apartment zachycujícího stav poté, co byly definovány kostry tříd reprezentujících svět hry, prostory, h-objekty a batoh Obrázek 10.1 Diagram tříd projektu A110z_GameStarted zachycujícího stav poté, co byla v testovacím režimu rozběhnuta hra Obrázek 11.1 Diagram tříd projektu A111z_FirstRequiredActions zachycujícího stav po definici tříd reprezentujících akce nápovědy, přechodu do sousedního prostoru a zvednutí h-objektu Obrázek 12.1: Chyby v konstruktoru třídy Room po odstranění konstant týkajících se h-objektů Obrázek 12.2 Diagram tříd projektu A112z_RequiredActionsCompleted, zachycujícího stav po definici tříd reprezentujících nepovinné akce Obrázek 13.1 Diagram tříd projektua113z_nonstandardactions zachycujícího stav poté, co byly definovány třídy reprezentující nestandardní příkazy hry bez podpory rozhovoru Obrázek 14.1 Stavový diagram rozhovoru přechody mezi stavy v závislosti na odpovědi hráče Obrázek 14.2 Diagram tříd projektua114z_conversationadded zachycujícího stav poté, co byly definována třídy realizující podporu rozhovoru Obrázek 16.1 Dialogové okno s úvodní zprávou hry čekající na zadání příkazu Obrázek 16.2 Aktuální diagram tříd balíčku s doposud definovanými třídami řešícími textové uživatelské rozhraní Strana 20 z 351

21 Seznam odboček podšeděných bloků 21 Seznam odboček podšeděných bloků Seznam odboček podšeděných bloků Odbočka podšeděný blok Co to je h-objekt Doména pedu.eu a balíček eu.pedu Návrhový vzor Stavitel (Builder) Návrhový vzor Strategie (Strategy) Datový typ Optional<E> Dva druhy správců Přístupová práva a jejich vlastnosti Kde inicializovat Změna typu návratové hodnoty Zabezpečení nezměnitelnosti kontejneru Kdo si má co pamatovat LSP (Liskov Substitution Principle) Substituční princip Liskové Proměnný počet parametrů a jeho specifika Strana 21 z 351

22 Úvod 22 Úvod Úvod Účel této publikace Většina úvodních kurzů objektově orientovaného programování seznámí své studenty s řadou objektových konstrukcí a způsobem jejich zápisu v nějakém programovacím jazyku, ale ne vždy se v nich najde dostatek prostoru k tomu, aby studenti mohli prokázat nabyté znalosti na nějakém netriviálním programu, ve kterém by byla použita většina konstrukcí, které si měli studenti v průběhu výuky osvojit, a který by obsahoval alespoň 20 vzájemně provázaných a kooperujících datových typů. Na VŠE už více než 10 let používáme k tomuto účelu zadání, podle nějž mají studenti vyvinout textovou konverzační hru, která vyhovuje sadě předem zadaných omezujících podmínek. Výhodou této aplikace je, že lze relativně snadno dosáhnout toho, aby každý student vyvíjel něco maličko jiného, ale na druhou stranu se jednotlivá řešení neliší natolik, aby nebylo možno vytvořit testovací program, který by všechna tato řešení přiměřeně zkontroloval. Jednou z výhod této úlohy je, že díky koncepci zadání a z něj odvozeného frameworku je možno definovat relativně univerzální testovací program, který bude kontrolovat nejenom algoritmickou správnost odevzdaného programu, ale z větší části i jeho architekturu. Přetrvávajícím problémem však je nedostatek prostoru k dostatečnému procvičení řešení obdobně komplexního problému na hodinách a z toho vyplývající problémy studentů při řešení závěrečné úlohy, která je ve svém celku přece jenom citelně složitější než relativně jednoduché AHA-příklady 1 vyvíjené na cvičeních. Domnívám se, že v této knize popisovaná aplikace a rámec, nad nímž je postavena, mohou být rozumnou inspirací pro vyučující, kteří hledají příklad pro semestrální či závěrečné práce nejenom na univerzitách, ale i na řadě středních škol, které se výuce programování věnují nad rámec povinných osnov. Neskromně si přitom myslím, že školy, jejichž stěžejním cílem není vychovat budoucí špičkové vývojáře a architekty, mohou tuto koncepci převzít v podstatě v té podobě, v jaké je v této publikaci vyložena. 1 Termín AHA-příklad používám pro příklady, jejichž základním cílem je demonstrovat princip funkce vysvětlované konstrukce vyvolat AHA-efekt. V zájmu tohoto cíle je proto zadání maximálně zjednodušeno a v drtivé většině případů naznačuje praktickou aplikovatelnost dané konstrukce jen velmi náznakově. Strana 22 z 351

23 Úvod 23 Komu je kniha určena Tato kniha má sloužit jako doplněk mých učebnic zabývajících se architekturou objektově orientovaných programů. Je primárně určena studentům úvodních kurzů programování, kteří na školách, na nichž učím, připravují svůj semestrální projekt a nedokáží si správně zapamatovat vše, co jsme si na přednáškách či cvičeních řekli. Má proto sloužit jako podklad k zopakování si látky z hodiny a současně jako vodítko při vývoji své vlastní aplikace. Obecně je publikace určena jako doprovodný materiál pro začátečnické kurzy, v nichž mají studenti prokázat schopnost tvorby složitějšího programu sestávajícího z řady navzájem provázaných tříd a respektujícího požadavky jakéhosi definičního rámce, který pro vytvářený program vymezuje jisté mantinely. Vycházím z toho, že v současné době už programátoři musejí prakticky vždy vytvářet své programy tak, aby vyhovovaly nějakému rámci, který je většinou vymezen použitým frameworkem. Tvorba programů postavených zcela na zelené louce, tj. programů, které se nemusejí ničemu přizpůsobovat, je v současné době poměrně výjimečná. Kniha přepokládá, že čtenář zná základy objektově orientovaného programování na úrovni základního kurzu mohli bychom říci na úrovni probrané v mých učebnicích Java 7 Učebnice objektové architektury pro začátečníky Java 8 Úvod do objektové architektury pro mírně pokročilé Výklad na tyto učebnice navazuje a ukazuje čtenářům, jak postupovat při návrhu poněkud většího a komplexnějšího projektu, než jsou projekty probírané na hodinách. Současně jim má pomoci osvojit si práci s externími knihovnami a frameworky. Rozdělení textu Původně jsem chtěl napsat rozsáhlejší publikaci sestávajících z několika částí. Pak by se ale s vydáním muselo počkat až na dobu, kdy budou všechny části napsané. Nakonec jsem se proto rozhodl vydat každou část samostatně. Původně plánované části se tak stávají samostatnými publikacemi, které na sebe budou navazovat. Plán je takový, že první z nich (ta, kterou právě čtete) bude obsahovat návod k vytvoření poněkud složitějšího programu, který se na středních školách může stát ročníkovou prací a na vysokých školách jednou ze semestrálních prací (záleží na rozsahu výuky). Strana 23 z 351

24 Úvod 24 Druhá publikace předvede, jak lze v programu pracovat s databází. Data, která byla v úvodním dílu součástí různých kontejnerů, budou na jejím počátku uložena do jednoduché databáze, s jejímž obsahem pak bude program pracovat. Třetí publikace bude předpokládat základní znalosti tvorby grafického uživatelského rozhraní. Ukážeme si v ní, jak lze původně textovou aplikaci z první publikace dovybavit grafickým uživatelským rozhraním. Navíc si předvedeme, jak je možno takovéto rozhraní typizovat, aby mohlo několik různých programů používat stejné rozhraní, resp. aby mohl jeden program používat různá rozhraní. Poslední z této série publikací bude věnována učitelům a zkušenějším studentům. Ukážu v ní, jak je možno vybudovat rámec definující ony výše zmíněné mantinely pro vytvářené programy a jak je pak možno tohoto rámce využít k automatizaci kontroly dodržení požadovaných vlastností zadání a následně funkčnosti odevzdaného řešení. Současně bych chtěl v tomto díle předvést, jak je možno definovat požadavky na modifikaci odevzdávaného řešení při obhajobě odevzdané práce. Ukážeme si, jak je možno specifikovat požadavky, jejichž splnění vyžaduje minimální změny v programu, avšak na druhou stranu vyžaduje jeho dobrou znalost, aby student věděl, kde a jak má něco změnit. Koncepce výkladu Ve výkladu se střídají pasáže popisující, co se má řešit, s pasážemi probírajícími teorii, jak se podobné třídy problémů řeší, a pasážemi zadávajícími konkrétní kroky k vyřešení zadaného problému. Vím, že mnohé studenty teorie nezajímá a chtějí jenom návod k tomu, jak problém vyřešit. I když s tímto přístupem tak docela nesouhlasím, tak jsem jim vyšel vstříc, a konkrétní rady, jak postupovat, uvádím v modře číslovaných odstavcích obdobných jako následující dva odstavce. 1. Tyto odstavce začínají v každé kapitole obsahující návod krokem číslo V těchto doporučeních postupu se zabývám především návody, jak to či ono zakódovat. Dobří programátoři ale doprovázejí svůj kód vhodnými dokumentačními komentáři. Předpokládám proto, že čtenář bude iniciativě doplňovat potřebné dokumentační komentáře i tehdy, když je v kódu vysloveně neuvedu (většinou tam budou). Toto uspořádání by mělo pomoci i těm, kteří si sice potřebnou teorii rádi přečtou, ale občas se jim doporučené kroky v okolním textu ztratí, takže pak některý přeskočí a tím do programu zanesou zbytečnou chybu, kterou pak musejí pracně hledat. Strana 24 z 351

25 Úvod 25 Terminologie Protože do mých kurzů chodí i studenti, kteří před tím prošli úvodními kurzy jiných učitelů, tak se občas stane, že jsou z těchto kukrů zvyklí používat jinou terminologii, než je ta, kterou používám ve svých kurzech já, a pak dochází k různým nedorozuměním. Abychom sjednotili používané názvosloví a nedocházelo k nedorozuměním, je v příloze A Používané termíny na straně 341 uveden seznam nejdůležitějších termínu spolu se stručným popisem jejich významu. Použitý jazyk Vím, že jsem známý tím, že používám ve svých programech české identifikátory. Vedly mne k tomu špatné zkušenosti s úrovní znalostí angličtiny mých studentů. Při stoupající pravděpodobnosti, že moji studenti nastoupí do některé z firem, v nichž pracují zaměstnanci z různých koutů světa a v nichž se proto primárně hovoří a programuje anglicky, jsem nakonec od této zásady upustil a začal ve všech svých učebnicích používat anglické identifikátory 2. Komentáře zůstávají české. Potřebné vybavení Pro úspěšné studium této knihy budete potřebovat dvě věci: dostatečně výkonný počítač, základní vývojovou sadu Javy (v této učebnici je použita Java 8). Pro úspěšný vývoj programů potřebujete ještě vhodný vývojový nástroj. Je zcela na vás, jaký nástroj zvolíte. Na hodinách používáme NetBeans a na toto IDE se budu občas odkazovat i ve výkladu. Toto vývojové prostředí je pro účely výuky pravděpodobně nejvhodnější 3 (alespoň recenze tak hovoří, a já s nimi po svých zkušenostech s ostatními profesionálními prostředími souhlasím), ale jeho použití není nezbytně nutné. Někteří studenti dávají přednost zažitému a maximálně jednoduchému prostředí BlueJ, které používám v úvodních hodinách, jiní použijí některé z dalších profesionálních prostředí většinou Eclipse nebo IntelliJ IDEA. 2 Přiznejme si, že tomu napomohl i stoupající zájem o anglická a další vydání mých učebnic a neúměrná pracnost převodu identifikátorů do jiných jazyků. 3 Myslím tím pokročilejší výuku, při níž má smysl použít profesionální vývojové prostředí. Pro úvodní kurzy určené naprostým začátečníkům je prozatím stále nejlepší prostředí BlueJ. Strana 25 z 351

26 Úvod 26 Doprovodné programy Všechny doprovodné programy zmiňované a používané v textu najdete na stránce knihy adrese 4. Měly byste zde najít tři soubory s následujícími názvy (alespoň v době psaní tohoto textu jmenují): Soubor Adv_15P_FW.ZIP obsahující použitý framework viz kapitolu 2 Podpůrný framework na straně 35. Označení 15P symbolizuje, že je to verze frameworku platná pro podzimní semestr roku V dalších pololetích proto není vyloučeno, že se změní, aby se nedaly příliš snadno přebírat práce z minulých semestrů. Generátor projektů uložený v souboru, který měl v době přípravy prvního vydání název Adv_TXT_v _ jar. Jak jistě odhadujete, jeho název obsahuje několik upřesňujících informací o jeho verzi: Úvodní text Adv_TXT označuje, že se jedná o soubor s generátorem programů pro textovou verzi adventury. Následující v15 oznamuje, že se jedná o verzi pro rok Následuje číslo podverze, z nějž vyčtete, že tento generátor je určen pro podzimní semestr. Obecně jsou podverze pro jarní semestr číslovány od 00 do 49, verze pro podzimní semestr pak čísly od 50 do 99. Poslední čtyřčíslí je verze v mém interním archivu verzí a čtenář je může ignorovat. Jak jistě sami odhadnete, následuje datum vytvoření daného generátoru. Program Zipper, který moji studenti používají k odevzdávání svých prací. (O jeho použití hovořím v podkapitole 2.8 Program Zipper na straně 48.) I on má v názvu číslo verze, ale to má informační hodnotu pouze pro učitele, kteří by jej chtěli používat. Formátování doprovodných programů Zkušenosti s výukou ukazují, že zejména začínající programátoři mají velký problém s orientací ve svých zdrojových kódech. Zavedl jsem proto ve svých učebnicích a doprovodných programech jednotné uspořádání kódu, které dodržuje následující zásady: 4 Překlad té závěrečné šifry je jednoduchý: jedná se o moji 49. knihu, která se zabývá vývojem aplikace označované jako Adventura, přičemž tato kniha pojednává o verzi s textovým uživatelským rozhraním. Strana 26 z 351

27 Úvod 27 Zdrojový kód libovolného datového typu (třídy i interfejsu) je rozdělen na tři části (sekce) Na počátku definice jsou třídní (= statické) členy, za nimi následují instanční (=nestatické) členy a na konci je pak sekce pro definice interních datových typů, u nichž už většinou nerozlišuji, zda jsou definovány jako vnořené (= statické) nebo vnitřní (=nestatické). Jedinou výjimkou jsou statické tovární metody, které jsou umístěny v sekci konstruktorů instancí, protože v kódu tyto konstruktory většinou nahrazují. Každá sekce je rozdělena na tři podsekce: Na počátku sekce jsou definovány atributy, přičemž nejprve jsou vždy definovány konstanty a teprve za nimi pak příslušné proměnné. Za atributy jsou definovány konstruktory a v instanční sekci také statické tovární metody. Za konstruktory následuje podsekce metod, která má také podrobnější dělení: nejprve jsou deklarovány abstraktní metody, za nimi konkrétní (u interfejsů implicitní/defaultní) metody. V každé z těchto částí jsou pak nejprve definovány přístupové metody (getry a setry) a teprve za nimi ostatní nesoukromé metody. Na konci podsekce jsou pak definovány soukromé metody. Šablony definice třídy a interfejsu si můžete prohlédnout v příloze B Šablony zdrojových kódů na straně 345. Podle dosavadních ohlasů studenti nejvíce oceňují striktní rozdělení definice na část určenou třídním členům a část určenou instančním členům. Zejména začátečníci používající BlueJ, který nenabízí žádný navigátor v rámci zdrojového kódu, pak vítají i to, že mají pokaždé poměrně jasnou představu, kde ten či onen člen hledat. Použité typografické konvence K tomu, abyste se v textu lépe vyznali a také abyste si vykládanou látku lépe zapamatovali, používám několik prostředků pro odlišení a zvýraznění textu. Strana 27 z 351

28 Úvod 28 Termíny Názvy Citace Adresy Program metoda(?) První výskyt nějakého termínu a další texty, které chci zvýraznit, vysazuji tučně. Názvy firem a jejích produktů vysazuji kurzivou. Kurzivou vysazuji také názvy kapitol, podkapitol a oddílů, na které se v textu odkazuji. Texty, které si můžete přečíst na displeji, např. názvy polí v dialogových oknech či názvy příkazů v nabídkách, vysazuji tučným bezpatkovým písmem. Názvy souborů a internetové adresy vysazuji obyčejným bezpatkovým písmem. Texty programů a jejich částí vysazuji neproporcionálním písmem, které je v elektronických verzích textu pro zvýraznění tmavě červené. Při odkazech na metody budu v závorkách za názvem metody vždy uvádět seznam typů jejich parametrů např. equals(object). Nebudeli v danou chvílí jasné, jaké má zmiňovaná metoda parametry, budu do závorek psát otazník. Kromě částí textu, které považuji za důležité zvýraznit nebo alespoň odlišit od okolního textu, najdete v textu ještě řadu doplňujících poznámek a vysvětlivek. Všechny budou v jednotném rámečku, který bude označen ikonou charakterizující druh informace, kterou vám chce poznámka či vysvětlivka předat. Symbol jing-jang bude uvozovat poznámky, s nimiž se setkáte na počátku každé kapitoly. Zde vám vždy prozradím, co se v dané kapitole naučíte. Otevřená schránka s dopisy označuje informace o projektu, s nímž budeme v dalším textu pracovat, nebo v něm najdete vzorové řešení aplikující probranou látku. Příslušný projekt získáte pomocí generátoru projektů popsaného výše. Obrázek knihy označuje poznámku týkající se používané terminologie. Tato poznámka většinou upozorňuje na další používané termíny označující stejnou skutečnost nebo na konvence, které se k probírané problematice vztahují. Seznam všech terminologických poznámek najdete v rejstříku pod heslem terminologie. Strana 28 z 351

29 Úvod 29 Obrázek počítače označuje zadání úkolu, který máte samostatně vypracovat. Seznam všech úloh najdete v rejstříku pod heslem úloha. Píšící ruka označuje obyčejnou poznámku, která pouze doplňuje informace z hlavního proudu výkladu o nějakou zajímavost. Ruka s hrozícím prstem upozorňuje na věci, které byste měli určitě vědět a na které byste si měli dát pozor, protože jejich zanedbání vás většinou dostane do problémů. Usměváček vás bude upozorňovat na různé tipy, kterými můžete vylepšit svůj program nebo zefektivnit svoji práci. Mračoun vás naopak bude upozorňovat na různá úskalí programovacího jazyka nebo programů, s nimiž budeme pracovat, a bude vám radit, jak se těmto nástrahám vyhnout či jak to zařídit, aby vám alespoň pokud možno nevadily. Brýle označují tzv. poznámky pro šťouraly, ve kterých se vás snažím seznámit s některými zajímavými vlastnostmi probírané konstrukce nebo upozorňuji na některé souvislosti, avšak které nejsou k pochopení látky nezbytné. Odbočka podšeděný blok Občas je potřeba vysvětlit něco, co nezapadá přímo do okolního textu. V takových případech používám podšeděný blok se silnou čarou po straně. Tento podšeděný blok je takovou drobnou odbočkou od ostatního výkladu. Nadpis podšeděného bloku pak najdete i v podrobném obsahu mezi nečíslovanými nadpisy. Strana 29 z 351

30 Úvod 30 Zpětná vazba Kniha vychází přesně v té podobě, v jaké opustila můj počítač. Protože jsme chtěli, aby vás kniha přišla co nejlevněji a aby byla co nejdříve dostupná, tak text neprošel závěrečnou redakční ani typografickou úpravou. Cena knihy je tak dána pouze režijními náklady nakladatele a marží prodejce. Domnívali jsme se totiž, že potenciální čtenáři budou tento přístup upřednostňovat. Z uvedených důvodů lze očekávat, že v knize může být větší množství chyb, než je v knihách, které těmito závěrečnými operacemi prošly. Objevíte-li proto v knize nějakou chybu nebo budete mít návrh na nějaké její vylepšení, neostýchejte se napsat na adresu rudolf@pecinovsky.cz. Pokusím se co nejdříve zanést na stránku knihy příslušná errata s opravou, kterou pak zapracujeme do případného dalšího vydání. Pokud vám bude někde připadat text nepříliš srozumitelný nebo budete mít nějaký dotaz, ať už k vykládané látce či použitému vývojovému prostředí, pošlete mail s předmětem ADVENT_DOTAZ. Bude-li se dotaz týkat něčeho obecnějšího, zveřejním na stránce knihy odpověď i pro ty ostatní, které by mohl obdobný dotaz napadnout za pár dní, anebo jsou natolik ostýchaví, že se netroufnou sami zeptat. Strana 30 z 351

31 Kapitola 1: Zadání projektu Zadání projektu Kapitola 1 Zadání projektu Co se v kapitole naučíte Tato kapitola je nejkratší kapitolou celé knihy. Zde si vysvětlíme zadání projektů, pro které v následujících kapitolách navrhneme společný rámec a následně vytvoříme jeden ukázkový projekt 1.1 Koncepce vyvíjené hry Vytvářenou aplikací bude jednoduchá konverzační hra, při níž se hráč snaží v konečném počtu kroků dostat k předem zadanému cíli. Vždy programu zadá textový příkaz naznačující, co má v daném okamžiku udělat, a program mu odpoví. V odpovědi hráči vysvětlí, v jakém stavu se po provedení tohoto příkazu nachází, a případně mu naznačí, jaké jsou jeho další možnosti. Bude to tedy takový rozhovor anebo, chcete-li, textový pink-ponk: hráč pošle text příkazu, program vrátí text odpovědi a tak stále kolem dokola, dokud hru nedohraje do konce (nezávisle na tom, zda dosáhne či nedosáhne požadovaného cíle hry) nebo dokud jej hra nepřestane bavit. Instance hry bude přitom definována jako jedináček, který po ukončení jednoho běhu bude schopen opětného spuštění nového běhu hry, aniž by bylo třeba ukončovat program 5. Hra bude probíhat ve virtuálním světě, v němž existuje několik prostorů, které spolu zadaným způsobem sousedí. Těmito prostory mohou být místnosti v budově, části krajiny, planety, etapy života apod. Hra musí umět provést akci realizující přechod z aktuálního prostoru do sousedního prostoru. 5 Požadavek na jedináčkovství hry je vyvolán potřebou přimět studenty řešit problematiku reinicializace objektu hry při jejím opětném spuštění a ujasnit si, co vše je třeba při spuštění hry uvést znovu do počátečního stavu. Strana 31 z 351

32 Kapitola 1: Zadání projektu 32 Za sousední prostor přitom považujeme takový, do nějž lze jednoduše přejít. Je-li třeba nejprve splnit nějakou podmínku, stane se onen potenciálně sousední prostor skutečně sousedním až po splnění dané podmínky. Tyto podmínky mohou být různé: Jsou-li sousedními prostory místnosti v budově, může být potřeba nejprve odemknout spojovací dveře. Jsou-li sousedními prostory části krajiny, můžeme se na sousední louku za potokem dostat např. až poté, co postavíme most přes potok. Jsou-li sousedními prostory semestry vysoké školy, kterou máte za cíl absolvovat, musíte odemknout následující semestr třeba tak, že dojdete na studijní oddělení a ukážete index s absolvovanými h-objekty, aby vás mohli zapsat do dalšího semestru. V každém prostoru se mohou nacházet různé h-objekty (viz podšeděný blok). Některé z nich může hráč vzít, uložit je do pomyslného batohu, aby mu v budoucnu pomohly ke splnění nějakého pomocného úkolu nebo dokonce cíle celé hry. Hra musí proto umět provést akci, která přesune zadaný h-objekt z aktuálního prostoru (tj. prostoru, v němž se právě nachází hráč) do batohu, a současně akci, která naopak přesune h-objekt z batohu do aktuálního prostoru. Co to je h-objekt Termín h-objekt používám z nedostatku nápadů na lepší pojmenování. Termín objekt, který by byl v dané situaci nejvýstižnější, vyvolává problémy při potřebě odlišení obecného objektu v programu a objektu (h-objektu) ve hře. Říkat mu položka, což by byl další možný překlad anglického item, zvoleného jako identifikátor pro tento druh objektů ve frameworku a ukázkovém programu, mi také nepřipadalo zcela vhodné. Pak by totiž mohla mít vaše hra v jeskyni tři položky: ohniště, meč a trpaslíka. Stejně mi připadala i synonyma předmět nebo věc, protože (jak jsem před chvílí naznačil) bychom pak mezi předměty/věci museli zařadit i pohádkové bytosti, osoby, zvířata a další tvory, které by se ve vašich hrách vyskytly. Nakonec jsem proto zakotvil u termínu h-objekt (= objekt hry), který mi připadá nejakceptovatelnější, na čemž jsme se shodli i se spolupracovníky. Budu-li citovat klasika : Můžete s tím nesouhlasit, můžete proti tomu protestovat, ale to je také vše, co s tím můžete dělat. Strana 32 z 351

33 Kapitola 1: Zadání projektu 33 Akce Příkaz V předchozím textu vás možná zarazilo, že jsem střídavě hovořil o příkazech a o akcích. Dohodněme se, že termínem akce budu označovat druh činnosti, např. přesun z prostoru do prostoru nebo zvednutí h-objektu. Naproti termínem příkaz budu označovat konkrétní zadání, např. jdi koupelna nebo zvedni meč. Množství h-objektů, které se do batohu vejdou, je však omezené (to je jedna z omezujících podmínek hry). Akce pro přesun h-objektu z prostoru do batohu proto nesmí povolit překročení kapacity batohu. Navíc musí ve světě hry existovat h-objekty, které není možno zvednout a přemístit do batohu (okno, potok, studijní referentka, ). Akce pro zvednutí h-objektu proto nesmí umožnit uložit do batohu nepřenositelný h-objekt. Mezi h-objekty mohou (ale nemusejí) být i takové, s nimiž může hráč komunikovat a případně od nich získat informaci potřebnou ke zdárnému pokračování. Tyto h-objekty mohou být jak živé (trpaslík, kouzelný dědeček, ), tak neživé (robot, magnetofon, knížka, ). Mezi h-objekty v prostoru mohou (ale opět nemusejí) být i takové, které jsou za jistých okolností novým prostorem s jeho vlastními h-objekty. Takovýmto h-objektem-prostorem může být např. truhla, trezor, nalezený batoh apod. Tyto nestandardní prostory nemají žádné sousedy, a proto se do nich musíme přesunout nějakým nestandardním příkazem např. otevři truhla a zpět příkazem zavři truhla. Takovýmto prostorem může být např. i deska stolu. Do jejího prostoru se přesunete např. příkazem koukni_na_stůl. Pak uvidíte jenom h-objekty, které se na stole nacházejí, a některý z nich se vám může hodit. 1.2 Příklady her Na scénářích hry se pozná nápaditost studenta. Ti prostodušší volí hry na téma Probudil(a) jste se po těžkém flámu v neznámém bytě. Najděte svoje spodky a svršky a opusťte byt. Ti o trochu nápaditější vykrádají banky, utíkají z vězení. Další vytvářejí hry na motivy pohádek (např. O kohoutkovi a slepičce nebo Červená karkulka) nebo známých knížek a televizních seriálů (Star track, Harry Potter, Červený trpaslík, Pán času, ). Občas se objeví hry s kuriózním námětem. Jeden student naprogramoval hru Život, ve které se hráč na startu narodí a prostory jsou jednotlivé životní etapy (nemluvně, školka, školák, středoškolák, Strana 33 z 351

34 Kapitola 1: Zadání projektu 34 ). Cílem hry je úspěšně umřít a do té doby zanechat hojné a dobře zaopatřené potomstvo. Jedna studentka naprogramovala hru představující studium na vysoké škole. Prostory byly jednotlivé semestry a k tomu, aby se další semestr sousedem stal aktuálního, musel hráč-student splnit jisté úkoly nasbírat potřebné h-objekty, kterými zde byly absolvované zkoušky. Jeden student vozíčkář naprogramoval hru, jejímž cílem bylo dostat se ráno z domova do školy, přičemž se na cestě objevovaly různé náhodné překážky (nefungující výtah do Metra, autobus nepřizpůsobený přepravě vozíčkářů, atd.). Díky náhodně se vyskytujícím překážkám se mohlo stát, že občas hra nešla úspěšně dohrát a hráč ten den do školy nedorazil. Jak vidíte, námětů je nepřeberné množství a je jenom na vaší fantazii, abyste vymysleli takový, aby vás návrh příslušného programu také trochu bavil. 1.3 Shrnutí co jsme se dozvěděli V kapitole jste se seznámili s koncepcí hry, o jejímž návrhu si budeme povídat ve zbytku knihy, a s jejími základními omezujícími podmínkami: Jedná se o textovou hru, v níž hráč zadává textové příkazy a hra mu po jejich provedení textově oznamuje výsledek provedené akce. Hra probíhá ve světě tvořeném několika prostory, mezi nimiž se hráč může přemisťovat. V prostorech mohou být h-objekty, z nichž některé je možno zvednout a odnést a některé ne. Zvednuté h-objekty přemístí hráč do batohu, který má končenou kapacitu. Mezi h-objekty mohou být takové, které s hráčem konverzují. Některé h-objekty mohou vystupovat i jako samostatný prostor. Strana 34 z 351

35 Kapitola 2: Podpůrný framework Podpůrný framework Kapitola 2 Podpůrný framework Co se v kapitole naučíte V této kapitole si ukážeme, co bychom měli dělat před tím, než začneme programovat vlastní hru. Seznámím vás s frameworkem, který na jednu stranu nastavuje autorům programu mantinely, ale na druhou stranu jim současně pomáhá zadanou úlohu snadněji zvládnout. Poté začneme postupně vytvářet zárodek našeho budoucího projektu. 2.1 Koncepce frameworku Abych studentům umožnil se soustředit na vlastní návrh hry a nemuseli se rozptylovat některými pomocnými činnostmi a abych jim (a s nimi i sobě) umožnil co nejsnadněji kontrolovat dodržení všech požadavků, vytvořil jsem speciální framework, který obsahuje řadu datových typů zjednodušujících návrh programu i jeho následné testování a vyhodnocení korektnosti jeho návrhu. Během následujícího výkladu vás postupně seznámím s některými důležitými datovými typy, které potřebujete znát k tomu, abyste poznali a pochopili koncepci té části frameworku, kterou specifikuje požadavky na váš návrh hry, a následně pak další datové typy, které využijete k návrhu vlastní hry. Vymezení mantinelů Jedním z účelů frameworku je vymezit mantinely, v nichž se mají studenti se svými návrhy her pohybovat. Studenti totiž občas špatně odhadnou svoje schopnosti a navrhují hry, které jsou sice dokonalé, ale jejich naprogramování je mimo možnosti daného studenta. Strana 35 z 351

36 Kapitola 2: Podpůrný framework 36 Součástí frameworku jsou proto i testovací třídy, které studentovi pomohou ověřit, že splnil požadované okrajové podmínky (minimální počet kroků nutný k dohrání hry, minimální počet definovaných akcí, minimální počet prostorů, ), a na druhou stranu mohou do jisté míry i ověřit, zda není zadání daného studenta zbytečně komplikované. Doména pedu.eu a balíček eu.pedu Když jsem koupil doménu pecinovsky.cz, nabídl jsem všem svým jmenovcům, že ji se mnou mohou sdílet a umisťovat na ni svoje stránky. Tím jsem si ale výrazně zkomplikoval situaci pro případ, kdy bych s doménou potřeboval provádět nějaké složitější operace. Proto jsem posléze pořídil doménu eu.pedu (pedu je zkratka z anglického programming education), do níž se chystám postupně umisťovat všechny svoje materiály týkající se výuky programování, především pak doprovodné programy ke svým učebnicím. Vzpomenete-li si na konvence, podle kterých se v Javě definují názvy balíčků, hned si odvodíte, že všechny mé výukové programy budou v některém z podbalíčků balíčku eu.pedu. Tak tomu bude i v této učebnici. Předpřipravené datové typy a soubory Druhým účelem frameworku je pak nabídnout studentům předpřipravené datové typy (interfejsy a třídy), které budou při tvorbě svého projektu potřebovat. Tyto datové typy jsou v balíčku eu.pedu.adv15p_fw.empty_classes. Samozřejmě, že si každý může definovat tyto datové typy po svém, nicméně součástí předdefinovaných typů jsou definice atributů a metod, které při definici daného typu bude student beztak muset definovat. Usnadnění testů Nezanedbatelným efektem takovéhoto frameworku je také poskytnutí nástrojů pro usnadnění vývoje a současně pro usnadnění kontroly odevzdaných prací. Framework poskytuje studentům testovací nástroje využitelné při návrhu a následném ladění programu. Obdobně poskytuje učitelům nástroje pro usnadnění závěrečného vyhodnocení odevzdaných prací. Umožňuje jim za prvé odhalit všechny oblíbené studentské chyby a za druhé jim usnadňuje ověření, nakolik se student v odevzdaném programu vyzná a je jej schopen dále upravovat. Vlastní framework je uložen v balíčku eu.pedu.adv15p_fw a jeho podbalíčcích. Strana 36 z 351

37 Kapitola 2: Podpůrný framework Propojení frameworku s vyvíjeným programem Framework je definován jako samostatný projekt, u nějž se předpokládá, že jej ve svém vývojovém prostředí definujete jako knihovnu, kterou bude vámi navržený projekt hry využívat. Jak jsem řekl na počátku, většina studentů v mých kurzech používá vývojové prostředí NetBeans. To, jak v tomto vývojovém prostředí definovat framework jako knihovnu a jak pak tuto knihovnu importovat do jiného projektu, je podrobně rozebráno v první části publikace [7], a nebudu to zde proto rozebírat. Ti, kteří se rozhodli používat nějaké jiné profesionální vývojové prostředí (nejspíše Eclipse či IntelliJ IDEA), si musí zjistit potřebný postup sami. Některým studentům ale připadá profesionální vývojové prostředí příliš náročné a dávají přednost setrvání u začátečnického prostředí BlueJ. Takovíto uživatelé mají dvě možnosti: Nechtějí-li být obtěžování zdrojovými kódy frameworku, mohou ve složce projektu definovat podsložku nazvanou +libs (nezapomeňte na počáteční +, které zabezpečuje, že název není možno použít jako identifikátor), a do ní pak zkopírovat JAR-soubor frameworku. Druhou (a řekl bych výrazně oblíbenější) možností je sloučit zdrojové kódy frameworku se zdrojovými kódy vlastního programu a vytvořit z nich jeden větší projekt. Tuto možnost mohou ostatně využít i uživatelé profesionálních prostředí, kteří nechtějí definovat můj framework jako knihovnu (má to své výhody i nevýhody). Vzhledem k tomu, že studenti beztak odevzdávají pouze obsah svého balíčku, tak na tom, jak vypadá neodevzdávaný zbytek projektu, nijak nezáleží. 2.3 Umístění zdrojových kódů do balíčku Právě jsem prozradil, že studenti odevzdávají pouze obsah svého balíčku. (Dostanou k tomu dokonce speciální program, o němž se za chvíli zmíním.) Ve svém balíčku musejí mít jak zdrojové kódy svého projektu, tak veškerou doprovodnou dokumentaci (uživatelskou příručku, UML diagramy apod.). Obecně platí, že bychom uspořádání projektu do balíčků měli definovat jako první. Z něj se pak odvodí vše další. Pojďme se proto podívat, jaké uspořádání používám ve svých kroužcích a jaké budu používat v této knize. Strana 37 z 351

38 Kapitola 2: Podpůrný framework 38 Rodičovský balíček Aby se maximálně usnadnilo závěrečné testování, potřeboval jsem, aby bylo možno umístit všechny odevzdané zdrojové a pomocné soubory (obrázky dokumentace apod.) do jednoho projektu. Zavedl jsem proto konvence, které pro každou skupinu studentů specifikují její pseudokořenový 6 balíček, v němž pak budou umístěny podbalíčky jednotlivých studentů. Jste-li řadovým čtenářem této knihy, pak pomocné programy předpokládají, že vaším pseudokořenovým balíčkem bude balíček eu.pedu._49_ (číslo 49 nemá žádný tajný význam, je odvozeno z toho, že tato kniha je mojí 49. knihou). Ten bude i rodičovským balíčkem doprovodných programů této publikace. Jste-li studentem nějakého kurzu, který využívá náměty z této učebnice, bude váš pseudokořenový balíček definován vaším vyučujícím. Pro ně jsou určeny následující pasáže. Řadoví čtenáři mohou pokračovat od pasáže Balíček studenta na straně 39. Dvě konvence Vzhledem k tomu, že se v důsledku všelijakých reorganizací a dalších vlivů počet studentů v jednotlivých semestrech řádově liší (jednotky až stovky), zavedl jsem konvence dvě: první rozlišuje studenty podle školy, kterou navštěvují, druhá je podle školy nerozlišuje. V následujících pasážích uvedu postupně obě. Důvodem je hlavně to, že chci učitelům, které bude tento text inspirovat k návrhu podobného zadání pro jejich studenty, nabídnout pro inspiraci obě možnosti. Konvence respektující školu studenta V dřívějších dobách se obsah látky vyučované na jednotlivých školách lišil, a proto tato konvence respektuje školu studenta již v názvu balíčku: Rodičovským balíčkem je balíček školy, jehož název je odvozený z její internetové adresy, např. cz.vse, cz.cvut.fjfi apod. V balíčku školy je podbalíček specifikující aktuální semestr výuky a vyučovaný h-objekt. Například: Na VŠE se základy OOP vyučují v předmětu Programování v Javě označovaném zkratkou 4IT101. V podzimním semestru roku 2015 proto bude jejich rodičovským balíčkem balíček cz.vse.p2015_ Balíček označuji jako pseudokořenový, protože to sice není skutečný kořenový balíček, ale vystupuje jako společný rodičovský balíček všech balíčků studentů dané skupiny. Strana 38 z 351

39 Kapitola 2: Podpůrný framework 39 Na FJFI ČVUT se základy OOP vyučují v předmětu Úvod do objektové architektury označovaný zkratkou 18UOA. V jarním semestru roku 2016 proto bude jejich rodičovským balíčkem balíček cz.cvut.fjfi.j2016_uoa. Probíhá-li výuka ve více skupinách, jejichž práce je třeba rozdělit podle skupin, může balíček h-objektu ještě obsahovat podbalíčky jednotlivých skupin. Pro ně jsem zvolil označení odvozené z času, kdy začíná výuka dané skupiny. Studenti ze skupiny na VŠE, jejíž výuka začíná v úterý v 07:30, by tak na podzim roku 2015 umisťovali svoje balíčky do rodičovského balíčku cz.vse.p2015_101._2_0730. Jak vidíte, pro označení dnů používám jejich pořadová čísla, takže mám pak jednotlivé balíčky seřazeny ve stejném pořadí, v jakém probíhá výuka jednotlivých skupin. Protože ale žádný identifikátor (a tedy ani název balíčku) nesmí začínat číslicí, vkládám před číselné označení dne znak podtržení. Konvence nerespektující školu studenta Poslední dobou se obsah přednášené látky na jednotlivých školách prakticky sjednotil, a proto studenty již nerozlišuji podle škol, a definuji pro ně zjednodušenou konvenci: Společným rodičovským balíčkem je podbalíček mého kořenového balíčku specifikující semestr a můj vlastní identifikátor daného h-objektu. Pro úvodní kurzy objektového programování, které ve skutečnosti vnímám jako úvod do objektové architektury 7, používám zkratku uoa. Na podzim 2015 by proto byl společným kořenovým balíčkem balíček eu.pedu.uoa_p2015. Je-li vhodné dělit odevzdané práce podle skupin, přibyde podbalíček definovaný podle konvence uvedené v posledním bodu předchozí pasáže. Studenti ze skupiny, jejíž výuka začíná v úterý v 07:30, by tak nezávisle na své škole umisťovali na podzim roku 2015 svoje balíčky do rodičovského balíčku eu.pedu.uoa_p2015._2_0730. Balíček studenta Popisovaná hra je jedním z povinných projektů, které musejí studenti v průběhu semestru vytvořit. Když učitel dostane k posouzení sadu scénářů od svých studentů, musí se v nich nějak vyznat. Bylo by proto vhodné, aby se v názvu balíčku objevilo jméno daného studenta. Na velkých školách se občas najdou dva studenti se stejným jménem i příjmením. Proto je vhodné doplnit předchozí identifikaci ještě něčím, co nám jedno- 7 Viz seznámení s metodikou Architecture First v [5]. Strana 39 z 351

40 Kapitola 2: Podpůrný framework 40 značně identifikující příslušného studenta. Jako identifikační řetězec studenta (v dalším textu o něm budu hovořit jako o ID studenta) používám jeho přihlašovací jméno do školního informačního systému jeho login. Není důležité, jak ID studenta vypadá to ponecháme na konvencích dané školy. Pro nás je důležité, že daného studenta v rámci jeho školy jednoznačně identifikuje. Teoreticky by nám pro jednoznačnou identifikaci studenta stačilo jeho ID, jenomže to, v čem se snadno vyzná počítač, ještě nemusí stejně dobře vyhovovat lidské obsluze. Proto zavádím identifikátor balíčku složený z ID studenta následovaného znakem podtržení a příjmením daného studenta, tj. název vytvořený podle pravidla (podle konvencí Javy jsou v názvech balíčků všechna písmena malá): id_prijmeni Vzhledem k občasným problémům s diakritikou v názvu souborů a složek při přechodu mezi operačními systémy 8 je navíc požadováno, aby v příjmení byly použity pouze ASCII znaky tak, jak to naznačuje i výše uvedené pravidlo. Budu-li své ukázkové programy vytvářet jako fiktivní student s ID=RUP001, jehož výuka začíná v pondělí v 00:00, bude moje ukázkové řešení pro podzimní kurzy v roce 2015 umístěno v balíčku eu.pedu.uoa_p2015._1_0000.rup001_pecinovsky. Balíček řadového čtenáře Jste-li řadovým čtenářem této knihy a budete-li si chtít vyzkoušet odevzdávání programů v režimu, v jakém je odevzdávají moji studenti, vyberte si jako název svého balíčku jeden z identifikátorů student_a až student_z. Balíček doprovodných programů Datové typy vzorového řešení v doprovodných programech k této publikaci budou umístěny v balíčku eu.pedu.adventure15p a jeho podbalíčcích. Pro informaci dodám, že do balíčku eu.pedu od jisté doby umisťuji všechny své programy určené pro výuky a poslední tři znaky názvu balíčku adventure15p oznamují, že se jedná o verzi projektu určenou pro podzimní semestr roku Pro každý semestr totiž definici maličko upravuji. 8 Java s diakritikou problémy nemá, ale problémy občas nastávají při přenosu programů z některých systémů, tedy konkrétně ze systémů Apple. Windows a Linux na PC se spolu bez problémů shodnou. Firma Apple ale musí vše dělat po svém, takže se někdy její počítače s těmi ostatními dostatečně nekamarádí. Strana 40 z 351

41 Kapitola 2: Podpůrný framework Pojmenování projektů Protože budete odevzdávat vždy pouze složku svého balíčku, do níž se název projektu nepromítne, je pojmenování vašeho projektu zcela na vás. Protože však k této knize je téměř ke každé kapitole nějaký doprovodný projekt zobrazující stav, k němuž jsme se v průběhu kapitoly dostali, bylo třeba pro názvy doprovodných projektů zavést nějakou konvenci. Názvy ukázkových projektů, které budu postupně budovat v rámci dalšího výkladu, budou vždy začínat znakem A (hry tohoto typu bývají označovány jako adventury) následovaný trojmístným číslem, v němž bude první číslice označovat díl knihy a zbylé dvě kapitolu, k níž se projekt vztahuje. Za číslem bude malé písmeno označující, ke které části kapitoly se projekt vztahuje. Projekty ze závěru kapitoly zde budou mít vždy písmeno z. Bude-li mít kapitola více doprovodných projektů, budou tato písmena naznačovat jejich pořadí. Za touto základní identifikací projektu bude následovat znak podtržení a text stručně charakterizující obsah projektu. Doprovodný projekt k závěru této kapitoly se proto bude jmenovat A102z_Start. 2.5 Vytváříte svůj projekt Nyní dostanete svůj první úkol: Podle postupu postupně probíraného ve zbytku kapitoly vytvořte nový projekt, který bude základem vaší budoucí hry. Tomuto projektu bude v doprovodných programech odpovídat projekt A102z_Start. 3. Vytvořte nový prázdný projekt. Jak už jsem řekl, jeho název je zcela na vás. 4. Zabezpečte správné propojení tohoto projektu s frameworkem hry (viz podkapitolu 2.2 Propojení frameworku s vyvíjeným programem na straně 37). 5. Podle zadaných pravidel (jejich možnou podobu jsem naznačoval v pasáži Rodičovský balíček na straně 38) vytvořte v projektu balíček vaší skupiny. 6. V balíčku skupiny vytvořte svůj podbalíček nazvaných podle vašich konvencí (viz pasáž Balíček studenta na straně 39). Ve svém balíčku nyní vytvoříte složku, do níž později uložíte dokumentaci. Vzpomeňte si, jak jsme v pasáži Předpřipravené datové typy a soubory na straně 36 hovořili o balíčku empty_classes. Teď jej začneme využívat: Strana 41 z 351

42 Kapitola 2: Podpůrný framework Ve zdrojových kódech frameworku najděte balíček empty_classes a zkopírujte z něj do svého balíčku složku DATA. Nyní přijde zase trochu vysvětlování. V zadávání vašeho úkolu budu pokračovat na straně 45 v pasáži Definice vlastního autorského interfejsu. 2.6 Jméno a ID autora Autora projektu sice jednoznačně identifikuje název jeho balíčku, ale pro snadnější a lepší tvorbu výstupních sestav s výsledky hodnocení by bylo vhodné, aby klíčové objekty programu uměly na požádání prozradit jméno svého autora (tentokrát úplné jméno i s diakritikou) a jeho ID. Tento požadavek specifikuje interfejs eu.pedu.adv15p_fw.game_txt.iauthor deklarující metody: public final String getauthorname() public final String getauthorid() Požadavek na to, aby nějaký objekt uměl prozradit jméno a ID svého autora pak zadáváme prostřednictvím požadavku, aby mateřská třída daného objektu implementovala interfejs IAuthor. Implementaci tohoto interfejsu ale vyžadujeme u více tříd. Není moudré, aby každá z nich definovala tyto metody znovu. Problémy klasického řešení Možná by někoho napadlo, že by všechny tyto třídy mohly mít společného předka (říkejme mu autorská třída), do nějž bychom tyto dvě metody vytkly. Bohužel, ne vždy je možno zařídit, aby třída, jejíž instance musí umět prozradit svého autora, mohla deklarovat onu autorskou třídu jako svého předka. Některé třídy mají totiž za povinnost být potomkem předem definované třídy, a jak víte, v Javě (stejně jako ve většině nejpoužívanějších jazyků) nemůže mít třída více předků. Možnost prostého zdědění těchto metod od rodičovské třídy tedy odpadá. Řešení v Javě 8 Java 8 ale přišla s novinkou, která nám s řešením předchozího problému pomůže. Touto novinkou jsou implicitní definice metod v interfejsech. V současné době se proto ukazuje jako optimální vyřešit tento požadavek tak, že každý student definuje speciální interfejs, v něm definuje konstantní statické atributy specifikující jeho příjmení a ID a současně také implicitní verze oněch metod. Pak už jen stačí, Strana 42 z 351

43 Kapitola 2: Podpůrný framework 43 aby třídy, které mají být schopny prozradit jméno a ID svého autora, implementovaly tento interfejs a tím automaticky zdědily obě požadované metody. Abych vám to maximálně ulehčil, definoval jsem interfejs IAuthorPrototype a přidal jej do balíčku eu.pedu.adv15p_fw.empty_classes (jeho definici najdete ve výpisu 2.1). Nyní vám proto stačí zkopírovat tento interfejs do vlastního projektu, přepsat hodnoty jeho statických konstant na vaše jméno a ID a případně interfejs přejmenovat (v ukázkovém řešení se tento interfejs jmenuje IAuthorRUP). Protože tento interfejs je potomkem interfejsu IAuthor, tak stačí v hlavičce každé třídy, která má implementovat interfejs IAuthor, deklarovat místo toho implementace vašeho autorského interfejsu a vše máte vyřešeno. Výpis 2.1: Definice interfejsu IAuthorPrototype, tentokrát i s deklarací balíčku a s importy (ty budou v příštích výpisech chybět) 1 /* The file is saved in UTF-8 codepage. 2 * Check: «Stereotype», Section mark-, Copyright-, Alpha-α, Beta-β, Smile- 3 */ 4 package eu.pedu.adv15p_fw.empty_classes; 5 6 import eu.pedu.adv15p_fw.game_txt.iauthor; /******************************************************************************* 11 * Instance interfejsu {@code IAuthorPrototype} umějí na požádání vrátit 12 * jméno a identifikační řetězec autora/autorky své třídy; 13 * tyto hodnoty mají uloženy ve svých statických konstantách 14 * {@link #AUTHOR_NAME} a {@link #AUTHOR_ID}. 15 * 16 Rudolf PECINOVSKÝ */ 19 public interface IAuthorPrototype extends IAuthor 20 { 21 //== STATIC CONSTANTS ========================================================== /** Jméno autora/autorky ve formátu <b>příjmení Křestní</b>, 24 * tj. nejprve příjmení psané velkými písmeny a za ním křestní jméno, 25 * u nějž bude velké pouze první písmeno a ostatní písmena budou malá. 26 * Má-li autor programu více křestních jmen, může je uvést všechna. */ 27 String AUTHOR_NAME = "PECINOVSKÝ Rudolf"; /** Identifikační řetězec autora/autorky zapsaný VELKÝMI PÍSMENY. 30 * Tímto řetězcem bývá většinou login do informačního systému školy. */ 31 String AUTHOR_ID = "RUP001"; //== STATIC METHODS ============================================================ Strana 43 z 351

44 Kapitola 2: Podpůrný framework //############################################################################## 40 //== ABSTRACT GETTERS AND SETTERS ============================================== /*************************************************************************** 43 * Vrátí jméno autora/autorky programu ve formátu <b>příjmení Křestní</b>, 44 * tj. nejprve příjmení psané velkými písmeny a za ním křestní jméno, 45 * u nějž bude velké pouze první písmeno a ostatní písmena budou malá. 46 * Má-li autor programu více křestních jmen, může je uvést všechna. 47 * 48 Jméno autora/autorky programu ve tvaru PŘÍJMENÍ Křestní 49 */ 51 default public String getauthorname() 52 { 53 return AUTHOR_NAME; 54 } /*************************************************************************** 58 * Vrátí identifikační řetězec autora/autorky programu 59 * zapsaný VELKÝMI PÍSMENY. 60 * Tímto řetězcem bývá většinou login do informačního systému školy. 61 * 62 Identifikační řetězec autora/autorky programu 63 */ 65 default public String getauthorid() 66 { 67 return AUTHOR_ID; 68 } //== OTHER ABSTRACT METHODS ==================================================== 73 //== DEFAULT GETTERS AND SETTERS =============================================== 74 //== OTHER DEFAULT METHODS ===================================================== //############################################################################## 79 //== NESTED DATA TYPES ========================================================= 80 } Jak jste si mohli všimnout, v konstantách tohoto vzorového interfejsu je definované moje jméno a fiktivní ID. Řada studentů ale demonstruje roztržitost hodnou profesora matematiky a klidně používá poloprázdný interfejs převzatý z frameworku. Testy jsou ale na tuto možnost připraveny a ověřují, jestli student Strana 44 z 351

45 Kapitola 2: Podpůrný framework 45 opravdu daný interfejs upravil, anebo implementuje pouze jeho zkopírovanou verzi. Mnou podepsané programy se vůbec nehodnotí. Definice vlastního autorského interfejsu Pokračujeme v zadávání úkolu vytvoření vlastního zárodečného projektu, které začalo na straně 41 v podkapitole 2.5 Vytváříte svůj projekt. Váš projekt je prozatím prázdný. No, tak úplně prázdný zase není máte v něm vytvořen svůj balíček a v něm složku se zárodkem budoucí dokumentace. Prozatím v něm ale nejsou žádné zdrojové kódy. To nyní změníme. 8. Z balíčku empty_classes zkopírujte do svého balíčku interfejs IAuthorPrototype. 9. Interfejs nějak vhodně přejmenujte (v doprovodných programech se ten můj bude nazývat IAuthorRUP) a nahraďte v něm moje jméno a ID svým vlastním. To ale ještě není konec. Zadání úkolu vytvoření vašeho vlastního zárodečného projektu pokračuje na straně 48 v pasáži Definice vlastní tovární třídy. 2.7 Interfejs IGSMFactory V druhém dílu učebnice Úvod do objektové architektury jsme se naučili jednoduchou cestu, jak specifikovat způsoby získání nějakého objektu bez použití reflexe. Tou cestou je definice tovární třídy, jejíž instance budou schopny objekt na požádání vyrobit. V balíčku eu.pedu.adv15p_fw.game_txt je proto definován interfejs IGSMFactory (mnemotechnická pomůcka: GSM znamená Game + Scenario Manager), jehož instance dokáží poskytnout klíčové objekty dané aplikace, konkrétně: hru, správce scénářů dané hry, který bude klíčovým objektem při jejím testování (o scénářích a jejich správcích budeme hovořit v příštích několika kapitolách), objekt realizující uživatelské rozhraní dané hry. Musíme mít stále na paměti, že obdobnou úlohu bude řešit řada studentů, takže nebude existovat jedna jediná hra, jediný správce scénářů a jediné uživatelské rozhraní. Připravíme se proto hned na to, že bude existovat nějaký interfejs pojmenovaný např. IGame, který bude specifikovat, co má taková hra vlastně umět. A vedle něj i interfejsy IScenarioManager a IUI deklarující požadavky na zmíněného správce scénářů a uživatelské rozhraní. Prozatím stačí znát názvy těchto interfejsů, v dalším textu pak postupně navrhneme jejich definice. Teď vám jenom dopředu prozradím, že se v dalším textu Strana 45 z 351

46 Kapitola 2: Podpůrný framework 46 ukáže rozumné, definovat pro všechny scénáře společného abstraktního rodiče, kterého nazveme AScenarioManager. Zdrojový kód interfejsu s definicemi instančních metod si můžete prohlédnout ve výpisu 2.2. Výpis 2.2: Definice interfejsu IGSMFactory 1 /******************************************************************************* 2 * Instance interfejsu {@code IGSMFactory} představují tovární objekty, 3 * které jsou schopny na požádání dodat instance klíčových objektů aplikace, 4 * konkrétně aktuální hry, jejího správce scénářů a uživatelského rozhraní. 5 * Název interfejsu je odvozen z (Game + ScenarioManager Factory). 6 */ 7 public interface IGSMFactory extends IAuthor 8 { 9 //== STATIC CONSTANTS ========================================================== /** Text zakončující oznámení o tom, 12 * že daná třída ještě není plnohodnotně definována. */ 13 String NOT_DEF = " ještě není plnohodnotně (= testovatelně) definována"; //== STATIC METHODS ============================================================ //************************************************************************** 20 //Zde chybí jedna definice, o které budeme hovořit později //############################################################################## 25 //== ABSTRACT GETTERS AND SETTERS ============================================== 26 //== OTHER ABSTRACT METHODS ==================================================== 27 //== DEFAULT GETTERS AND SETTERS =============================================== /*************************************************************************** 30 * Vrátí odkaz na (jedinou) instanci správce scénářů. 31 * 32 Požadovaný odkaz 33 */ 34 public default AScenarioManager getscenariomanager() 35 { 36 throw new UnsupportedOperationException("Třída správce scénářů" + 37 NOT_DEF); 38 } /*************************************************************************** 42 * Vrátí odkaz na (jedinou) instanci textové verze hry; 43 * dokud ještě hra neexistuje, vyhazuje po zavolání výjimku 44 * {@link UnsupportedOperationException}. 45 * Strana 46 z 351

47 Kapitola 2: Podpůrný framework Požadovaný odkaz 47 UnsupportedOperationException 48 * Třída hry ještě není plnohodnotně (= testovatelně) definována 49 */ 50 public default IGame getgame() 51 { 52 throw new UnsupportedOperationException("Třída textové hry" + 53 NOT_DEF); 54 } /*************************************************************************** 58 * Vrátí odkaz na instanci třídy realizující uživatelské rozhraní 59 * textové verze hry; 60 * dokud tato třída neexistuje, vyhazuje po zavolání výjimku 61 * {@link UnsupportedOperationException}. 62 * 63 Požadovaný odkaz 64 UnsupportedOperationException 65 * Třída definující uživatelské rozhraní ještě není plnohodnotně 66 * (= testovatelně) definována 67 */ 68 public default IUI getui() 69 { 70 throw new UnsupportedOperationException("Třída uživatelského rozhraní" + 71 NOT_DEF); 72 } //== OTHER DEFAULT METHODS ===================================================== //############################################################################## 81 //== NESTED DATA TYPES ========================================================= 82 } Jistě jste si všimli komentáře na řádcích 18 a 19 upozorňujícího na absenci definice jedné statické metody. Tuto metodu a její použití si vysvětlíme v pasáži Jak získat instanci tovární třídy na straně 71. Ti pozornější si navíc jistě všimli, že všechny tři metody jsou definovány jako implicitní. Teď totiž nemáme ani hru, ani správce scénářů a už vůbec ne nějaké uživatelské rozhraní. Zavolání těchto metod zatím vyhodí výjimku. Tovární třída, která bude tento interfejs implementovat pak v okamžiku, kdy bude umět příslušný objekt získat, přebije příslušnou metodu vlastní verzí, která daný objekt vrátí. Strana 47 z 351

48 Kapitola 2: Podpůrný framework 48 Tovární třídu vytvoříte nejsnadněji tak, že zkopírujete její prázdnou verzi třídu eu.pedu.adv15p_fw.empty_classes.emptygsmfactory, kterou pak přejmenujete a vhodně upravíte. Tovární třída patří mezi ty, jejichž instance musejí umět prozradit jméno a ID svého autora (interfejs IGSMFactory je potomkem interfejsu IAuthor). Proto prázdná verze tovární třídy implementuje interfejs IAuthorPrototype: public class EmptyGSMFactory implements IGSMFactory, IAuthorPrototype Nesmíte zapomenout nahradit v hlavičce třídy tuto implementaci implementací svého ekvivalentu tohoto interfejsu, který bude jako autora uvádět vás. Jinak budou metody vracet špatné odkazy. Definice vlastní tovární třídy Pokračujeme v zadávání úkolu vytvoření vlastního zárodečného projektu, které jsme opustili v pasáži Definice vlastního autorského interfejsu na straně Ve zdrojových kódech frameworku najděte balíček empty_classes a do svého balíčku z něj zkopírujte třídu IGSMFactory. 11. Zkopírovanou třídu vhodně přejmenujte (v doprovodných programech se ta moje jmenuje RUPGSMFactory). 12. V hlavičce třídy nastavte implementovaný autorský interfejs na ten svůj. 2.8 Program Zipper Moji studenti mají navíc za úkol uložit soubory ze svého balíčku do archivu vytvořeného za pomoci programu Zipper. Tento program vytvoří archiv typu ZIP, avšak před vlastním zazipováním nejprve: Zkontroluje, zda studenti vytvořili svůj balíček správně, tj. zda jej správně pojmenovali a umístili. Ze složky balíčku a jejích podsložek vyzobe soubory, které je třeba odeslat, a ignoruje naopak soubory, které se posílat nemají. Zkontroluje dodržení některých dalších vyhlášených konvencí (např. to, že v odevzdaných programech a textových souborech nejsou řádky, jejichž délka přesahuje předem zadanou mez). Pokud program Zipper najde nějakou chybu (tj. nedodrží-li student některé povinné konvence), odmítne příslušný archiv vytvořit a oznámí studentovi, co je Strana 48 z 351

49 Kapitola 2: Podpůrný framework 49 třeba v jeho programu opravit. Studenti se tak vyvarují svých oblíbených chyb, za které by zbytečně ztratili body. Protože vytvoření archivu prostřednictvím programu Zipper je jistá forma kontroly správnosti odevzdávaného úkolu, přibude poslední bod: 13. Zabalte svůj balíček programem Zipper a takto vytvořený archiv odevzdejte. 2.9 Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Zopakujme si, co jsme v této kapitole dozvěděli a co jsme naprogramovali: Seznámili jsme se s frameworkem, pod nímž musejí běžet programy řešící zadanou úlohu. Vysvětlili jsme si význam tohoto frameworku pro studenty i učitele. Seznámili jsme se se dvěma způsoby propojení frameworku s vyvíjeným programem: Použít framework jako samostatnou knihovnu. Sloučit zdrojové kódy frameworku s kódy vyvíjeného programu. Probrali jsme zásady, podle se studenti řídí při specifikaci umístění a pojmenování balíčku s jejich programem. Seznámili jsme se s interfejsem eu.pedu.adv15p_fw.game_txt.iauthor, který musejí implementovat třídy, jejichž instance budou mít (mimo jiné) za úkol, prozradit jméno a ID svého autora. Ukázali jsme si, jak lze toto zadání efektivně řešit definicí vlastního interfejsu odvozeného od předpřipraveného interfejsu IAuthorPrototype v balíčku eu.pedu.adv15p_fw.empty_classes. Jak jste se již dozvěděli v podkapitole 2.4 Pojmenování projektů, ukázkovým projektem odpovídajícím zadání v předchozí podkapitole je projekt A102z_Start. To je také výchozí projekt pro následující demonstrace. Strana 49 z 351

50 Kapitola 3: Koncepce testů scénáře Koncepce testů scénáře Kapitola 3 Koncepce testů scénáře Co se v kapitole naučíte V této kapitole se soustředíme na přípravu testů, které budou prověřovat správnost vytvořených programů. Přitom budeme mít na mysli, že každý ze studentů ve skupině bude vyvíjet vlastní verzi hry, takže bychom měli testy připravit tak, aby co nejjednodušeji otestovaly kteroukoliv z vyvíjených her. V průběhu kapitoly se vám budu snažit ukázat, jak můžete takovéto řešení navrhnout. Řada čtenářů má komplex z toho, že oni by to tak vymyslet nedokázali. Nenechte se tím zviklat. Všechno chce jistou zkušenost. Až budete zkušenější, nebudou vám takovéto konstrukce dělat problémy a dost možná, že byste nakonec dokázali požadované řešení navrhnout lépe než já. Snažte se jenom sledovat tok mých úvah a porozumět tomu, proč jsem se rozhodl použít prezentované řešení. Kdybyste náhodou ztratili nit, nezoufejte přeskočte zbytek kapitoly a přejděte k podkapitole 4.10 Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali na straně 82, kde je souhrn toho, co by měl tvůrce hry znát. Časem se můžete k přeskočené analýze vrátit. 3.1 Jak testovat Program nestačí jenom napsat, ale každý program je potřeba také otestovat, aby autor včas odhalil případné chyby. V dřívějších dobách zaujímali pracovníci pověření testováním programů nejspodnější místa hodnotového žebříčku. Situace se od té doby výrazně změnila. Většina vývojářů pochopila, že správný návrh a rea- Strana 50 z 351

51 Kapitola 3: Koncepce testů scénáře 51 lizace testů může na jednu stranu výrazně zefektivnit proces vývoje a na druhou stranu dobře otestovaný, bezchybný program zvyšuje renomé týmu u zákazníků, což zvyšuje pravděpodobnost lukrativních zakázek v budoucnu. Dnes proto patří testeři k uznávaným členům vývojových týmů. Programování řízené testy V moderním programování se často používá metodika vývoje označovaná jako vývoj řízený testy, pro niž se používá zkratka TDD z anglického Test Driven Development. Tato metodika doporučuje, abychom vždy nejprve napsali testy, a teprve pak začali vyvíjet program. Při psaní programu se pak stačí soustředit pouze na to, aby výsledný program prošel napsanými testy. Výhodou takto koncipovaného vývoje je to, že včasně psané testy opravdu testují, co má program dělat. Testy psané na konci vývoje většinou testují, že program správně pracuje, ale někdy jim unikne, že správně dělá něco jiného, než co bylo původním zadáním. Kdysi jsem psal učebnici programování ve verších a tam jsem tuto skutečnost popisoval básničkou: Když se k cíli dostat chceme, bývá jedno, kudy jdeme. Dej však pozor, abys hnedle nezjistil, že cíl je vedle. Metodikou TDD se budu snažit řídit i v tomto dílu. Před tím, než se pustíme do programování zadaného úkolu, se proto vždy nejprve pokusíme navrhnout, jak bychom průběžně testovali, nakolik se nám zadaný úkol podařilo vyřešit. Totéž bychom měli udělat i s naší hrou. Vyvstává však otázka, jak takovou hru testovat. Jednotkové a integrační testy V mých učebnicích se většinou navrhuje sada testovacích metod pro třídy testovaného programu a jejich instance. Takovéto testy označujeme jako jednotkové testy, protože testují jednotlivé jednotky budoucího programu. U větších programů však s jednotkovými testy nevystačíme. U nich totiž potřebujeme vědět nejen to, že jednotlivé jednotky pracují správně, ale také to, že tyto jednotky správně vzájemně spolupracují. U rozsáhlejších programů proto po odladění jednotlivých jednotek přicházejí na řadu integrační testy, které mají za úkol otestovat integraci jednotlivých jednotek do programu a jejich vzájemnou spolupráci. Naše hra už bude natolik složitý program, že by si integrační testy zasloužil. Na druhou stranu ale zase nebude natolik složitá, abychom nemohli oba dva druhy testů sloučit do jednoho komple- Strana 51 z 351

52 Kapitola 3: Koncepce testů scénáře 52 tu, který otestuje celkové chování programu a přitom současně prověří i funkci jeho jednotlivých jednotek. Možnosti testování naší hry Znovu se tedy vracíme k otázce, jak naši hru testovat. Jak je v programování obvyklé, nabízí se několik možností: Připravit testovací metody, které budou hře zadávat jednotlivé příkazy a kontrolovat, jak na ně hra reaguje, tj. co po jejich zadání hráči odpoví a do jakého se dostane stavu. Nevýhodou takovéhoto testu je jeho značná pracnost před testováním každého příkazu bychom museli nejprve připravit výchozí situaci, pak zadat příkaz a poté zkontrolovat výsledný stav. Vznikne tak sada relativně nezávislých testů, při jejichž vytváření můžeme přehlédnout některé vzájemné vazby. Druhou možnosti je definovat jakýsi scénář, jenž by obsahoval zadání posloupnosti příkazů specifikující, jak by se hra mohla hrát. Ke scénáři bychom připravili nějakou univerzální testovací metodu, které bychom tento scénář požadovaného průchodu hrou zadali. Naše univerzální testovací metoda by pak hře postupně zadávala příkazy z tohoto scénáře a po každém příkazu zkontrolovala, zda na něj hra správně zareagovala. Pro tuto kontrolu však potřebujeme, aby krok takového scénáře obsahoval nejenom příkaz, který se má hře zadat, ale i jakýsi popis stavu, do kterého by měla hra po provedení zadaného příkazu přejít. Druhé řešení nám přiblíží test realitě. Takovýto scénář totiž převede původní verbální (slovní) zadání úlohy, kterému rozumí jenom člověk, na zadání, kterému je schopen porozumět i počítač. Počítač je mu navíc schopen nejenom porozumět, ale také s jeho pomocí otestovat, zda se výsledný program opravdu chová podle zadaných pravidel. Vytvořené scénáře budeme navíc moci použít nejenom k testování správného chodu hry, ale i k demonstraci toho, jak je možno hru hrát nebo naopak toho, jak se hra hrát nemá. Vydáme se proto touto cestou. V následujících pasážích se vám pokusím vysvětlit, jak byste mohli připravit základní sadu tříd, které by vám mohly proces testování maximálně usnadnit. Protože celá problematika je ale přece jenom maličko složitější, nebudu po vás chtít, abyste na konci uměli tyto třídy přímo konstruovat. Cílem tohoto výkladu je ukázat vám základní principy konstrukce takovéto sady. To vám usnadní tvorbu programu, který bude tuto sadu využívat. Strana 52 z 351

53 Kapitola 3: Koncepce testů scénáře Třída Scenario Asi bych měl prozradit, že jsem si termín scénář nevypůjčil od tvůrců filmů, ale že je to běžný termín používaný v softwarovém inženýrství. Scénáře představují popisy možných průchodů programem. Náš scénář se bude od těch klasických odlišovat v jedné věci: nebude popisovat průchod programem (tj. naší hrou) slovy, ale bude to objekt, který bude tento průchod specifikovat způsobem využitelným v testovacích programech, pomocí nichž budeme tento průchod simulovat a testovat. Ve frameworku je v balíčku scenario (to je samozřejmě zkrácený název balíčku, jehož úplný název je eu.pedu.adv15p_fw.scenario) definována pomocná třída Scenario. Jednotlivé scénáře budou jejími instancemi. Jak by tedy měla vypadat programová definice takového scénáře? Dopředu můžeme odhadnout, že scénářů by mohlo být více, a proto by bylo vhodné je nějak pojmenovat a tím pádem také definovat metodu String getname() která na požádání jméno toho svého scénáře prozradí. Scénář má reprezentovat možný průběh hry. Průběh hry můžeme rozložit na sadu kroků. V každém kroku zadáme hře nějaký příkaz, jímž ji přivedeme do nějakého dalšího stavu, který nám hra ve své odpovědi naznačí. Scénář by měl proto reprezentovat tuto sadu kroků a na požádání ji pro testovací či simulační účely poskytnout. Dohodneme-li se, že jednotlivé kroky budou uloženy jako instance třídy ScenarioStep, pak by scénář měl jako poskytovatele kroků nabídnout buď iterátor (implementoval by interfejs Iterable<ScenarioStep>), anebo datovod. Nejlepší by bylo, kdyby nabízel obojí. Měl by tedy definovat metody: Iterator<ScenarioStep> iterator() Stream <ScenarioStep> stream() 3.3 Kroky scénáře třída ScenarioStep Abychom mohli běh hry podle zadaného scénáře testovat, potřebovali bychom, aby scénář poskytl o každém kroku dostatek informací. My bychom pak tyto informace porovnali s informacemi, které získáme od běžící hry. Ve frameworku je v podbalíčku scenario definována třída ScenarioStep, jejíž instance obsahují informace o jednotlivých krocích scénáře. Krok scénáře musí obsahovat jak zadávanou akci, tak informace o stavu, do nějž má hra po provedení Strana 53 z 351

54 Kapitola 3: Koncepce testů scénáře 54 (případně odmítnutí) této akce přejít. Instance třídy ScenarioStep jsou přepravky 9, které obsahují všechny potřebné informace související s daným testovacím krokem. Měli bychom si tedy rozmyslet, co charakterizuje stav hry. Nejdůležitější informací v kroku scénáře je zadávaný příkaz. Reakci na něj budeme testovat a následující informace tak budou popisovat stav hry po vykonání tohoto příkazu, resp. po pokusu o jeho vykonání. První, co nás bude při testu reakce hry na zadaný příkaz nejspíše zajímat, je odpověď hry, tj. jakou zprávu hráči po provedení příkazu vypíše. Ze zadání víme, že hráč má v průběhu hry procházet různými prostory. Jednou z charakteristik by tedy měl být aktuální prostor, tj. prostor, v němž se bude hráč nacházet po provedení příslušného příkazu. Z aktuálního prostoru můžeme přejít do některých jiných prostorů a do některých odtud naopak přejít nemůžeme (chceme-li např. ze sklepa na dvůr, musíme nejprve do haly). Množina přímo dostupných sousedů aktuálního prostoru se může v průběhu hry měnit např. odemčením dveří se zpřístupní další místnost. Další charakteristikou by proto mohly být prostory, které s aktuálním prostorem bezprostředně sousedí a můžeme se do nich proto bez problémů přesunout. V prostorech se mohou nacházet nejrůznější h-objekty, přičemž tímto termínem budeme označovat nejenom neživé h-objekty (např. meč), ale mohou to být i živé h-objekty (např. vlk v pohádce o Karkulce) či jakékoliv jiné h-objekty, které se vám do vaší představy hodí. Některé z h-objektů si může hráč vzít na další cestu, s jinými může komunikovat, další mohou mít jiné vlastnosti. Aktuální množina h-objektů v aktuálním prostoru by tak mohla být další charakteristikou stavu (např. po zvednutí h-objektu musí tento h-objekt z prostoru zmizet a objevit se v batohu). Víme, že hráč si může některé h-objekty vzít s sebou do pomyslného batohu na další cestu. Množina h-objektů, které má hráč aktuálně v batohu, může výrazně ovlivnit další průběh hry (např. k tomu, aby hráč mohl zabít draka, musí mít v batohu meč). Dalším charakteristikou výsledného stavu by tedy měl být obsah batohu. Když budeme procházet scénářem krok za krokem, bylo by asi vhodné jednotlivé kroky číslovat, abychom případně věděli, kde zadání kroku hledat anebo naopak kolik kroků nám ještě zbývá do konce hry. Přidáme tedy ještě pořadí kroku scénáře. 9 O návrhovém vzoru Přepravka se můžete dočíst např. v [2], [3], [4], [6] nebo [1]. Strana 54 z 351

55 Kapitola 3: Koncepce testů scénáře 55 Jednoslovnost názvů Jak už jsem řekl, studenti občas špatně odhadnou svoje schopnosti a neuvědomují si, jak obtížné je pro začátečníka naprogramovat některé jimi navržené vlastnosti. Jednou z nich jsou i víceslovné názvy akcí, prostorů a h-objektů. Jedním z dalších omezení navrhovaných scénářů proto bude, že názvy musejí být jednoslovné, přesněji nesmějí obsahovat mezery. Chcete-li ve hře použít víceslovný název, vložte mezi jeho jednotlivá slova znak podtržen např. název_ze_čtyř_slov. Tím toto omezení obejdete a uděláte z něj název, který je formálně jednoslovný. 3.4 Typ kroků scénáře třída TypeOfStep Na výše uvedené charakteristiky byste jistě po chvíli přemýšlení přišli sami. Přidám ale ještě jeden atribut, který by vás asi napadlo zařadit až po chvíli. Jak jsme si řekli, kroky scénáře definujeme proto, abychom pak s jejich pomocí mohli testovat hru. Zkušenost však ukazuje, že začátečníci většinou testování (a s ním i tvorbu příslušných scénářů) podcení a napáchají ve scénářích řadu chyb. Je proto třeba co nejlépe zkontrolovat i tyto scénáře. K tomu bychom ale potřebovali vědět, jaká jsou pravidla a omezení dané akce. Víme například, že při pokládání h-objektu by měl tento h-objekt zmizet z batohu a naopak se objevit v aktuálním prostoru. Navíc by se při pokládání h-objektu neměl měnit aktuální prostor. Obdobně bychom našli podmínky i pro další akce. Navíc je třeba umožnit kontrolu toho, že scénář opravdu obsahuje podklady pro prověrku vykonávání všech akcí, které budou označeny za povinné, resp. aby pro prověrku reakcí programu na všechny zadané možné chybné příkazy uživatele (např. když uživatel požádá o přesun do neexistujícího prostoru). Zavedl jsem proto výčtový typ TypeOfStep (stále jsme v podbalíčku scenario), jehož hodnotami jsou jednotlivé možné typy kroků scénářů. V komentářích v jeho výpisu si můžete přečíst, jaké typy kroků se mohou ve scénářích vyskytovat. Z komentářů ve výpisu 3.1 si můžete odvodit, že atribut typeofstep slouží nejenom k tomu, abychom mohli zkontrolovat, zda je krok korektně napsaný, ale také k dalším kontrolám: zda scénář obsahuje všechny požadované typy kroků, tj. jestli otestuje všechny verze chybného zadání, na něž musí program umět reagovat, zda je ten který příkaz zadán se správným počtem parametrů, díky sloučení typů akcí do skupin lze u povinných akcí zkontrolovat: Strana 55 z 351

56 Kapitola 3: Koncepce testů scénáře 56 zda se pro stejnou akci používá vždy stejný název, zda stejný název není použit pro různé akce. Ve výpisu 3.1 se také můžete přesvědčit, že třída TypeOfStep není jenom obyčejný výčtový typ, ale že je vybavena ještě několika dalšími schopnostmi např. od ní můžeme získat množinu povinně testovaných řádných, resp. chybných příkazů, abychom si v testu mohli ověřit, že každý z nich byl v nějakém kroku scénáře otestován. Výpis 3.1: Výčtový typ TypeOfStep 1 /******************************************************************************* 2 * Instance třídy {@code TypeOfStep} představují 3 * možné typy kroků zadávaných ve scénáři. 4 * Znalost typu kroku umožňuje zkontrolovat správnost zadání dat 5 * v jednotlivých krocích scénáře. 6 */ 7 public enum TypeOfStep 8 { 9 //== VALUES OF THE ENUMERATION TYPE ============================================ /** Typ kroku neurčeného pro zařazení do nějakého scénáře, ale pouze pro 12 * doplnění chybového hlášení a pomocné akce.*/tsnot_set(6,-1, null), //Typy řádných kroků, které se musí všechny objevit v základním úspěšném scénáři 15 /** Startovací krok s prázdným názvem. */tsstart (0,-1, null), 16 /** Hráč se přesune do sousedního prostoru. */tsmove (0, 1, null), 17 /** Úspěšné "zvednutí" h-objektu v prostoru. */tspick_up (0, 1, null), 18 /** Úspěšné položení h-objektu z batohu. */tsput_down (0, 1, null), //Na následující typy kroků musí hra umět zareagovat => 21 // musí být otestovány v základním chybovém scénáři /** Příkaz okamžitě ukončující hru. */tsend (1, 0, null), 24 /** Nápověda. */tshelp (1, 0, null), //Problémy se správným zadáním příkazu 27 /** Startovací příkaz není prázdný řetězec. */tsnot_start(2,-1, null), 28 /** Nestartovací zadání prázdného řetězce. */tsempty (2,-1, tsstart), 29 /** Hra nezná zadanou akci. */tsunknown (2,-1, null), //Vyvolání některé ze základních akcí bez povinného parametru 32 /** Nebylo zadáno, kam se přesunout. */tsmove_wa (2, 0, tsmove), 33 /** Nebylo zadáno, co se má zvednout.*/tspick_up_wa (2, 0, tspick_up), 34 /** Nebylo zadáno, co se má položit. */tsput_down_wa (2, 0, tsput_down), //Problémy se změnou místnosti 37 /** Cílový prostor není sousedem aktuálního.*/tsbad_neighbor(2, 1, tsmove), //Problémy se zvednutim či položením h-objektu Strana 56 z 351

57 Kapitola 3: Koncepce testů scénáře /** Zadaný h-objekt v prostoru není. */tsbad_item (2, 1, tspick_up), 41 /** Zadaný h-objekt nelze zvednout. */tsunmovable (2, 1, tspick_up), 42 /** Další h-objekt se nevejde do batohu. */tsbag_full (2, 1, tspick_up), 43 /** Zadaný h-objekt v batohu není. */tsnot_in_bag (2, 1, tsput_down), //Nepovinné typy kroků 46 /** Bezparametrická akce, která nepatří mezi základní povinné akce, 47 * mění pouze nějaký vnitřní stav hry. */tsnon_standard0(3, 0, null), /** Jednoparametrická akce, která nepatří mezi základní povinné akce, 50 * mění pouze nějaký vnitřní stav hry. */tsnon_standard1(3, 1, null), /** Dvouparametrická akce, která nepatří mezi základní povinné akce, 53 * mění pouze nějaký vnitřní stav hry. */tsnon_standard2(3, 2, null), /** Tříparametrická akce, která nepatří mezi základní povinné akce, 56 * mění pouze nějaký vnitřní stav hry. */tsnon_standard3(3, 3, null), /** Zadaný krok nepopisuje klasickou akci, 59 * ale je součástí rozhovoru hráče s nějakou postavou či zařízením hry 60 * nebo nějaké obdobné činnosti. */tsdialog (4,-1, null), //Typy kroku nepoužitelných pro testování reakce hry na zadaný příkaz 63 /** Krok tohoto typu nebude možno použít pro test správné funkce hry, 64 * protože neobsahuje data o stavu po provedení příkazu. 65 * Krok je určen pouze pro předvedení funkce hry. 66 * Demonstrační kroky se používají např. při testování funkce 67 * uživatelského rozhraní. */tsdemo (5,-1, null), 68 ; //############################################################################## 73 //== CONSTANT CLASS ATTRIBUTES ================================================= /** Základní typy kroků povinně definovaných akcí. */ 76 public static final Set<TypeOfStep> BASIC_ACTIONS; /** Typy kroků, které musí být povinně obsaženy v základním scénáři. */ 79 public static final Set<TypeOfStep> REGULAR_COMMANDS; /** Typy kroků, jež musí být povinně obsaženy v základním chybovém scénáři. 82 * Sem patří nápověda + nestandardní (předčasné) ukončení hry + 83 * různé druhy chybně zadaných příkazů, které nesmí hru rozhodit. 84 * Na všechny tyto typy nestandardních příkazů musí hra umět správně 85 * reagovat, a základní chybový scénář slouží k testu těchto reakcí. */ 86 public static final Set<TypeOfStep> MISTAKE_COMMANDS; //== VARIABLE CLASS ATTRIBUTES ================================================= Strana 57 z 351

58 Kapitola 3: Koncepce testů scénáře //############################################################################## 95 //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== static { 98 Set<TypeOfStep> basic = EnumSet.noneOf(TypeOfStep.class); 99 Set<TypeOfStep> regular = EnumSet.noneOf(TypeOfStep.class); 100 Set<TypeOfStep> mistake = EnumSet.noneOf(TypeOfStep.class); 101 for (TypeOfStep steptype : TypeOfStep.values()) { 102 if (Subtype.stCORRECT.equals(stepType.SUBTYPE)) { 103 basic.add(steptype); 104 regular.add(steptype); 105 } 106 else if (Subtype.stHELPSTOP.equals(stepType.SUBTYPE)) { 107 basic.add(steptype); 108 mistake.add(steptype); 109 } 110 else if (Subtype.stMISTAKE.equals(stepType.SUBTYPE)) { 111 mistake.add(steptype); 112 } 113 } 114 //Definuji všechny množiny jako neměnné 115 REGULAR_COMMANDS = Collections.unmodifiableSet(regular); 116 MISTAKE_COMMANDS = Collections.unmodifiableSet(mistake); 117 BASIC_ACTIONS = Collections.unmodifiableSet(basic); 118 } //== CLASS GETTERS AND SETTERS ================================================= 123 //== OTHER NON-PRIVATE CLASS METHODS =========================================== 124 //== PRIVATE AND AUXILIARY CLASS METHODS ======================================= //############################################################################## 129 //== CONSTANT INSTANCE ATTRIBUTES ============================================== /** Podtyp testu. */ 132 public final Subtype SUBTYPE; /** Počet parametrů. */ 135 public final int PARAM_COUNT; /** Skupina ekvivalentních typů = typů aktivujících stejnou akci. */ 138 public final TypeOfStep GROUP; //== VARIABLE INSTANCE ATTRIBUTES ============================================== 142 Strana 58 z 351

59 Kapitola 3: Koncepce testů scénáře //############################################################################## 146 //== CONSTUCTORS AND FACTORY METHODS =========================================== /*************************************************************************** 149 * Definuje nový typ kroku a na základě hodnoty parametru 150 * mu přiřadí příslušný podtyp a požadovaný počet parametrů. 151 * 152 subtype Ordinální číslo podtypu (aby byl zápis kratší) 153 paramcount Požadovaný počet parametrů. <br> 154 * Záporné hodnoty mají speciální význam: <br> 155 * {@code -1} se používá pouze u prázdného příkazu<br> 156 * {@code -2} se používá u typů akcí, 157 * u nichž nemá smysl počet parametrů zkoumat 158 group Skupina ekvivalentních typů akcí 159 */ 160 private TypeOfStep(int subtype, int paramcount, TypeOfStep group) 161 { 162 this.subtype = Subtype.values()[subtype]; 163 this.param_count = paramcount; 164 this.group = (group == null)? this : group; 165 TypeOfStep basic; 166 } //== ABSTRACT METHODS ========================================================== 171 //== INSTANCE GETTERS AND SETTERS ============================================== /*************************************************************************** 174 * Vrátí podtyp příslušného kroku scénáře. 175 * 176 Podtyp daného kroku scénáře 177 */ 178 public Subtype getsubtype() 179 { 180 return SUBTYPE; 181 } //== OTHER NON-PRIVATE INSTANCE METHODS ======================================== 186 //== PRIVATE AND AUXILIARY INSTANCE METHODS ==================================== //############################################################################## 191 //== NESTED DATA TYPES ========================================================= /*************************************************************************** 194 * Podtypy testovacích kroků jsou roztříděny do několika skupin; Strana 59 z 351

60 Kapitola 3: Koncepce testů scénáře * výčtový typ {@code Subtype} tyto skupiny definuje. 196 */ 197 public enum Subtype 198 { 199 /** Správně zadaný příkaz povinně zařazený do HAPPY. 0 */ stcorrect, 200 /** Správně zadaný povinný příkaz HELP či STOP. 1 */ sthelpstop, 201 /** Povinně testovaný chybně zadaný příkaz. 2 */ stmistake, 202 /** Nepovinný příkaz. 3 */ stnonstandard, 203 /** Součást rozhovoru. 4 */ stdialog, 204 /** Demonstrační krok bez doprovodných informací, 205 * který proto nelze použít k testu funkce hry. 5 */ stdemo, 206 /** Krok vytvořený pro pomocné práce. 6 */ stundefined; 207 } } 3.5 Třída ScenarioStep podrobněji Víme již, jaké mohou být typy kroků, tak nám již nic nebrání zanalyzovat v podbalíčku scenario třídu ScenarioStep. Musíte mi ale odpustit, že ji tu pro úsporu místa neuvedu celou (není to jenom prostoduchá přepravka), ale uvedu jen její atributy vycházející z našich předchozích úvah najdete je ve výpisu 3.2. Kompletní výpis třídy si můžete prohlédnout v doprovodných programech. Třída DBG_Logger Budete-li procházet zdrojový kód třídy ScenarioStep, najdete v něm atribut DBG typu DBG_Logger, který je potomkem třídy Logger. Instance této třídy a jejích potomků pomáhají programátorům ovlivňovat kdy a kam se budou zapisovat některé pomocné zprávy. Podrobněji se k této otázce vrátíme v některé z pozdějších lekcí. Výpis 3.2: Výchozí podoba třídy ScenarioStep 1 /******************************************************************************* 2 * Třída {@code ScenarioStep} slouží jako přepravka k uchovávání informaci 3 * o jednotlivých zadávaných příkazech scénáře 4 * a o očekávaných stavech programu po jejich provedení. 5 * <p> 6 * Kroky scénáře obsahují informace sloužící k ověření 7 * správné reakce hry na příkaz zadávaný v příslušném kroku scénáře. 8 */ 9 public class ScenarioStep 10 { 11 //XXXXX Vynechané atributy a metody třídy 12 Strana 60 z 351

61 Kapitola 3: Koncepce testů scénáře //== CONSTANT INSTANCE ATTRIBUTES ============================================== /** Index daného kroku scénáře - typicky jeho pořadí v rámci scénáře. */ 16 public final int index; /** Typ daného kroku scénáře. */ 19 public final TypeOfStep typeofstep; /** Zadávaný příkaz. */ 22 public final String command; /** Zpráva vydaná v reakci na zadaný příkaz; 25 * obsahuje-li {@code null}, nepočítá se s ověřováním reakce hry. */ 26 public final String message; /** Prostor, v němž se bude hráč nacházet po vykonaní příkazu. */ 29 public final String area; /** Názvy prostorů, které bezprostředně sousedí s prostorem, 32 * v němž se bude hráč nacházet po vykonaní příkazu, 33 * a které jsou proto z tohoto prostoru přímo dostupné. */ 34 public final String[] neighbors; /** Názvy h-objektů aktuálně se nacházejících v prostoru, 37 * v němž se bude hráč nacházet po vykonaní příkazu. */ 38 public final String[] items; /** Názvy h-objektů v batohu po vykonání příkazu. */ 41 public final String[] bag; /** Příznak toho, že daný krok je posledním krokem scénáře. */ 44 public final boolean thelast; //XXXXX Další atributy a metody instancí vynechány 48 } Na konci výpisu 3.2 se nachází deklarace atributu thelast, o němž jsme ještě nehovořili. Jak dokumentační komentář napovídá, zadání tohoto příkazu by mělo vést k tomu, že daný krok je posledním krokem běhu hry a po jeho provedení se hra vypíná. Slouží k tomu, aby mohl testovací program snadno zjistit, že daný krok je posledním krokem hry a že je třeba po jeho provedení otestovat, zda je hra doopravdy vypnutá. Strana 61 z 351

62 Kapitola 3: Koncepce testů scénáře Typ scénáře třída TypeOfScenario Softwarové inženýrství učí, že scénářů může být více. Zvláštní místo mezi nimi zaujímá ten, který popisuje základní bezproblémový průchod programem. Autoři učebnic jej většinou označují jako základní nebo hlavní úspěšný scénář. Ostatní scénáře jsou pak většinou označovány jako alternativní a popisují průchod programem, při němž není všechno optimální. Obdobně bychom měli postupovat i my. Potřebujeme scénář, který ukáže, jak projít co nejjednodušeji hrou. Vedle toho bychom měli mít scénáře, které budou definovat požadované chování hry v situacích, když hráč nepostupuje optimálně anebo dokonce když zadává špatné instrukce. Bylo by však vhodné, abychom mohli u každého scénáře zjistit jeho primární účel. Ve frameworku je proto v podbalíčku scenario definován výčtový typ TypeOfScenario, jehož instance reprezentují jednotlivé možné typy scénářů. Zestručněnou definici tohoto výčtového typu (chybí podrobnější dokumentační komentáře) najdete ve výpisu 3.3. (Jak si můžete ve výpisu všimnout, úspěšný scénář označuji ve frameworku jako šťastný (happy), protože je to scénář popisující stav, kdy se vše daří.) Výpis 3.3: Definice třídy TypeOfScenario 1 /******************************************************************************* 2 * Instance výčtového typu {@code TypeOfScenario} představují možné typy 3 * scénářů hry. 4 */ 5 public enum TypeOfScenario 6 { 7 //== VALUES OF THE ENUMERATION TYPE ============================================ 8 9 /** Scénář procházející možnou cestu vedoucí k dosažení cíle 10 * a obsahující současně informace pro otestování reakcí hry 11 * na zadávané příkazy. */ schappy, /** Scénář sloužící k otestování reakcí hry 14 * na chybně zadané příkazy 15 */ scmistakes, /** Scénář, podle nějž je možno testovat chod hry, 18 * ale který nepatří do žádného z výše uvedených dvou povinných typů. 19 * Tento scénář musí být testovatelný, 20 * a proto nesmí obsahovat žádný demonstrační krok. 21 */ scgeneral, /** Scénář sloužící pouze k demonstraci možné cesty 24 * a neumožňující testování chodu hry. 25 */ scdemo, 26 } Strana 62 z 351

63 Kapitola 3: Koncepce testů scénáře Simulace průběhu hry Když už si nějaké scénáře definujeme, bylo by příjemné mít možnost vyzkoušet si, jak by se podle nich hrálo. Nemyslím teď skutečnou hru, na to bychom potřebovali mít definovanou naši hru, a její definice je ještě daleko před námi. Stačilo by ale, kdybychom mohli spustit simulaci takovéhoto hraní, která by příslušné vstupy a výstupy někam vytiskla a my bychom pak mohli porozmýšlet, jak dobře / snadno / zábavně / by se taková hra mohla hrát. Přidáme proto požadavek, abychom mohli scénář požádat o předvedení simulace přehrání hry podle jeho kroků. Instance třídy Scenaroi proto budou nabízet metodu void simulate() která na standardní výstup vytiskne posloupnost příkazů hráče a příslušných odpovědí hry definovanou v daném scénáři. Demonstrační scénáře samozřejmě přehrát nepůjdou, protože nemají ve svých krocích uloženu předpokládanou odpověď hry, ale u těch testovatelných by to neměl být problém. 3.8 Minimální požadavky na hru a její scénáře Projekt vytvoření jednoduché textové konverzační hry, o kterém se tu průběžně bavíme, má sloužit k tomu, aby na něm studenti prokázali svoji schopnost vytvořit netriviální objektový program, s řadou navzájem závislých tříd vyhovující předem zadanému rámci. Aby byl program opravdu netriviální, je vhodné jeho složitost zespodu omezit nějakými minimálními požadavky. Tyto požadavky jsou uloženy jako atributy přepravky, která je instancí třídy Limits v balíčku scenarios. V nich jsou uchovávány následující požadavky: Minimální počet kroků základního spěšného scénáře. Minimální počet prostorů, kterými hráč musí cestou k cíli projít. Minimální celkový počet prostorů hry, přesněji prostorů, které hráč při procházení podle základního úspěšného scénáře navštíví nebo zahlédne, přičemž za zahlédnutý prostor je považován takový, který hráč buď přímo navštíví, anebo je bezprostředním sousedem (tj. hráč by se do něj mohl v případě zájmu jednoduše přesunout) některého z prostorů, které hráč navštívil. Minimální počet nestandardních, nově definovaných akcí, což jsou akce, které musí student definovat vedle těch povinných, kterými jsou akce pro odstartování hry, pro přesun do sousedního prostoru, pro zvednutí h-objektu a Strana 63 z 351

64 Kapitola 3: Koncepce testů scénáře 64 jeho přesun do batohu, pro položení h-objektu (přemístění h-objektu z batohu do aktuálního prostoru), žádost o nápovědu a předčasné ukončení hry. 3.9 Diagram tříd balíčku scenario V předchozím textu jsme se seznámili s několika třídami, které zprostředkovávají specifikaci požadavků na scénáře budoucí hry. Všechny uvedené třídy jsou definovány v balíčku scenario (přesněji eu.pedu.adv15p_fw.scenario). Jednoduchý diagram tříd tohoto balíčku si můžete prohlédnout na obrázku 3.1. Obrázek 3.1 Jednoduchý diagram tříd balíčku scenario Jak jste si jistě všimli, v balíčku je také třída AScenarioManager, i níž jsme ještě nehovořili. K té se probojujeme v následující kapitole Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Zopakujme si, co jsme v této kapitole dozvěděli a co jsme naprogramovali. V této kapitole jsme rozebírali, co by měl umět framework, který budou naše programy využívat. Jistě tušíte, že framework již má požadované vlastnosti. Pojďme si tedy zopakovat, co o něm víme: Vývoj hry povedeme podle metodiky TDD (Test Driven Development), která doporučuje nejprve napsat testy a teprve pak začít psát vlastní program. Strana 64 z 351

65 Kapitola 3: Koncepce testů scénáře 65 Pro naše testování si definujeme scénáře, které budou charakterizovat konkrétní průběh hry. Všechny datové typy, které probíráme v této kapitole, se nacházejí v balíčku eu.pedu.adv15p_fw.scenario. Scénáře jsou instancemi třídy Scenario. Scénáře jsou jednoznačně určené svým názvem. Scénáře jsou v podstatě přepravkami uchovávajícími posloupnost kroků. Krok scénáře je instancí třídy ScenarioStep. Scénář definuje metody: String getname() Iterator<ScenarioStep> iterator() Stream <ScenarioStep> stream() První vrací název daného scénáře, zbylé dvě slouží k procházení jeho jednotlivých kroků. Krok scénáře je typická přepravka (atributy jsou veřejné konstanty) s informacemi o stavu, do nějž se hra dostane po zadání konkrétního příkazu. Má tyto atributy: int index; //Pořadí kroku v rámci daného scénáře TypeOfStep typeofstep; //Typ daného kroku scénáře String command; //Příkaz realizující tento krok scénáře String message; //Zpráva vypsaná po zadání příkazu String area; //Prostor, v němž skončí hráč po zadání příkazu String[] neighbors; //Sousedé aktuálního prostoru (= východy) String[] items; //H-objekty vyskytující se v daném prostoru String[] bag; //Aktuální obsah batohu boolean thelast; //Informace, zda je daný krok posledním krokem scénáře Každý krok scénáře má přiřazen svůj typ instanci výčtového typu TypeOfStep. Typ scénáře specifikuje počet parametrů daného kroku a některé jeho další charakteristiky. Svůj typ má i celý scénář. Tento typ je instancí výčtového typu TypeOfScenario. Hodnotami výčtového typu TypeOfScenario jsou: schappy Základní úspěšný scénář popisující úspěšné odehrání hry. scmistakes Základní chybový scénář popisující reakce na chybně zadané příkazy. scgeneral Obecný testovací scénář, podle nějž je možno hru zahrát a otestovat její chování oproti chování naplánovanému ve scénáři. Strana 65 z 351

66 Kapitola 3: Koncepce testů scénáře 66 scdem Scénář sloužící pouze k demonstraci chování hry. Podle něj je možné hru zahrát, ale neposkytuje podklady k jejímu otestování. Strana 66 z 351

67 Kapitola 4: Správce scénářů Správce scénářů Kapitola 4 Správce scénářů Co se v kapitole naučíte V této kapitole si ukážeme, jak je možno definovat správce scénářů, o nichž jsme hovořili v kapitole minulé. Ten bude mít na starosti vytvoření příslušných h-objektů i usnadnění jejich použití při testech. 4.1 Interfejs IScenarioManager V minulé kapitole jsme se seznámili s koncepcí scénářů definujících možné průběhy hrou. Hned jsme si také řekli, že takovýchto budeme potřebovat více, a definovali jsme si základní charakteristiky prvních dvou z nich: základního úspěšného a základního chybového scénáře. Při našich návrzích bychom však neměli zapomínat na jedno ze základních pravidel programování: Kdykoliv začneme uvažovat o tom, že bychom mohli mít v programu více objektů stejného typu, měli bychom se také zamyslet nad tím, jestli by nebylo vhodné definovat nějakého správce, který by měl všechny tyto objekty na starosti. Tento správce by mohl na požádání poskytnout požadovaný objekt (v našem případě scénář) a současně by nám poskytoval informace, které jsou všem spravovaným objektům společné. Stejně bychom měli postupovat i v případu scénářů. Požadavky na správce scénářů definujeme v interfejsu IScenarioManager, který bude spolu s ostatními interfejsy definujícími požadavky na odevzdaný program v balíčku eu.pedu.adv15p_fw.game_txt. Pojďme si tedy ujasnit, co by vlastně měl takový správce scénářů umět a jaké by měl mít vlastnosti. Strana 67 z 351

68 Kapitola 4: Správce scénářů Správce by měl být jedináček Jedním z těchto požadavků je, aby správce scénářů byl definován jako jedináček. Je to logické: máme-li jako jedináčka definovat hru, měl by být obdobně definován i její správce. Tak alespoň zabezpečíme, že budou všechny scénáře dané hry spravované na jednom místě a při jejich správě nebude vznikat zmatek. Z předchozího plyne, že třída správce scénářů by měla definovat veřejnou statickou tovární metodu nazveme ji podle konvence getinstance. Bohužel, v těle interfejsu můžeme definovat pouze takové požadavky, které překladač dokáže zkontrolovat. Se zbylými jsme odkázáni na kontrakt popisovaný v dokumentačním komentáři. 4.3 Spravované scénáře Správce by měl především umožňovat, abychom si od něj mohli kdykoliv vyžádat kterýkoliv ze spravovaných scénářů. Otázkou však zůstává, jak správce pozná, který ze scénářů chceme. Můžeme se opět vybrat z několika možností. Nejjednodušší z nich budou dvě: Očíslujeme jednotlivé scénáře (např. podle pořadí, v němž jsme je zadávali) a budeme pak po správci chtít scénář se zadaným indexem. Scénáře pojmenujeme a budeme pak zadávat jméno požadovaného scénáře. Těžko říct, který z uvedených přístupů je lepší každý má své výhody a nevýhody. Staří programátoři pamatující období, kdy se více dbalo na efektivitu programu než na jeho přehlednost by asi zvolili ten první. Ti, kteří respektují současné zásady prosazující maximální přehlednost a srozumitelnost programu dají předost tomu druhému. Jako moderní programátoři bychom měli patřit do té druhé skupiny, a rozhodneme se pro obecnější označování scénářů jmény. Správce scénářů by měl proto poskytovat metodu: Scenario getscenario(string name) V podkapitole 3.6 Typ scénáře třída TypeOfScenario na straně 62 jsme si říkali, že mezi našimi scénáři by měly být dva scénáře, které budou mít výjimečné postavení: Prvním je Základní úspěšný scénář popisující průběh hry, kdy vše probíhá optimálně. Identifikátory atributů a proměnných, které s ním souvisí, začínají slovem happy, protože tento scénář patří k těm šťastným, jimž vše vychází. Strana 68 z 351

69 Kapitola 4: Správce scénářů 69 Druhým je Základní chybový scénář popisující reakce hry na různé druhy chybných zadání. Identifikátory s ním souvisejících atributů a proměnných začínají slovem mistake. Dohodněme se, že každý správce scénářů musí spravovat minimálně tyto dva scénáře základní úspěšný scénář bude pojmenovaný _HAPPY_ a základní chybový _MISTAKE_ (podtržítka jsem na začátek a konce vložil proto, abych tyto názvy odlišil od ostatních názvů scénářů). Pak můžeme tyto názvy definovat v interfejsu IScenarioManager jako konstanty a vzápětí v něm definovat implicitní metody: default Scenario gethappyscenario() { /* tělo metody*/ } default Scenario getmistakescenario() { /* tělo metody*/ } Základní úspěšný a základní chybový scénář jsou sice důležité, avšak neměli bychom zapomínat ani na ostatní. Práci s nimi by měla usnadnit metoda Collection<String> getallscenarionames(); která vrátí kolekci názvů všech scénářů, jež má daný správce ve správě, abychom si z nich mohli vybrat, který scénář budeme chtít spustit. Protože lze očekávat, že budeme chtít občas pracovat nejenom s nějakými vybranými scénáři, ale budeme je chtít postupně použít všechny, bylo by vhodné, aby byly instance správce iterovatelné a streamovatelné, tj. použitelné jako zdroje dat pro datovod 10. Správce scénářů proto doplníme metodami: Iterator<Scenario> iterator(); Stream<Scenario> stream(); Bude-li správce scénářů (označím jej jako scenariomanager) iterovatelný, tj. bude-li implementovat interfejs Iterable<Scenario> (a bude tedy implementovat metodu iterator()) budeme moci vytvářet cykly typu: for (Scenario scenario : scenariomanager) { //Definice zpracování aktuálního scénáře } Bude-li správce scénářů vybaven metodou pro vytvoření datovodu, budeme moci používat modernější způsoby zpracování typu scenariomanager.stream().foreach(/*akce provedená s každým scénářem*/) doplněné o případné filtry, mapy a další metody pracující s datovody. 10 O datovodech (anglicky stream) se můžete dočíst např. v [7]. Strana 69 z 351

70 Kapitola 4: Správce scénářů Jak získat hru k testování Zatím to bylo jednoduché (alespoň doufám). Teď se nám to trochu zkomplikuje, protože vstoupíme na doposud neprobádanou půdu. Kdyby na vás byla látka příliš obtížná, můžete tuto pasáž přeskočit a vrátit se k ní později. Scénář obsahuje informace o tom, jak lze hrát nějakou konkrétní hru. Říkali jsme si, že každý student bude vyvíjet vlastní hru. Různé scénáře mohou patřit různým hrám. Každý správce scénářů by měl spravovat scénáře vztahující se k jedné konkrétní hře. Bylo by proto vhodné, abychom mohli získat odkaz na hru, kterou si podle jeho scénářů můžeme zahrát, resp. kterou můžeme pomocí těchto scénářů otestovat. Jednou z možností je vložit nějakým způsobem tento odkaz do třídy správce scénářů. S tím ale vzniká drobný problém, protože když připravujeme scénáře budoucí hry, tak ještě žádná hra neexistuje. Museli bychom proto ve chvíli, kdy začneme danou hru vyvíjet, upravit zdrojový kód správce jejích scénářů. Můžeme ale k problému přistoupit i jinak. V kapitole 2.7 Interfejs IGSMFactory na straně 45 jsme se seznámili s tovární třídou, jejíž instance umějí poskytnout všechny klíčové objekty aplikace, mezi nimi i instanci správce scénářů a instanci hry (samozřejmě až od okamžiku, kdy začnou existovat). Tovární třída přitom vznikne ještě před třídou správce scénářů pokud jste pracovali podle pokynů v podkapitole 2.5 Vytváříte svůj projekt na straně 41, tak byste již měli mít vytvořen projekt, jehož součástí je, mimo jiné, i příslušná tovární třída, byť zatím ještě žádné instance vyrábět neumí. Kdyby správce scénářů dokázal poskytnout odkaz na svůj tovární objekt, mohli bychom se na testovanou hru zeptat tohoto továrního objektu. Jak získat objekt třídy Když začneme přemýšlet nad tím, jak vrácení továrního objektu zařídit, zjistíme, že to není zcela jednoduché. Správce scénářů není sdružen s nějakou konkrétní tovární instancí, ale s celou tovární třídou. Na druhou stranu to mám jednoduché v tom, že libovolná instance této tovární třídy na požádání vrátí (přesněji řečeno měla by vrátit) tu samou instanci scénáře, resp. hry. Obě tyto třídy by totiž měly být definovány podle návrhového vzoru Jedináček (Singleton), takže bude jedno, jakou instanci sdružené tovární třídy o danou hru požádáme. Když totiž řekneme, že scénář patří ke hře Xyz, říkáme tím, že scénářem můžeme testovat všechny hry, které jsou instancemi třídy Xyz. Asi by proto bylo výhodnější, kdyby nám správce nějakým způsobem sdělil, se kterou třídou se jeho scénáře druží, abychom ji pak mohli v případě potřeby požádat o její instanci konkrétní hru. Strana 70 z 351

71 Kapitola 4: Správce scénářů 71 Má to ale jeden háček: doposud jsme pracovali pouze s objekty, které jsou instancemi nějaké třídy. Pokud jsme někdy potřebovali oslovit přímo třídu, zapsali jsme do programu její jméno. Zatím jsme se nenaučili, jak uložit třídu do proměnné. Protože jste ale již pokročilejší, řekl bych, že je nejvyšší čas to změnit. Jak jistě víte z obecné teorie objektově orientovaného programování (a jak si můžete připomenout v příloze A Používané termíny na straně 341), existují jazyky, ve kterých je třída normální objekt. Takovým jazykem je např. Smalltalk jazyk, jehož autoři položili základ celému celé OO paradigmatu. Jak je vysvětleno např. v [4], Java mezi takovéto jazyky nepatří. V Javě není třída přímo dostupná jako objekt. Na objekt třídy se musíme obracet přes její class-objekt, což je instance třídy Class 11. Připomínám, že class-objekt vystupuje v programu jako reprezentant daného datového typu. Reprezentant, který zprostředkovává přístup k informacím o jím reprezentovaném typu, resp. požadavky na tento typ. Každému datovému typu (tj. nejen třídě, ale i výčtovému typu, interfejsu a každému primitivnímu typu) je přiřazen právě jeden class-objekt. Class-objekt je běžný objekt, který můžeme předávat jako parametr, ukládat do proměnných a vracet jako funkční hodnotu. S jeho pomocí můžeme o jím zastupovaném datovém typu získat řadu zajímavých informací a možností např. přes něj můžeme požádat třídu o vytvoření její instance. Po správci scénářů proto budeme chtít, aby znal class-objekt své tovární třídy, jejímiž instancemi jsou tovární objekty, které umějí vytvořit jak daného správce, tak s ním sdruženou hru. Budeme proto po něm chtít, aby definoval metodu Class<? extends IGSMFactory> getfactoryclass() jež vrátí class-objekt, který můžeme požádat o instanci dané tovární třídy a tu pak o instanci sdružené hry. Jak získat instanci tovární třídy Řekl jsem, že metoda vrátí class-objekt, pomocí nějž můžeme požádat třídu o vytvoření její instance. K tomu slouží metoda newinstance(), jež vytvoří novou instanci daným class-objektem zastupované třídy pomocí jejího bezparametrického konstruktoru. Na této metodě je sympatické to, že s její pomocí můžeme naprogramovat zavolání bezparametrického konstruktoru nějaké třídy, aniž bychom dopředu (tj. při psaní programu) věděli, jaké třídy. Musí mít jenom dostupný bezparametrický konstruktor (nesmí být např. soukromý). 11 Hovořil jsem o tom v pasáži Class-objekt datového typu Xxx na straně 341. Strana 71 z 351

72 Kapitola 4: Správce scénářů 72 Většina operací s class-objektem patří do tzv. reflexe (mohli bychom použít také starší český termín sebezpyt), což je proces, při němž program analyzuje svoji vlastní strukturu. Na základě zjištěných skutečností pak může program přijmout rozhodnutí o dalším postupu. Pomocí reflexe můžeme elegantně řešit řadu úloh, které by byly jinak řešitelné jen velmi obtížně. Na volání metody newinstance() je nepříjemné to, že může vyvolat řadu kontrolovaných přerušení, která je třeba ošetřit. Interfejs IGSMFactory naštěstí s takovouto potřebou počítá a definuje statickou metodu getinstanceoffactory 12, které nám vrátí instanci třídy zadané v parametru. Zájemci si mohou její kód prohlédnout ve výpisu 4.1. Výpis 4.1: Definice metody getinstanceoffactory(class<t>) v interfejsu IGSMFactory 1 /*************************************************************************** 2 * Vrátí odkaz na instanci zadané tovární třídy. 3 * Předpokládá přitom, že tato třída má dostupný nulární konstruktor. 4 * 5 <T> Typ tovární třídy 6 factoryclass Class-objekt tovární třídy 7 Požadovaný odkaz 8 RuntimeException Instanci se nepodařilo vytvořit 9 */ 10 public static <T extends IGSMFactory> 11 T getinstanceoffactory(class<t> factoryclass) 12 { 13 T result; 14 try { 15 result = factoryclass.newinstance(); 16 } 17 catch (InstantiationException IllegalAccessException ex) { 18 throw new RuntimeException( 19 "\nnepodařilo se vytvořit instanci třídy " + factoryclass, ex); 20 } 21 return result; 22 } 4.5 Testování korektnosti správce scénářů Už tedy víme, jak bude rodič našeho správce scénářů vytvářet a poskytovat jednotlivé scénáře a jak bude umožňovat získání instance hry prostřednictvím po- 12 To je ta metoda, která byla ve výpisu 2.2 na straně 44 vynechaná. Strana 72 z 351

73 Kapitola 4: Správce scénářů 73 skytnutí class-objektu tovární třídy. Na začátku kapitoly jsme si ale říkali, že scénáře (a s nimi i jejich správce) zavádíme pouze proto, abychom všem usnadnili testování. Měli bychom se proto zamyslet nad tím, jak si prostřednictvím správce scénářů tuto činnost usnadnit. Nejprve je si třeba uvědomit, že první, co budeme muset otestovat, je vlastní správce scénářů. Při definici podkladů pro tvorbu scénářů je totiž možno nadělat řadu chyb (a většinu z nich si studenti neodpustí). Navíc jsou v zadání práce definovány jisté požadavky, které je také třeba dodržet. U správce scénářů je třeba zkontrolovat: jsou-li definovány oba základní scénáře základní úspěšný a základní chybový, vyhovění požadavkům zadání: základní úspěšný scénář: musí začínat startovním krokem (krok typu tsstart), musí mít zadaný minimální počet kroků, musejí v něm být použity povinné akce pro přesun (krok typu tsmove), zvednutí h-objektu (krok typu tspick_up) a položení h-objektu (krok typu tsput_down), musí v něm být použit zadaný minimální počet vlastních akcí (krok typu tsnon_standardx; kolikrát bude každá z těchto akcí použita, se nekontroluje), musí být navštíven zadaný minimální počet prostorů, hráč musí zahlédnout zadaný minimální počet prostorů (navštíví je nebo budou sousedem některého z navštívených prostorů), základní chybový scénář musí začínat nejméně jedním krokem testujícím špatné odstartování hry (krok typu tsnot_start), za ním musí následovat startovní krok (krok typu tsstart), celý scénář musí být ukončen krokem pro předčasné ukončení hry (krok typu tsend), krok ukončující hru nesmí být použit uprostřed scénáře, musí být začleněny všechny typy kroků definujících reakci na špatně zadaný příkaz, tj. kroků, jejichž typ kroku má v prvním parametru konstruktoru hodnotu 1, konzistenci jednotlivých kroků při základním přesunu z prostoru do prostoru (krok typu tsmove): Strana 73 z 351

74 Kapitola 4: Správce scénářů 74 prostor zadaný v parametru musí být mezi sousedy aktuálního prostoru, hráč se přesune do prostoru zadaného v parametru, nezmění se obsah batohu, při zvedání a pokládání h-objektů (kroky typu tspick_up a tsput_down): nezmění se aktuální prostor, zvedaný h-objekt zmizí z aktuálního prostoru a objeví se v batohu, pokládaný h-objekt se přesune z batohu do aktuálního prostoru, u kroků typu tsnon_standardx musí počet předávaných parametrů odpovídat číslu uvedenému jako poslední znak typu kroku, název použitý pro jeden druh akce (např. zvednutí h-objektu) nesmí být použit po jiný druh akce (např. přechod z prostoru do prostoru) včetně typů tsnon_standardx. Jak vidíte, již na samotném správci scénáři, resp. na jím spravovaných scénářích je toho ke kontrole opravdu dost. Bylo by proto vhodné definovat metodu void autotest() po jejímž zavolání se oslovený správce scénářů otestuje sám. 4.6 Testování korektnosti hry Když už víme, že náš správce scénářů neobsahuje ty nekřiklavější chyby, mohli bychom jej konečně použít k tomu, k čemu jsme jej od počátku vytvářeli: k otestování vytvořené hry prostřednictvím spravovaných scénářů. Koncepci scénářů a jejich správce jsme od počátku vytvářeli tak, aby nám umožnila jednotné testování nezávisle na testované hře. Je proto logické, že by bylo vhodné ten univerzální test opět vytknout do společného rodiče, tj. do třídy AScenarioManager. Ta by mohla definovat dvojici metod: boolean testgame() boolean testgamebyscenarios(string... names) První z nich provede základní test tak, že hru příslušnou danému správci scénářů postupně dvakrát za sebou odehraje podle základního úspěšného scénáře a poté ještě jednou podle základního chybového scénáře. Druhá z nich otestuje hru prostřednictvím jejího simulovaného odehrání prostřednictvím scénářů, jejichž názvy obdrží v parametru. (Z toho je zřejmé, že ta první zřejmě zavolá tu druhou s příslušnými parametry.) Strana 74 z 351

75 Kapitola 4: Správce scénářů Definice interfejsu IScenarioManager Tím jsme základní analýzu požadavků na správce scénářů ukončili. Definici interfejsu IScenarioManager, který by je měl deklarovat, si můžete prohlédnout ve výpisu 4.2. Výpis 4.2: Definice interfejsu IScenarioManager 1 /******************************************************************************* 2 * Instance interfejsu {@code IScenarioManager} představují správce scénářů 3 * své hry předpokládající textové uživatelské rozhraní. 4 */ 5 public interface IScenarioManager extends IAuthor, Iterable<Scenario> 6 { 7 //== STATIC CONSTANTS ========================================================== 8 9 /** Název základního úspěšného scénáře. */ 10 public static final String HAPPY_SCENARIO_NAME = "_HAPPY_"; /** Název základního chybového scénáře. */ 13 public static final String MISTAKE_SCENARIO_NAME = "_MISTAKE_"; //== STATIC METHODS ============================================================ //############################################################################## 22 //== ABSTRACT GETTERS AND SETTERS ============================================== /*************************************************************************** 25 * Vrátí třídu, přesněji řečeno class-objekt třídy, 26 * jejíž instance umějí realizovat spravované scénáře. 27 * 28 Třída hry schopné realizovat spravované scénáře 29 */ 30 public Class<? extends IGame> getgameclass() 31 ; /*************************************************************************** 35 * Vrátí instanci hry, kterou můžeme hrát podle spravovaných scénářů. 36 * Metoda však nezaručuje stav hry, tj. jestli je např. právě rozehraná 37 * a nebude ji proto třeba ji před dalším spuštěním nejprve ukončit. 38 * 39 Instance hry hratelné podle spravovaných scénářů 40 */ 41 public IGame getgame() 42 ; 43 Strana 75 z 351

76 Kapitola 4: Správce scénářů /*************************************************************************** 46 * Vrátí seznam názvů všech spravovaných scénářů. 47 * 48 Seznam s názvy scénářů 49 */ 50 public Collection<String> getallscenarionames() 51 ; /*************************************************************************** 55 * Vrátí scénář se zadaným názvem. 56 * 57 name Název požadovaného scénáře 58 Scénář se zadaným názvem; není-li, vrátí {@code null} 59 */ 60 public Scenario getscenario(string name) 61 ; //== OTHER ABSTRACT METHODS ==================================================== /*************************************************************************** 69 * Základní test ověřující, jestli správce scénářů vyhovuje 70 * zadaným okrajovým podmínkám. 71 * Správce scénáře se automaticky prověří a výsledky vytisknou 72 * již při vytváření testu. 73 * 74 Projde-li správce testem, vrátí {@code true}, 75 * jinak vrátí {@code false} 76 */ 77 public boolean autotest() 78 ; /*************************************************************************** 82 * Prověří, že hra pracuje podle obou povinných scénářů, 83 * tj. podle základního úspěšného, ještě jednou podle základního úspěšného 84 * a pak podle základního chybového. 85 * 86 Projdou-li testy, vrátí {@code true}, 87 * jinak vrátí {@code false} 88 */ 89 public boolean testgame() 90 ; /*************************************************************************** 93 * Prověří hru tak, že ji "zahraje" dle scénářů se zadanými názvy. 94 * Zadaný scénář musí být testovací. 95 * Strana 76 z 351

77 Kapitola 4: Správce scénářů names Názvy testovacích scénářů 97 Projdou-li testy, vrátí {@code true}, 98 * jinak vrátí {@code false} 99 */ 100 public boolean testgamebyscenarios(string... names) 101 ; /*************************************************************************** 104 * Vrátí iterátor, který bude postupně poskytovat 105 * jednotlivé spravované scénáře v pořadí, v jakém byly zadány. 106 * 107 Iterátor přes spravované scénáře 108 */ 110 public Iterator<Scenario> iterator() 111 ; /*************************************************************************** 115 * Vrátí datovod, který bude postupně poskytovat 116 * jednotlivé spravované scénáře v pořadí, v jakém byly zadány. 117 * 118 Datovod spravovaných scénářů 119 */ 120 public Stream<Scenario> stream() 121 ; //== DEFAULT GETTERS AND SETTERS =============================================== /*************************************************************************** 127 * Vrátí základní úspěšný scénář happy scenario. 128 * 129 Základní úspěšný scénář 130 */ 131 default Scenario gethappyscenario() 132 { 133 return getscenario(happy_scenario_name); 134 } /*************************************************************************** 138 * Vrátí základní chybový scénář. 139 * 140 Základní chybový scénář 141 */ 142 default Scenario getmistakescenario() 143 { 144 return getscenario(mistake_scenario_name); 145 } Strana 77 z 351

78 Kapitola 4: Správce scénářů //== OTHER DEFAULT METHODS ===================================================== //############################################################################## 154 //== NESTED DATA TYPES ========================================================= 155 } 4.8 Společný rodič správců třída AScenarioManager Základní požadavky kladené na správce scénářů jsme tedy shrnuli. Nyní bychom si měli ještě rozmyslet, jak takového správce scénářů vytvoříme. Opět připomenu, že připravujeme úlohu, kterou bude typicky řešit celá řada studentů, přičemž každý z nich bude připravovat jinou hru. Každá z těchto her bude mít svoji vlastní sadu scénářů, kterou bude spravovat správce scénářů přiřazený dané hře. Zamyslíte-li se nad metodami definovanými v interfejsu IScenarioManager, jistě vám dojde, že odchylky správců scénářů jednotlivých her nebudou tak zásadní, aby se kvůli nim musely lišit definice jejich metod. Lišit se budou pouze data, s nimiž budou jednotliví správci a jejich scénáře pracovat. Bylo by proto rozumné tyto metody vytknout do společného předka a osvobodit studenty od nutnosti jejich definice (beztak by ty shodné metody od sebe pouze opisovali). Framework proto definuje abstraktní třídu AScenarioManager, která je povinným společným rodičem správců scénářů všech studentů. Jak si možná vzpomenete (viz diagram tříd balíčku eu.pedu.adv15p_fw.scenario na obrázku 3.1 na straně 64), třída je ve stejném balíčku jako ostatní třídy týkající se scénářů. Tato rodičovská třída převezme na svá bedra definice všech metod vyžadovaných interfejsem IScenarioManager. Každý student pak vytvoří svého potomka, jenž společnému rodiči dodá potřebná data především pak odkaz na s ním sdruženou tovární třídu (přesněji řečeno její class-objekt) a podklady pro vytvoření jednotlivých scénářů. Otázkou ale stále zůstává, jak mu tato data předat. Velmi častou cestou předávání dat mezi rodiči a jejich potomky bývají parametry rodičovského konstruktoru. Tuto cestu můžeme použít pro předání classobjektu třídy hry. Pro předávání scénářů se však tato cesta nehodí, protože by v našem případě bylo takovéto předávání poměrně nepřehledné a navíc bychom měli potomkům umožnit, aby mohli své scénáře před jejich předáním rodiči ještě na poslední chvíli nějak upravit. Strana 78 z 351

79 Kapitola 4: Správce scénářů 79 Vytvoření scénářů S řešením tohoto problému by nám mohl poradit návrhový vzor Stavitel (anglicky Builder). Ten doporučuje, abychom v případě, kdy je vybudování nějakého objektu náročné, rozdělili toto budování do mezi dva objekty řídící objekt (stavitel) a výkonný objekt (viz podšeděný blok Návrhový vzor Stavitel). Konstruktor rodičovského podobjektu, tj. konstruktor třídy AScenarioManager, proto od vytváření scénářů osvobodíme a pověříme jej jen zapamatováním základního údaje, kterým je třída hry (známe-li třídu hry, můžeme ji požádat o její jedinou instanci). Hlavička rodičovského konstruktoru bude mít tedy tvar: protected AScenarioManager(Class<? extends IGSMFactory> factoryclass) Návrhový vzor Stavitel (Builder) Návrhový vzor Stavitel (anglicky Builder) doporučuje oddělit reprezentaci složitějších objektů od procesu jejich budování. O vybudování objektu se podělí dva druhy objektů: Řídící objekt (můžeme mu říkat podle názvu vzoru Stavitel) ví, z jakých součástí má být daný objekt vybudování a jak má být z těchto součástí postaven. Výkonný objekt (ten by byl v naší analogii označen jako Dělník) ví, jak vytvořit součást, o kterou bude požádán. Tento přístup přináší dvě výhody: Zjednodušení definice onoho složitého objektu, která tak nebude svádět své uživatele k využívání metod, které nejsou určeny pro daný účel. (Při používání objektu nebudeme používat metody určené pro jeho vybudování a naopak.) Možnost vytvořit daný složitý objekt z jiných stavebních prvků, které se v nové konstelaci mohu jevit jako výhodnější. (Stavitel zůstane, ale dělník se vymění.) První z výhod je vám určitě zřejmá. Ta druhá plyne z toho, že stavitel sice ví, jak daný objekt poskládat z dodaných součástí, ale nemusí se starat o to, co jsou dodané součásti zač. Všichni jste se již jistě setkali se stavebnicí Lego. Ta má dvojnásobně zvětšeného sourozence nazvaného Duplo a určeného pro malé děti, které by mohly malé kostičky lega spolknout. Když se dítě-stavitel naučí stavět s duplem, může úplně stejnou (i když menší) skládačku postavit i s legem a naopak. Strana 79 z 351

80 Kapitola 4: Správce scénářů 80 Předání podkladů pro vytvoření scénářů společnému rodiči Koncepčně jsme tedy problém vyřešili: o vytvoření scénářů se postará konstruktor potomka, jenž bude vystupovat v roli stavitele, protože bude vědět, jaké konkrétní scénáře budou jeho instance spravovat. Postupně předá svému rodičovskému podobjektu, jenž bude vystupovat v roli výkonného objektu, potřebné podklady pro vytvoření jednotlivých požadovaných scénářů, a ten pak na jejich základě potřebné scénáře vybuduje. Zbývá tedy už jen vyřešit, jak budou studentští správci předávat svému rodiči podklady pro vytvoření jednotlivých scénářů. Jistě vás napadlo, že nejlepší bude definovat v rodičovské třídě metodu, která by potomku umožnila předávat podklady pro vytvoření scénáře již zkonstruovanému rodiči. Těmito podklady bude pole kroků budoucího scénáře. Volaná metoda pak vytvoří scénář a při té příležitosti zkontroluje, zda jsou dodrženy všechny požadované podmínky (např. že se dva přidávané scénáře nejmenují stejně). Vytvořený scénář u sebe navíc ihned zaregistruje. Tato metoda by měla být definována jako chráněná (protected), protože do vzájemné komunikace rodiče a potomka nikomu cizímu nic není. Současně by neměla být přebitelná, tj. měla by být definována s modifikátorem final. Metoda totiž slouží mimo jiné k organizaci interních informací rodiče, a je proto potřeba zabezpečit, aby ji potomci nemohli nějak nevhodně ovlivnit. Dopředu prozradím, že pro naše účely by bylo vhodné, kdybychom měli tři takovéto výkonné metody: První metoda bude umět vytvořit standardní testovací scénář, takže jí bude třeba dodat kroky jako instance třídy ScenarioStep, protože ty umějí uchovat všechny informace potřebné k otestování stavu hry po provedení zadaného kroku. Druhá metoda bude umět vytvořit pouze demonstrační scénář, takže se spokojí se zadáním textů jednotlivých příkazů. Informace o stavu hry po provedení příkazu nevyplní, protože ví, že se při demonstraci nic nekontroluje a žádné jiné informace než zadávaný příkaz proto nebudou potřeba. Třetí metoda bude předpokládat, že jsme potřebný scénář již nějak získali, a pouze jej zařadí mezi spravované (samozřejmě až po kontrole, že ve správě již není scénář se shodným názvem). Z popisu vlastností požadovaných metod je zřejmé, že implementace bude zřejmě vypadat tak, že první dvě metody nějak vytvoří scénář, a potom zavolají třetí metodu, aby jej zařadila do správy. Strana 80 z 351

81 Kapitola 4: Správce scénářů 81 Ještě by ale bylo vhodné zabezpečit, aby se sada scénářů nemohla měnit za běhu programu. Toho můžeme dosáhnout např. tak, že definujeme speciální (opět chráněnou) metodu, jejímž zavoláním potomek rodiči oznámí, že už mu zadal všechny scénáře a že už žádné další přidávat nebude. Mohli bychom říci, že tím připravenou sadu scénářů zalepí, aby ji už nebylo možno dále měnit. Tuto zalepovací metodu pojmenujeme seal(). Tento přístup (který mimochodem znáte ze třídy Multishape) zabezpečí dvě věci: Sada bude spolehlivá, protože jakmile ji potomek jednou vytvoří, nebude ji již nikdo moci modifikovat. Se sadou scénářů nebude moci nikdo pracovat dříve, než bude její vytváření ukončeno, protože rodičovské metody pracující se scénáři při každém volání nejprve zkontrolují, zda už bylo zadávání scénářů skončeno, tj. zda je sada scénářů již zalepena. Pokud tomu tak nebude, vyhodí běhovou výjimku. Tyto úvahy nás vedou k tomu, že by společný rodič správců scénářů měl (mimo jiné) obsahovat následující metody: protected final void addscenario(string name, TypeOfScenario type, ScenarioStep... steps) protected final void addscenario(string name, String... command) protected final void seal() Tyto metody by pak volal konstruktor dceřiného objektu. Optimální je volat je vzápětí po vytvoření rodičovského podobjektu a hned také objekt zalepit. Detaily definice konkrétních správců budeme řešit až v kapitole 5 Návrh správce scénářů konkrétní hry na straně 84, v níž se tomuto problému budeme podrobně věnovat. Rozbor požadavků na třídu AScenarioManager jako společného rodiče správců scénářů teď opustíme. Pokračování necháme až do dílu, v němž budeme podrobněji rozebírat, jak zprostředkovat testování správce scénářů a příslušné hry. 4.9 Shrnutí požadavků Tím bychom mohli naši analýzu požadavků na správce scénářů ukončit. Jak jsme si ukázali, většina je jich natolik obecných, že je můžeme realizovat ve společném rodiči všech scénářů, který bude součástí frameworku. Na správce scénářů konkrétní hry tak zbývá jediné: definovat jednotlivé kroky vytvářených scénářů a správně domluvit se svým rodičovským objektem vytvoření jednotlivých scénářů a jejich registraci. To se naučíme v příští kapitole, tj. v kapitole 5 Návrh správce scénářů konkrétní hry na straně 84. Strana 81 z 351

82 Kapitola 4: Správce scénářů Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Zopakujme si, co jsme v této kapitole dozvěděli a co jsme naprogramovali: Všechny scénáře přiřazené dané hře má na starost správce scénářů. Třídy definující jednotlivé správce scénářů musí implementovat interfejs IScenarioManager. Správci scénářů jsou povinně potomky abstraktní třídy AScenarioManager. Třída AScenarioManager má jednoparametrický konstruktor vyžadující zadání class-objektu tovární třídy sdružené s daným správcem scénářů. Správce scénářů musí povinně definovat základní úspěšný scénář pojmenovaný _HAPPY_ a základní chybový scénář pojmenovaný _MISTAKE_. Správce scénářů je nezměnitelně svázán s hrou, kterou je možno hrát podle jím spravovaných scénářů. Třída AScenarioManager definuje pro své instance následující metody: Scenario getscenario(string name) Vrátí scénář se zadaným názvem. Scenario gethappyscenario() Vrátí instanci základního úspěšného scénáře. Scenario getmistakescenario() Vrátí instanci základního chybového scénáře. Collection<String> getallscenarionames() Vrátí kolekce s názvy všech spravovaných scénářů. Iterator<Scenario> iterator() Vrátí iterátor umožňující procházet spravované scénáře a zpracovávat je v cyklu v pořadí, v němž byly vytvořeny správcem. Stream<Scenario> stream() Vrátí datovod umožňující procházet spravované řetězce a zpracovávat je v pořadí, v němž byly vytvořeny správcem. Class<? extends IGame> getgameclass() Metoda vrací class-objekt třídy, jejímiž instancemi jsou hry testovatelné spravovanými scénáři. IGame getgame() Metoda vrací instanci hry testovatelné spravovanými scénáři. Strana 82 z 351

83 Kapitola 4: Správce scénářů 83 void addscenario(string name, TypeOfScenario type,scenariostep... steps) Metoda vytvoří scénář zadaného typu se zadanými kroky a přidá jej mezi spravované. void addscenario(string name, String... commands) Metoda vytvoří demonstrační (tj. netestovatelné) scénáře s kroky se zadanými příkazy. void addscenario(scenario scenario) Metoda zařadí zadaný scénář do správy. void seal() Metoda zalepí vytvořenou sadu scénářů, takže již do ní nebude možno nic přidávat ani v ní cokoliv měnit. Správce scénářů lze plnohodnotně používat až jeho zalepení. void autotest() Zavoláním této metody se scénář sám otestuje, zda splňuje všechny podmínky uvedené v pasáži Testování korektnosti správce scénářů na straně 72. boolean testgame() Provede základní test hry příslušné danému správci scénářů tak, že ji postupně dvakrát za sebou odehraje podle základního úspěšného scénáře a poté ještě jednou podle základního chybového scénáře. boolean testgamebyscenarios(string... names) Otestuje hru prostřednictvím scénářů, jejichž názvy obdrží v parametru. Tvorba jednotlivých scénářů je realizována rodičovskou třídou prostřednictvím metod addscenario(???) a void seal() uvedených v podbodech předchozího bodu. Tyto metody je optimální volat v konstruktoru vzápětí po vytvoření rodičovského podobjektu. Strana 83 z 351

84 Kapitola 5: Návrh správce scénářů konkrétní hry Návrh správce scénářů konkrétní hry Kapitola 5 Návrh správce scénářů konkrétní hry Co se v kapitole naučíte V minulé kapitole jsme si ukázali, jak jsou nastaveny mantinely rámce pro to, abychom mohli definovat vlastního správce scénářů, jenž by se měl stát základem testů následně vyvíjené hry. V této kapitole si vysvětlíme, jak takového správce scénářů doopravdy navrhnout. Ukážu vám definici dvou druhů scénářů: první bude poněkud jednodušší a svým způsobem i přehlednější, ale bude přinášet jisté komplikace při případných budoucích úpravách. Druhý bude sice trochu méně přehledný, ale na druhou stranu vám při případných úpravách leccos usnadní. V této kapitole se opět vrátíme k programování. Ukázková řešení budou dále rozšiřovat projekt A102z_Start obsahující závěrečnou podobu projektu z kapitoly 2 Podpůrný framework. 5.1 Zadání, které budeme řešit Seznámili jste se s třídami frameworku účastnícími se na přípravě scénářů, takže máte vše připraveno k tomu, abyste mohli definovat správce scénářů vaší vzácné hry. Než se však do jeho definice pustíte, musíte si nejprve rozmyslet, o čem naše hra bude. Zkusím vám vše naznačit na příkladu jednoduché vzorové hry. Vzorová hra, na níž se pokusím vše demonstrovat, bude probíhat podle následujícího námětu (textového scénáře): Strana 84 z 351

85 Kapitola 5: Návrh správce scénářů konkrétní hry 85 Jste na služební cestě a přijíždíte unaveni do služebního bytu. Máte hlad a žízeň. Hledáte tedy kuchyň a v ní ledničku, abyste se trochu posilnili. Lednička ale nejde otevřít. Leží na ní papír, který však nemůžete přečíst bez brýlí. Musíte proto najít brýle, abyste si přečetli, že lednička někdy nejde otevřít, dokud se něčím nepodloží. Musíte tedy najít časopis (nic jiného vhodného v bytě není) a podložit s ním ledničku. Když ledničku otevřete a chcete si z ní vzít pivo, tak na vás lednička promluví a oznámí vám, že je inteligentní a jakýkoliv alkoholický nápoj vás nechá vzít pouze tehdy, pokud ji přesvědčíte, že vám je již víc než 18 let. Musíte s ní tedy pohovořit a přesvědčit ji o své plnoletosti. Pak už si můžete vzít, co chcete, a konečně se osvěžit. Průběh hry můžete zrychlit, když při průchodu bytem cestou k ledničce předem seberete brýle a časopis, abyste se pak pro ně nemuseli vracet a případně je i hledat. Ještě více pak hru zrychlíte, když se nebudete pokoušet nic číst, rovnou přijdete do kuchyně s časopisem a podložíte ledničku. Předchozí scénář jsem se pokusil navrhnout tak, aby obsahoval dva typické nestandardní h-objekty: h-objekt, který po otevření či jiné ekvivalentní operaci představuje nestandardní prostor, a h-objekt, s nímž je možno pohovořit. Inteligentní lednička představuje oba dva. Pojďme nyní definovat programové scénáře, které prověří, zda následně vytvořená hra odpovídá tomuto námětu. Jak už z předchozího víme, přímo je definovat nemůžeme. Musíme vytvořit správce scénářů, který jednotlivé scénáře definuje prostřednictvím svého rodiče. My pak musíme pro tohoto rodiče definovat jednotlivé kroky budoucího scénáře. Pojďme se tedy podívat, jak bychom mohli takového správce definovat. 5.2 Prázdný správce scénářů Abych vám vývoj aplikace co nejvíce ulehčil, připravil jsem ve frameworku balíček empty_classes (přesněji eu.pedu.adv15p_fw.empty_classes), v němž najdete poloprázdné verze (mohli bychom říci šablony) tříd, které má vaše aplikace obsahovat. Šablonou správců scénářů je třída EmptyScenarioManager, která představuje výchozí verzi vašeho správce scénářů. Začněte proto tím, že ji zkopírujete do balíčku vaší budoucí hry a vhodně ji přejmenujete (minimálně bych doporučil smazat v názvu počáteční Empty). Strana 85 z 351

86 Kapitola 5: Návrh správce scénářů konkrétní hry 86 Import k odstranění Refaktorační modul vám při kopírování třídy často automaticky vloží na začátek zdrojového kódu příkaz importující třídy z balíčku, odkud jste danou třídu zkopírovali, tj. v našem případě příkaz: import eu.pedu.adv15p_fw.empty_classes.*; Ten byste měli rychle odstranit, aby všichni viděli, vaše nová třída nechce mít se třídami z tohoto balíčku nic společného. Implementace interfejsu IAuthorPrototype Třída EmptyScenarioManager implementuje interfejs IAuthorPrototype, který zabezpečoval implementaci interfejsu IAuthor. Tento interfejs ale vydává za autora mne. V pasáži Řešení v Javě 8 na straně 42 jsme si ale ukazovali, jak definovat vlastní interfejs, který bude mít podobnou funkci. V podkapitole 2.5 Vytváříte svůj projekt na straně 41 jste dokonce měli za úkol začlenit svoji verzi tohoto interfejsu do svého projektu. Upravte proto hlavičku třídy tak, aby implementovala váš interfejs. Potomek třídy AScenarioManager Třída EmptyScenarioManager je potomkem abstraktní třídy AScenarioManager, která je součástí frameworku. Tato rodičovská třída obsahuje definice všech metod, o nichž jsem hovořil v minulé kapitole. Jediné, co vám zbývá, je přizpůsobit dokumentační komentář třídy vaší plánované hře a doplnit definice kroků úspěšného a chybového scénáře. Vše ostatní je již připraveno. Class-objekt sdružené tovární třídy Jako první definice v těle zkopírované třídy je definice statické konstanty FACTORY_CLASS s class-objektem sdružené tovární třídy. Změňte oslovení prázdné tovární třídy EmptyGSMFactory na oslovení té vaší. Nyní už zbývá pouze doplnit kroky scénářů vaší budoucí hry. Jak už jsem slíbil, ukážu vám dva způsoby, jak kroky scénářů definovat. Úprava sdružené tovární třídy Vaše tovární třída ještě neví, že jste právě definovali třídu správce scénářů. Teď je ta správná chvíle jí to vysvětlit. Odkomentujte v tovární třídě metodu Strana 86 z 351

87 Kapitola 5: Návrh správce scénářů konkrétní hry 87 getscenariomanager() a v příkazu return nahraďte název fiktivní třídy ScenarioManager názvem té vaší. 5.3 Jednoduchý správce využívající literály Nejprve vám ukážu jednodušší verzi správce, v níž se zadávají přímo texty, které se budou předávat konstruktoru jednotlivých kroků scénáře. Třídu tohoto správce nazveme RUPScenarioManagerLit, kde závěrečné Lit naznačuje, že texty jsou zadávány prostřednictvím textových literálů. Výňatek z definice ukazuje výpis 5.1. V něm najdete definice některých dále potřebných konstant a také podklady pro definice prvních tří kroků budoucího základního úspěšného scénáře, abyste získali představu o tom, jak můžete připravit podklady pro vytvoření vlastního scénáře. Výpis 5.1: Výpis první části zdrojového kódu třídy RUPScenarioManagerLit až po definici prvních dvou kroků úspěšného (šťastného) scénáře 1 /******************************************************************************* 2 * Instance třídy {@code RUPScenarioManagerLit} slouží jako správce scénářů, 3 * které mají prověřit a/nebo demonstrovat správnou funkci plánované hry, 4 * přičemž texty jsou v této třídě definovány pomocí literálů. 5 * <p> 6 * Jednotlivé scénáře jsou iterovatelné a "streamovatelné" posloupností kroků 7 * specifikovaných instancemi třídy 8 * {@link eu.pedu.adv15p_fw.scenario.scenariostep} 9 * z frameworku, do nějž je třeba vyvíjenou hru hra začlenit. 10 * <p> 11 * Správce scénářů je jedináček, který má na starosti všechny scénáře 12 * týkající se s ním sdružené hry. 13 * <p> 14 * Jednotlivé spravované scénáře se musí lišit svým názvem, 15 * přičemž názvy základního úspěšného a základního chybového scénáře 16 * jsou předem pevně dány a není je možno změnit. 17 */ 18 public class RUPScenarioManagerLit extends AScenarioManager 19 implements IAuthorRUP 20 { 21 //== CONSTANT CLASS ATTRIBUTES ================================================= /** Tovární třída, jejíž instancemi jsou tovární objekty poskytující 24 * instanci správce scénářů i hry, jejíž scénáře daný správce spravuje. */ 25 private final static Class<? extends IGSMFactory> FACTORY_CLASS = 26 RUPGSMFactory.class; /** Pomocné konstanty pro rozhovor s ledničkou. */ 29 private static final int AGE = 20; 30 private static final int THIS_YEAR; Strana 87 z 351

88 Kapitola 5: Návrh správce scénářů konkrétní hry private static final int BORN_YEAR; 32 static { 33 THIS_YEAR = LocalDate.now().getYear(); 34 BORN_YEAR = THIS_YEAR - AGE; 35 } /*************************************************************************** 39 * Počáteční krok hry, který je pro všechny scénáře shodný. 40 * <p> 41 * Konstruktor plnohodnotné instance třídy {@link ScenarioStep} 42 * vyžaduje následující parametry: 43 <pre> {@code 44 TypeOfStep typeofstep; //Typ daného kroku scénáře 45 String command; //Příkaz realizující tento krok scénáře 46 String message; //Zpráva vypsaná po zadání příkazu 47 String area; //Prostor, v němž skončí hráč po zadání příkazu 48 String[] neighbors; //Sousedé aktuálního prostoru (= východy) 49 String[] items; //H-objekty vyskytující se v daném prostoru 50 String[] bag; //Aktuální obsah batohu 51 }</pre> 52 =======================================================================<br> 53 * Kroky scénáře musejí navíc vyhovovat následujícím požadavkům: 54 * <ul> 55 * <li>žádná z položek nesmí obsahovat prázdný odkaz.</li> 56 * <li>s výjimkou položky {@code command} nesmí být žádný řetězec 57 * prázdný (tj. {@code ""})</li> 58 * <li>prázdný řetězec se nesmí objevit ani jako položka některého 59 * z vektorů {@code neighbors}, {@code items} či {@code bag}</li> 60 * </ul> 61 * <br> 62 *************************************************************************** 63 */ 64 private static final ScenarioStep START_STEP = 65 new ScenarioStep(0, tsstart, "", //Spouštěcí příkaz 66 "Vítáme vás ve služebním bytě. Jistě máte hlad." + 67 "\nnajděte v bytě ledničku - tam vás čeká svačina." 68, 69 "Předsíň", 70 new String[] { "Ložnice", "Obývák", "Koupelna" }, 71 new String[] { "Botník", "Deštník" }, 72 new String[] {} 73 ); /*************************************************************************** 77 * Kroky základního úspěšného scénáře 78 * popisující očekávatelný úspěšný průběh hry. 79 * Z těchto kroků sestavený scénář nemusí být nutně nejkratším možným 80 * (takže to vlastně ani nemusí být základní úspěšný scénář), 81 * ale musí vyhovovat všem okrajovým podmínkám zadání, 82 * tj. musí obsahovat minimální počet kroků, Strana 88 z 351

89 Kapitola 5: Návrh správce scénářů konkrétní hry * projít požadovaný.minimální počet prostorů 84 * a demonstrovat použití všech požadovaných akcí. 85 */ 86 private static final ScenarioStep[] HAPPY_SCENARIO_STEPS = 87 { 88 START_STEP 89, 90 new ScenarioStep(tsMOVE, "jdi koupelna", 91 "Přesunul(a) jste se do místnosti: Koupelna" 92, 93 "Koupelna", 94 new String[] { "Předsíň" }, 95 new String[] { "Brýle", "Umyvadlo", "Časopis" }, 96 new String[] {} 97 ) 98, 99 new ScenarioStep(tsPICK_UP, "vezmi brýle", 100 "Vzal(a) jste h-objekt: Brýle" 101, 102 "Koupelna", 103 new String[] { "Předsíň" }, 104 new String[] { "Umyvadlo", "Časopis" }, 105 new String[] { "Brýle" } 106 ) Počáteční krok Ve výpisu si všimněte, že data popisující první krok scénáře jsou uložena samostatně v atributu START_STEP (řádky 65 až 74). V dokumentačním komentáři tohoto atributu je současně popsán význam jednotlivých parametrů předávaných konstruktoru třídy ScenarioStep, jejíž instance představují kroky budoucích scénářů. Atribut START_STEP je zde definován proto, že všechny scénáře začínají stejným startovním krokem. Zadání to sice nepředepisuje, takže to není povinné, ale řekl bych, že je to nanejvýš užitečné. Přiznám se, že zatím ještě nikdo nepřišel s hrou, která by začínala nějakým náhodným stavem. I kdyby tomu ale tak bylo, tak ve scénářích musí být počáteční krok (a nejen ten) jednoznačně definován, takže nakonec zjistíte, že i pro takovouto hru je vhodné definovat společný počáteční krok scénářů, aby bylo možno později všechny případné změny provést na jednom místě a nemuseli jste bloudit zdrojovým kódem a hledat jednotlivé definice počátečních kroků, abyste je společně upravili. Pokračovací kroky Na pokračovacích krocích je příjemné to, že je můžete zadávat tak, že zkopírujete definici předchozího kroku a upravíte v ní pouze těch pár informací, které se od Strana 89 z 351

90 Kapitola 5: Návrh správce scénářů konkrétní hry 90 minulého kroku změnily. Tím se doba potřebná k jejich definici výrazně zkrátí a definice celého správce scénářů se tak zefektivní. Protože ve výpisu definic následujících kroků není nic pozoruhodného, tak je zde přeskočíme (koho zajímají, může si otevřít zdrojový kód doprovodných programů) a ve výpisu 5.2 proto pokračujeme až od definice posledního kroku úspěšného (šťastného) scénáře. Výpis 5.2: Výpis druhé části zdrojového kódu třídy RUPScenarioManagerLit od posledního kroku úspěšného (šťastného) scénáře až po definici prvních čtyř kroků chybového scénáře 1 //===== Poslední krok úspěšného scénáře ===== 2 new ScenarioStep(tsEND, "konec", 3 "Konec hry. \nděkujeme, že jste zkusil(a) naši hru." 4, 5 "Kuchyň", 6 new String[] { "Obývák", "Ložnice" }, 7 new String[] { "Lednička", "Papír" }, 8 new String[] { "Pivo" } 9 ) }; /** Krok testující špatné spuštění hry je definován zvlášť, 15 * aby bylo možno správně nastavit indexy následujících kroků. */ 16 private static final ScenarioStep WRONG_START = 17 new ScenarioStep(-1, 18 tsnot_start, "Start", 19 "\nprvním příkazem není startovací příkaz." + 20 "\nhru, která neběží, lze spustit pouze startovacím příkazem.\n" 21, 22 "", 23 new String[] {}, 24 new String[] {}, 25 new String[] {} 26 ); static { ScenarioStep.setIndex(1); } /*************************************************************************** 33 * Základní chybový scénář definující reakce 34 * na povinnou sadu typů chybně zadaných příkazů. 35 */ 36 private static final ScenarioStep[] MISTAKE_SCENARIO_STEPS = 37 { 38 WRONG_START, 39 Strana 90 z 351

91 Kapitola 5: Návrh správce scénářů konkrétní hry START_STEP, new ScenarioStep(tsUNKNOWN, "maso", 43 "Tento příkaz neznám." + 44 "\nchcete-li poradit, zadejte příkaz?" 45, 46 "Předsíň", 47 new String[] { "Ložnice", "Obývák", "Koupelna" }, 48 new String[] { "Botník", "Deštník" }, 49 new String[] {} 50 ) 51, 52 new ScenarioStep(tsEMPTY, "", 53 "Zadal(a) jste prázdný příkaz." 54, 55 "Předsíň", 56 new String[] { "Ložnice", "Obývák", "Koupelna" }, 57 new String[] { "Botník", "Deštník" }, 58 new String[] {} 59 ), Přechod mezi scénáři Základní chybový scénář Tato pasáž je určena pro ty hloubavější typy, kteří chtějí pochopit, proč jsem část programu ve výpisu 5.2 napsal právě takto. Koho detaily nezajímají, může ji přeskočit a pokračovat za dalším nadpisem. Možná někoho zarazí separátní definice kroku testujícího reakci na špatné spuštění hry uloženého v atributu WRONG_START, když je evidentní, že tento test bude proveden pouze jednou na počátku chybového scénáře. Důvodem je to, že třída ScenarioStep přiděluje vytvářeným instancím indexy a já jsem nechtěl toto číslování bourat. Připomínám, že počáteční krok chybového scénáře má za úkol otestovat reakci na nestartovní příkaz zadaný neběžící hře. U reakce na tento příkaz se netestuje stav hry, takže je jedno, jaký stav (tj. aktuální prostor, jeho sousedé, h-objekty a obsah batohu) do kroku zadáme. Jediné, co se bude testovat, je odpověď hry. Podíváte-li se ve výpisu 5.1 na straně 87 na definici atributu START_STEP začínající na řádku 65, zjistíte, že pro vytvoření startovacího kroku je použit konstruktor, který má oproti konstruktorům použitým pro vytvoření dalších kroků o parametr více prvním parametrem tohoto konstruktoru je index daného kroku. Třída ScenarioStep si nastavenou hodnotu indexu kroku zapamatuje a dokud znovu nepoužijete indexovací konstruktor, přiřadí každému dalšímu kroku index o jedničku větší, než měl krok předchozí. Strana 91 z 351

92 Kapitola 5: Návrh správce scénářů konkrétní hry 92 Startovací krok má index 0, protože se provede ještě před tím, než hráč začne hrát. Krok popisující reakci na první příkaz hráče pak má index o jedničku větší, což je právě to, co potřebujeme. Krok pro test špatného odstartování hry jsem uložil do atributu WRONG_START, proto, abych mu mohl přiřadit index -1, protože se má vykonat ještě před tím, než se hra odstartuje. Mohl bych mu jej sice přiřadit i v případě, kdybych jej definoval uvnitř pole kroků, ale pak by nastal problém s indexem dalšího vytvořeného kroku. Dalším zadávaným krokem je startovací. Ten se nevytváří, protože jsme jej již dávno vytvořili a v poli kroků se na něj pouze odkazujeme. Bude mít proto stále svůj index 0 nezávisle na tom, co se kolem děje. Tam by problém nebyl. Problém by nastal až u následujícího kroku. U něj bych musel buď použít konstruktor umožňující zadání indexu, anebo bych se musel smířit s tím, že bude mít index o jedničku větší než naposledy vytvořený krok, takže nulu. Ani jedna z těchto možností se mi nelíbí. Proč se mi nelíbí krok se zopakovaným indexem 0, je zřejmé. Přímé nastavení indexu se mi nelíbí proto, že kroky scénáře se nejčastěji definují tak, že se vždy zkopíruje předchozí krok a změní se v něm pár údajů. Při tomto kopírování však hrozí, že u této kopie zapomenu smazat nastavení indexu, a uprostřed scénáře se mi zase začnou kroky číslovat od jedničky. Zvolil jsem proto alternativní cestu. Využil jsem toho, že třída ScenarioStep definuje statickou metodu setindex(int), která umožňuje zadat index příště vytvářené instance. Vložil jsem proto za definici atributu WRONG_START statický inicializační blok (ve výpisu 5.2 je na řádku 29), který třídě ScenarioStep vysvětlí, že má dalšímu vytvořenému kroku přiřadit index 1. Po testu reakce na špatné spuštění hry a následný řádný startovací příkaz následují ve výpisu 5.2 dva další testované kroky. Pak je výpis znovu předčasně ukončen, protože počet kroků je opět velký. Zbytek kroků proto přeskočíme a přejdeme k definici prvního atributu za kroky chybového scénáře. Výpis 5.3: Výpis třetí části zdrojového kódu třídy RUPScenarioManagerLit začínající za definicí kroků chybového scénáře (tj. definicí atributu s odkazem na jedináčka) a pokračující až do konce definice třídy 1 /** Jediná instance této třídy. Spravuje všechny scénáře sdružené hry. */ 2 private static final RUPScenarioManagerLit MANAGER = 3 new RUPScenarioManagerLit(); //== VARIABLE CLASS ATTRIBUTES ================================================= Strana 92 z 351

93 Kapitola 5: Návrh správce scénářů konkrétní hry //############################################################################## 12 //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== /*************************************************************************** 15 * Statický konstruktor je u definice konstant AGE, THIS_YEAR a BORN_YEAR 16 * a pak ještě jednou před definicí konstanty MISTAKE_SCENARIO_STEPS 17 * Takováto inicializace by měla být před každou další konstantou 18 * definující kroky dalšího scénáře. 19 */ //== CLASS GETTERS AND SETTERS ================================================= 24 //== OTHER NON-PRIVATE CLASS METHODS =========================================== 25 //== PRIVATE AND AUXILIARY CLASS METHODS ======================================= //############################################################################## 30 //== CONSTANT INSTANCE ATTRIBUTES ============================================== 31 //== VARIABLE INSTANCE ATTRIBUTES ============================================== //############################################################################## 36 //== CONSTUCTORS AND FACTORY METHODS =========================================== /*************************************************************************** 39 * Vrátí správce scénářů - jedinou instanci této třídy. 40 * 41 Správce scénářů 42 */ 43 public static RUPScenarioManagerLit getinstance() 44 { 45 return MANAGER; 46 } /*************************************************************************** 50 * Vytvoří instanci představující správce scénářů hry. 51 * V rámci konstruktoru je vhodné vytvořit všechny scénáře 52 * a správce scénářů poté zalepit. 53 * <p> 54 * Jednotlivé spravované scénáře se musí lišit svým názvem, 55 * přičemž názvy základního úspěšného a základního chybového scénáře 56 * jsou předem pevně dány a není je možno změnit. 57 * Jim zadávané názvy jsou proto pouze formální, protože 58 * jim volaná metoda stejně přiřadí ty předem definované. 59 * <p> 60 * Kontrakt metody 61 * {@link #addscenario(string, TypeOfScenario, ScenarioStep...) } 62 * vyžaduje, aby byl jak první přidán úspěšný scénář, tj. scénář typu Strana 93 z 351

94 Kapitola 5: Návrh správce scénářů konkrétní hry * {@link TypeOfScenario.scHAPPY}, a jako druhý chybový scénář, 64 * tj. scénář typu {@link MISTAKE_SCENARIO_NAME}. 65 * Na pořadí následně přidávaných scénářů nezáleží. 66 */ 67 private RUPScenarioManagerLit() 68 { 69 super(factory_class); addscenario(happy_scenario_name, 72 TypeOfScenario.scHAPPY, HAPPY_SCENARIO_STEPS); 73 addscenario(mistake_scenario_name, 74 TypeOfScenario.scMISTAKES, MISTAKE_SCENARIO_STEPS); 75 seal(); 76 } //== ABSTRACT METHODS ========================================================== 81 //== INSTANCE GETTERS AND SETTERS ============================================== 82 //== OTHER NON-PRIVATE INSTANCE METHODS ======================================== 83 //== PRIVATE AND AUXILIARY INSTANCE METHODS ==================================== //############################################################################## 88 //== NESTED DATA TYPES ========================================================= //############################################################################## 93 //== TEST METHODS AND CLASSES ================================================== /*************************************************************************** 96 * Metoda prověřující daného správce scénářů 97 * či hru definovanou scénáři tohoto správce. 98 * <p> 99 * U správce scénářů se prověřuje, zda vyhovuje zadaným okrajovým podmínkám, 100 * tj. jestli: 101 * <ul> 102 * <li>umí vrátit správně naformátované jméno autora/autorky hry 103 * a jeho/její ID.</li> 104 * <li>definuje základní úspěšný a základní chybový scénář.</li> 105 * <li>základní chybový scénář má následující vlastnosti: 106 * <ul> 107 * <li>startovní příkaz, jehož název je prázdný řetězec</li> 108 * <li>minimální požadovaný počet kroků</li> 109 * <li>minimální počet navštívených prostorů</li> 110 * <li>minimální počet "zahlédnutých" prostorů</li> 111 * <li>minimální počet vlastních (nepovinných) akcí</li> 112 * <li>použití akcí pro přechod z prostoru do prostoru 113 * zvednutí nějakého h-objektu a položení nějakého h-objektu</li> 114 * <li>křížová konzistence akcí a stavů po jejich zadání</li> Strana 94 z 351

95 Kapitola 5: Návrh správce scénářů konkrétní hry * </ul> 116 * </li> 117 * <li>základní chybový scénář má následující vlastnosti: 118 * <ul> 119 * <li>chybné odstartování příkazem jenž není prázdný řetězec</li> 120 * <li>startovní příkaz, jehož název je prázdný řetězec</li> 121 * <li>použití všech povinných chybových typů kroku definovaných 122 * ve třídě<br> 123 * {@link eu.pedu.adv15p_fw.scenario.typeofstep}</li> 124 * <li>vyvolání nápovědy</li> 125 * <li>ukončení příkazem pro nestandardní ukončení hry</li> 126 * </ul> 127 * </li> 128 * </ul> 129 * <p> 130 * U hry se prověřuje, zda je možno ji zahrát přesně tak, 131 * jak je naplánováno ve scénářích. 132 * 133 args Parametry příkazového řádku - nepoužívané. 134 */ 135 public static void main(string[] args) 136 { 137 //Otestuje, zda správce scénářů a jeho scénáře vyhovují požadavkům 138 MANAGER.autoTest(); //Vypíše na standardní výstup simulovaný průběh hry 141 //odehrané podle základního úspěšného scénáře 142 MANAGER.getHappyScenario().simulate(); //Testování hry prováděné postupně podle obou povinných scénářů. 145 //přičemž základní úspěšný scénář se prochází dvakrát za sebou 146 // MANAGER.testGame(); //Testování hry dle scénáře se zadaným názvem 149 // MANAGER.testGameByScenarios("???"); //Odehrání hry dle scénáře se zadaným názvem 152 // MANAGER.playGameByScenario("???"); System.exit(0); 155 } } Umístění statických inicializačních bloků Když jsme hovořili o statických inicializačních blocích, nabádal jsem vás, abyste definovali vždy jen jeden. Teď jste se právě setkali se situací, kdy může být užitečné mít v programu statických inicializačních bloků více. Strana 95 z 351

96 Kapitola 5: Návrh správce scénářů konkrétní hry 96 První z nich je ve výpisu 5.1 na řádcích 33 až 36 a inicializuje dvě konstanty definované těsně před ním, druhý reinicializuje index kroku scénáře a setkali jsme se s ním ve výpisu 5.2 je na řádku 29. Kdybyste potřebovali definovat další scénáře, doporučuji vám tento reinicializační krok zopakovat před každou další definicí. Rozhodnete-li se však porušit konvence a umístit inicializační bloky na nějaká nestandardní místa, bylo by vhodné o tom čtenáře programu uvědomit např. komentářem v místě vyhrazeném pro standardní umístění inicializačního bloku, jak si to můžete prohlédnout ve výpisu 5.3 na řádcích 14 až 19. Konstruktor Jak jistě víte, aby mohl být správce korektně definován jako jedináček, musí být jeho konstruktor soukromý a o instanci musíme žádat zavoláním tovární metody většinou pojmenované getinstance(), která vrátí hodnotu uloženou ve statickém atributu. Ten je deklarován jako poslední konstanta třídy a výpis 5.3 s ním začíná. Vlastní konstruktor předá konstruktoru rodičovského podobjektu class-objekt třídy testované hry. (Ten byl deklarován na jako první statická konstanta této třídy viz výpis 5.1, řádek 27.) Prozatím je v ní prázdný odkaz, protože jsme ještě žádnou hru nedefinovali. Až ji začneme definovat, ihned tuto definici opravíme. Po vytvoření rodičovského podobjektu (výpis 5.3, řádek 69) jej konstruktor požádá o zařazení dvou povinných scénářů (řádky 71 až 74) a výsledek zalepí (řádek 75). Tím je správce připraven k použití. Hlavní metoda Výpis 5.3 končí definicí hlavní metody. V ní jsou připraveny příkazy pro otestování různých částí vytvářeného projektu. Ve výpisu je většina z nich zakomentovaná. Živý je pouze příkaz žádající vytvořeného správce scénářů, aby se sám otestoval (výpis 5.3, řádek 139). Spustíme-li správce scénářů z výpisů 5.1 až 5.3, získáme výstup, který najdete ve výpisu 5.4. (Abyste viděli, jak vypadá zpráva o chybě, zakomentoval jsem na konci chybového scénáře před krokem s vyvoláním nápovědy tři kroky testující reakci hry, kdy uživatel zapomene zadat parametr.) Na začátku výpisu (řádky 2 6) se dozvíte, jaké jsou požadavky na rozměry úspěšného scénáře a za nimi pak následuje oznámení čí správce je testován, úplný název testované třídy a zpráva prvního kroku, v níž by měla hra oznámit svůj základní námět. Následuje a hrubý popis toho, na co testovací program přišel. Strana 96 z 351

97 Kapitola 5: Návrh správce scénářů konkrétní hry 97 Výpis 5.4: Výpis autotestu ukázkového správce scénářů z výpisů 5.1 až 5.3 se zakomentovanými kroky testujícími v chybovém scénáři reakci za zadání příkazů bez parametrů 1 Verze frameworku: Minimální požadované "rozměry" úspěšného scénáře: 3 Minimální počet kroků = 10 4 Minimální počet prostorů hry = 6 5 Minimální počet navštívených prostorů = 4 6 Minimální počet definic vlastních akcí = 4 7 Testuji správce scénářů autora: RUP999 PECINOVSKÝ Rudolf 8 Instance třídy: eu.pedu.adventure15p.rupscenariomanagerlit 9 ########## START: St :34: Uvítací zpráva: 12 =============== 13 Vítáme vás ve služebním bytě. Jistě máte hlad. 14 Najděte v bytě ledničku - tam vás čeká svačina ############################################################################# 17 Autor: PECINOVSKÝ Rudolf 18 Třída správce: class eu.pedu.adventure15p.rupscenariomanagerlit 19 Scénář: _HAPPY_ 20 ===== Start testu ===== : tsstart tsmove - jdi koupelna tspick_up - vezmi brýle tspick_up - vezmi časopis tsmove - jdi předsíň tsmove - jdi obývák tsmove - jdi kuchyň tsnon_standard1 - otevři lednička tsbag_full - vezmi papír tsput_down - polož časopis tspick_up - vezmi papír tsnon_standard1 - přečti papír tsnon_standard1 - nasaď brýle tsnon_standard1 - přečti papír tspick_up - vezmi časopis tsnon_standard2 - podlož lednička časopis tsput_down - polož papír tsnon_standard2 - podlož lednička časopis tsnon_standard1 - otevři lednička tsunmovable - vezmi pivo tsdialog tsdialog tsnon_standard1 - zavři lednička tsend - konec 45 ===== Konec testu ===== 46 Kroků testu: 24 - vyhovuje 47 Vlastních akcí: 5 - vyhovuje [NASAĎ, OTEVŘI, PODLOŽ, PŘEČTI, ZAVŘI] Strana 97 z 351

98 Kapitola 5: Návrh správce scénářů konkrétní hry Zmíněno prostorů: 6 - vyhovuje [koupelna, kuchyň, lednička, ložnice, obývák, předsíň] 49 Z toho navštíveno: 5 - vyhovuje [koupelna, kuchyň, lednička, obývák, předsíň] 50 Zadané akce: 9 - [jdi, konec, nasaď, otevři, podlož, polož, přečti, vezmi, zavři] 51 Neprovedených typů kroků: 13 - [tshelp, tsempty, tsunknown, tsmove_wa, tspick_up_wa, tsput_down_wa, tsbad_neighbor, tsbad_item, tsnot_in_bag, tsnon_standard0, tsnon_standard3, tsdemo, tsnot_set] 52 Z toho povinných: 0 - [] 53 Nenavštívené prostory: [ložnice] 54 Zmíněné h-objekty: [botník, brýle, deštník, houska, lednička, papír, pivo, rum, salám, televize, umyvadlo, víno, časopis] 55 ===== Test ukončen 56 Autor: PECINOVSKÝ Rudolf 57 Třída správce: class eu.pedu.adventure15p.rupscenariomanagerlit 58 Scénář: _HAPPY_ 59 ===== Scénář vyhověl 60 ############################################################################# ############################################################################# 64 Autor: PECINOVSKÝ Rudolf 65 Třída správce: class eu.pedu.adventure15p.rupscenariomanagerlit 66 Scénář: _MISTAKE_ 67 ===== Start testu ===== : tsnot_start - Start tsstart tsunknown - maso tsempty tspick_up - vezmi deštník tsmove - jdi koupelna tsbad_neighbor - jdi záchod tsbad_item - vezmi koupelna tsunmovable - vezmi umyvadlo tsnot_in_bag - polož papír tspick_up - vezmi brýle tsbag_full - vezmi Časopis tshelp -? tsend - konec 82 Nepokryté typy akcí: [tsmove_wa, tspick_up_wa, tsput_down_wa] 83 ===== Konec testu ===== 84 Kroků testu: Vlastních akcí: 5 - [NASAĎ, OTEVŘI, PODLOŽ, PŘEČTI, ZAVŘI] 86 Zmíněno prostorů: 4 - [koupelna, ložnice, obývák, předsíň] 87 Z toho navštíveno: 2 - [koupelna, předsíň] 88 Zadané akce: 5 - [?, jdi, konec, polož, vezmi] 89 Neprovedených typů akcí: 11 - [tsput_down, tsmove_wa, tspick_up_wa, tsput_down_wa, tsnon_standard0, tsnon_standard1, tsnon_standard2, tsnon_standard3, tsdialog, tsdemo, tsnot_set] 90 Z toho povinných: 3 - [tsput_down_wa, tspick_up_wa, tsmove_wa] 91 Nenavštívené prostory: [ložnice, obývák] Strana 98 z 351

99 Kapitola 5: Návrh správce scénářů konkrétní hry Zmíněné h-objekty: [botník, brýle, deštník, umyvadlo, časopis] 93 ===== Test ukončen 94 Autor: PECINOVSKÝ Rudolf 95 Třída správce: class eu.pedu.adventure15p.rupscenariomanagerlit 96 Scénář: _MISTAKE_ 97 ===== Scénář NEVYHOVĚL 98 ############################################################################# ########## STOP: St :34: Trvání testu 252 ms Test scénáře byl NEÚSPĚŠNÝ 102 Konec testu scénářů autora: RUP999 PECINOVSKÝ Rudolf ############################################################################# 107 Správce scénářů NEPROŠEL testy - nejméně jeden scénář NEVYHOVĚL! 108 ############################################################################# Jak se můžete ve výpisu přesvědčit, úspěšný scénář testu vyhověl, ale neúspěšný ne. Příčina tohoto nevyhovění je oznámena na řádku 90, kde se můžete dozvědět, že scénář neobsahuje kroky tří povinných typů konkrétně testy toho, jak bude hra reagovat, když hráč zapomene zadat, kam se má přesunout, resp. co se má zvednout, resp. co se má položit. 5.4 Trochu chytřejší správce Výše uvedená definice má jednu nevýhodu: kdykoliv se budeme chtít ve hře odkázat na některý z textů ze scénáře, budeme muset tento text zkopírovat. Pokud jej budeme chtít později změnit (najdeme v něm překlep nebo se rozhodneme pro lepší formulaci), budeme muset najít všechny jeho výskyty a tuto opravu do nich zanést. Testovací program navíc nebude spokojen s naším řešením ani tehdy, když někde v textu posílaných zpráv a názvů pojmenovaných objektů (akcí, prostorů a h-objektů) uděláme nějaký, byť malý překlep. Někde přehlédneme nenápadnou mezeru a budeme se divit, co že je na našem programu špatně. Programátorsky výhodnější řešení je proto definovat (nejlépe v odděleném zdrojovém souboru) sadu textových konstant a nahradit ve správci scénářů textové literály těmito konstantami. Scénář tím sice trochu ztratí na své přehlednosti, ale na druhou stranu budeme moci v budoucnu měnit texty vždy jenom na jediném místě v definici příslušné konstanty. Uvedené řešení má další výhodu: rozhodnete-li se v budoucnu program lokalizovat do jiného jazyka, bude stačit změnit pouze třídu definující jednotlivé konstanty a zbytek programu může zůstat jaký je. Strana 99 z 351

100 Kapitola 5: Návrh správce scénářů konkrétní hry Definice třídy Texts Definici třídy Texts definující jednotlivé textové konstanty pro naši hru si můžete prohlédnout ve výpisu 5.5. Výpis 5.5: Definice třídy Texts definující jednotlivé textové konstanty 1 /******************************************************************************* 2 * Knihovní třída {@code Texts} slouží jako schránka na textové konstanty, 3 * které se používají na různých místech programu. 4 * Centralizací definic těchto textových řetězců lze nejsnadněji dosáhnout toho, 5 * že texty, které mají být shodné na různých místech programu, 6 * budou doopravdy shodné. 7 */ 8 class Texts 9 { 10 //== CONSTANT CLASS ATTRIBUTES ================================================= /** Jméno autora programu. */ 13 static final String AUTHOR_NAME = "PECINOVSKÝ Rudolf"; /** Xname autora programu. */ 16 static final String AUTHOR_ID = "PECR999"; /** Názvy používaných prostorů - místností. */ 19 static final String 20 PŘEDSÍŇ = "Předsíň", 21 LOŽNICE = "Ložnice", 22 OBÝVÁK = "Obývák", 23 KOUPELNA= "Koupelna", 24 KUCHYŇ = "Kuchyň"; /** Názvy používaných h-objektů. */ 28 static final String 29 BOTNÍK = "Botník", 30 DEŠTNÍK = "Deštník", 31 BRÝLE = "Brýle", 32 UMYVADLO= "Umyvadlo", 33 TELEVIZE= "Televize", 34 ČASOPIS = "Časopis", 35 LEDNIČKA= "Lednička", 36 PAPÍR = "Papír", 37 PIVO = "Pivo", 38 RUM = "Rum", 39 SALÁM = "Salám", 40 HOUSKA = "Houska", 41 VÍNO = "Víno", 42 POSTEL = "Postel", 43 ZRCADLO = "Zrcadlo", 44 ŽUPAN = "Župan"; Strana 100 z 351

101 Kapitola 5: Návrh správce scénářů konkrétní hry /** Názvy používaných akcí. */ 48 static final String 49 phelp = "?", 50 pjdi = "Jdi", 51 pnasaď = "Nasaď", 52 potevři = "Otevři", 53 ppodlož = "Podlož", 54 ppolož = "Polož", 55 ppřečti = "Přečti", 56 pvezmi = "Vezmi", 57 pzavři = "Zavři", 58 pkonec = "Konec"; /** Formát dodatku zprávy informujícího o aktuálním stavu hráče. */ 62 static final String 63 SOUSEDÉ = "Sousedé: ", 64 H-OBJEKTY = "H-objekty: ", 65 BATOH = "Batoh: ", 66 FORMÁT_INFORMACE = "\n\nnacházíte se v místnosti: %s" + 67 "\n" + SOUSEDÉ + "[%s]" + 68 "\n" + H-OBJEKTY + "[%s]" + 69 "\n" + BATOH + "[%s]"; /** Texty zpráv vypisovaných v reakci na příkazy vyvolávají povinné akce. 73 * Počáteční z (zpráva) slouží k odlišení od stavů. */ 74 static final String 75 znení_start = "\nprvním příkazem není startovací příkaz." + 76 "\nhru, která neběží, lze spustit pouze startovacím příkazem.\n", zporadím = "\nchcete-li poradit, zadejte příkaz?", 79 zprázdný_příkaz = "\nzadal(a) jste prázdný příkaz." + zporadím, 80 zneznámý_příkaz = "\ntento příkaz neznám." + zporadím, zanp = "\nzadaná akce nebyla provedena", zpřesun = "\npřesunul(a) jste se do místnosti: ", 85 zcil_nezadan = zanp + "\nnebyla zadána místnost, do níž se má přejít", 86 znení_cil = zanp + "\ndo zadané místnosti se odsud nedá přejít", zzvednuto = "\nvzal(a) jste h-objekt: ", 89 zpoloženo = "\npoložil(a) jste h-objekt: ", 90 zh-objekt_nezadan = zanp + "\nnebyl zadán h-objekt, s nímž mám manipulovat", 91 ztěžký_h-objekt = zanp + "\nzadaný h-objekt nejde zvednout: ", 92 znení_h-objekt = zanp + "\nzadaný h-objekt v místnosti není: ", 93 znení_v_batohu = zanp + "\nh-objekt není v batohu: ", 94 zbatoh_plný = zanp + 95 "\nzadaný h-objekt nemůžete vzít, máte už obě ruce plné", 96 Strana 101 z 351

102 Kapitola 5: Návrh správce scénářů konkrétní hry znápověda = "\npříkazy, které je možno v průběhu hry zadat:" + 98 "\n============================================\n", zuvítání = 101 "\nvítáme vás ve služebním bytě. Jistě máte hlad." "\nnajděte v bytě ledničku - tam vás čeká svačina.", zcelé_uvítání = zuvítání String.format(FORMÁT_INFORMACE, 106 PŘEDSÍŇ, cm(ložnice, OBÝVÁK, KOUPELNA), 107 cm(botník, DEŠTNÍK), cm()), zkonec = "\nkonec hry. \nděkujeme, že jste zkusil(a) naši hru."; /** Texty vypisované v reakci na příkazy vyvolávající nepovinné akce. */ 113 static final String 114 zlednice_nejde_otevřít = 115 "\nlednička nejde otevřít. Na ledničce leží nějaký popsaný papír.", zchce_přečíst = 118 "\nrozhodl(a) jste se přečíst ", znemá_brýle = "." "\ntext je psán příliš malým písmem, které je rozmazané." "\nmusíte si nasadit brýle.", znasadil_brýle = 125 "\nnasadil(a) jste si brýle.", znapsáno_papír = "." "\nna papíru je napsáno:" "\nlednička stojí nakřivo, a proto jde špatně otevírat." "\nbudete-li mít problémy, něčím ji podložte.", zchce_podložit = 133 "\nrozhodl(a) jste se podložit h-objekt ", zh-objektem = 136 " h-objektem ", znelze_nadzvednout = 139 "\nbohužel máte obě ruce plné a nemáte ledničku čím nadzvednout.", zlednička_podložena = 142 "\nlednička je úspěšně podložena - nyní by již měla jít otevřít.", zotevřel_ledničku = 145 "\núspěšně jste otevřel(a) ledničku.", zbere_alkohol = 148 "\npokoušíte si vzít z inteligentní ledničky ", Strana 102 z 351

103 Kapitola 5: Návrh správce scénářů konkrétní hry zkolik_let = 151 "\ntoto je inteligentní lednička, která neumožňuje " "\npodávání alkoholických nápojů mladistvým." "\nkolik je vám let?", znarozen = 156 "\nv kterém roce jste se narodil(a)?", zodebral = 159 "\nvěřím vám a předávám vám požadovaný nápoj." "\nodebral(a) jste z ledničky: ", znezapomeň = 163 "\ndobrou chuť. Nezapomeňte zavřít ledničku.", zzavřel_ledničku = 166 "\núspěšně jste zavřel(a) ledničku."; //== VARIABLE CLASS ATTRIBUTES ================================================= //############################################################################## 175 //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== 176 //== CLASS GETTERS AND SETTERS ================================================= 177 //== OTHER NON-PRIVATE CLASS METHODS =========================================== /*************************************************************************** 180 * Vrátí řetězec obsahující zadané názvy oddělené čárkami. 181 * 182 názvy Názvy, které je třeba sloučit 183 Výsledný řetězec ze sloučených zadaných názvů 184 */ 185 static String cm(string... názvy) 186 { 187 String result = Arrays.stream(názvy) 188.collect(Collectors.joining(", ")); 189 return result; 190 } //== PRIVATE AND AUXILIARY CLASS METHODS ======================================= //############################################################################## 199 //== CONSTANT INSTANCE ATTRIBUTES ============================================== 200 //== VARIABLE INSTANCE ATTRIBUTES ============================================== Strana 103 z 351

104 Kapitola 5: Návrh správce scénářů konkrétní hry //############################################################################## 205 //== CONSTUCTORS AND FACTORY METHODS =========================================== /** Soukromý konstruktor zabraňující vytvoření instance.*/ 208 private Texts() {} //== ABSTRACT METHODS ========================================================== 213 //== INSTANCE GETTERS AND SETTERS ============================================== 214 //== OTHER NON-PRIVATE INSTANCE METHODS ======================================== 215 //== PRIVATE AND AUXILIARY INSTANCE METHODS ==================================== //############################################################################## 220 //== NESTED DATA TYPES ========================================================= 221 } 5.6 Definice správce scénářů využívajícího konstanty Správce scénářů využívající konstanty se bude od správce používajícího textové literály lišit pouze v konkrétní podobě zadání textů v definicích kroků scénáře (viz definice prvních tří kroků úspěšného scénáře ve výpisu 5.6). Jinak budou oba stejné. Výpis 5.6: Definice prvních tří kroků úspěšného scénáře ve třídě ManagerWithConstants 1 /*************************************************************************** 2 * Počáteční krok hry, který je pro všechny scénáře shodný. 3 */ 4 private static final ScenarioStep START_STEP = 5 new ScenarioStep(0, tsstart, "", // Spouštěcí příkaz 6 zuvítání //zcelé_uvítání 7, 8 PŘEDSÍŇ, 9 new String[] { LOŽNICE, OBÝVÁK, KOUPELNA }, 10 new String[] { BOTNÍK, DEŠTNÍK }, 11 new String[] {} 12 ); /*************************************************************************** 16 * Kroky základního úspěšného scénáře 17 * popisující očekávatelný úspěšný průběh hry. 18 * Z těchto kroků sestavený scénář nemusí být nutně nejkratším možným Strana 104 z 351

105 Kapitola 5: Návrh správce scénářů konkrétní hry * (takže to vlastně ani nemusí být základní úspěšný scénář), 20 * ale musí vyhovovat všem okrajovým podmínkám zadání, 21 * tj. musí obsahovat minimální počet kroků, 22 * projít požadovaný.minimální počet prostorů 23 * a demonstrovat použití všech požadovaných akcí. 24 */ 25 private static final ScenarioStep[] HAPPY_SCENARIO_STEPS = 26 { 27 START_STEP 28, 29 new ScenarioStep(tsMOVE, pjdi + " " + KOUPELNA, 30 zpřesun + KOUPELNA 31, 32 KOUPELNA, 33 new String[] { PŘEDSÍŇ }, 34 new String[] { BRÝLE, UMYVADLO, ČASOPIS }, 35 new String[] {} 36 ) 37, 38 new ScenarioStep(tsPICK_UP, pvezmi + " " + BRÝLE, 39 zzvednuto + BRÝLE 40, 41 KOUPELNA, 42 new String[] { PŘEDSÍŇ }, 43 new String[] { UMYVADLO, ČASOPIS }, 44 new String[] { BRÝLE } 45 ) 46, Jak jsem již naznačil, takto koncipovaný správce je trochu univerzálnější a lze jej snadněji přizpůsobovat různým měnícím se požadavkům. 5.7 Prověrka ekvivalence obou správců Ekvivalenci obou zmíněných správců prověříte jednoduše. Stačí u každého z nich spustit autotest a porovnat obdržené výsledky např. tak, že každý z nich uložíte do souboru a obsah těchto souborů pak necháte porovnat nějakým programem. Většina lepších vývojových prostředí tuto možnost nabízí 13. Když už prověříte výsledky autotestu, můžete totéž provést se simulacemi hry podle základního úspěšného scénáře. Výsledky by opět měly být pro oba správce stejné. 13 Jak se to dělá u NetBeans jsem vysvětloval v první části publikace [7]. Strana 105 z 351

106 Kapitola 5: Návrh správce scénářů konkrétní hry Tovární třída v doprovodném projektu Tovární třída je definována i v doprovodném projektu A105z_ScenarioManager shrnujícím výsledky této kapitoly. Jmenuje se RUPGSMFactory a požadovaná metoda getscenariomanager() je v ní naprogramována tak, abyste si zakomentováním, resp. odkomentováním příslušného příkazu mohli vybrat, kterého správce scénářů bude metoda vracet. Její definici si můžete prohlédnout ve výpisu 5.7. Výpis 5.7: Definice metody getscenariomanager() ve třídě RUPGSMFactory 1 /*************************************************************************** 2 * Vrátí odkaz na instanci správce scénářů. 3 * 4 Požadovaný odkaz 5 */ 7 public AScenarioManager getscenariomanager() 8 { 9 return RUPScenarioManagerCon.getInstance(); 10 // return RUPScenarioManagerLit.getInstance(); 11 } 5.9 Úkol Takže po celé té předehře se konečně dostáváme k prvnímu skutečnému úkolu (ten předchozí byl pouze zahřívací ). Zkuste nyní vymyslet vlastní námět takovéto hry a definujte její úspěšný a chybový scénář vyhovující dále popsaným omezujícím podmínkám. Omezující podmínky jsou následující: Při postupu podle úspěšného scénáře musí hráč dosáhnout požadovaného cíle hry. V prostorech musí být h-objekty, z nichž některé je možno zvednout a jiné zase ne. (Nemusí být ve všech prostorech.) Hra musí definovat následující akce: Startovací akci, jejíž název je prázdný řetězec. Prostý přechod z prostoru do sousedního prostoru. Zvednutí h-objektu, tj. jeho odebrání z aktuálního prostoru a uložení v batohu. Strana 106 z 351

107 Kapitola 5: Návrh správce scénářů konkrétní hry 107 Položení h-objektu, tj. jeho přesun z batohu do aktuálního prostoru. Předčasné ukončení hry. Nápověda. Vaše vlastní akce. Kroky, v nichž budou v testovacích scénářích tyto akce vyvolány, budou typu tsnon_standardx, kde x zastupuje číslici, která udává počet parametrů. Všechna použití dané akce musí mít vždy stejný počet parametrů. V úspěšném scénáři musí být povinně použity akce pro přesun a akce pro zvednutí a položení h-objektu. Úspěšný scénář musí mít jistý minimální rozměr specifikovaný veřejnou konstantou AScenarioManager.LIMITS, která je instancí třídy Limits. Tento minimální rozměr se dozvíte např. tak, že konstantu vytisknete. Pak se můžete dozvědět např. že: Minimální požadované "rozměry" úspěšného scénáře: Minimální počet kroků = 10 Minimální počet prostorů hry = 6 Minimální počet navštívených prostorů = 4 Minimální počet definic vlastních akcí = 4 Hra musí být schopna se korektně vypořádat s nesprávně zadanými příkazy uživatele. V chybovém scénáři se proto musejí vyskytovat všechny typy kroků, jejichž konstruktoru je ve výčtovém typu TypeOfStep zadána v prvním parametru hodnota 1 (kroky s podtypem sthelpstop) nebo 2 (kroky s podtypem stmistake). V chybovém scénáři proto musíte prověřit správné reakce na: Pokus o odstartování hry jiným než startovacím příkazem Zadání příkazu pro neexistující akci. Zadání prázdného příkazu uprostřed hry Zadání akcí vyžadujících parametr bez tohoto parametru. Příkaz k přechodu do prostoru, který neexistuje anebo v daný okamžik není přístupným sousedem aktuálního prostoru. Příkaz k zvednutí h-objektu, který se v aktuálním prostoru nevyskytuje. Příkaz k zvednutí nepřenositelného h-objektu. Příkaz k položení h-objektu, který nemáte v batohu. Příkaz ke zvednutí h-objektu, který se již nevejde do batohu. Kromě výše uvedených povinných součástí můžete do svého návrhu vložit řadu dalších akcí a nápadů. Fantazii a tvořivosti se meze nekladou. Strana 107 z 351

108 Kapitola 5: Návrh správce scénářů konkrétní hry 108 Dodatečný požadavek k usnadnění testů Jak si jistě domyslíte z následujícího stručného přehledu, náměty her mohou být velice rozmanité. Aby vám mohl framework pomoci s otestováním vaší hry, potřeboval by mít alespoň nějaké záchytné body. Proto jsou na váš svět hry kladeny následující okrajové podmínky: Výchozí prostor, tj. prostor, v němž se hra vždy začíná, musí mít alespoň jednoho souseda, do kterého je možno se ihned bez problému přesunout. Tento prostor nemusí nijak souviset s koncepcí hry. Může to být nějaký pomocný prostor (třeba kumbál, slepá ulice, opuštěná planeta, ), jehož jediným účelem je, aby váš svět hry vyhověl tomuto požadavku. V tomto pomocném prostoru musí být nějaký přenositelný h-objekt, tj. h-objekt, který je možno vzít do batohu. Není nutné, aby se tento h-objekt do batohu vešel. Bude-li váš batoh na počátku hry plný, testovací program jej může nejprve vyprázdnit, a teprve pak se pokusit onen h-objekt zvednout. Náměty S jedním velmi stručným přehledem námětů jste se potkali již v podkapitole 1.2 Příklady her na straně 33. Pro vaši inspiraci uvádím několik dalších nápadů tak, jak jsem je posbíral z odevzdaných studentských prací, v nichž jsem opravil pouze některé do nebo volající gramatické chyby: Po prohýřené noci se probouzíš zamčený(ná) v cizím bytě. Nejprve musíš najít své svršky, pak telefonní číslo majitele bytu, jemuž zavoláš. Od něj se dozvíš, kde jsou klíče, aby ses dostal(a) ven. Jsi před tajemným hradem, v němž je uvězněna princezna, kterou hlídá drak. Musíš získat zbroj, dostat se do hradu, najít a přemoci draka a pak najít a osvobodit princeznu. Jsi myš. Tvým úkolem je dostat se i s dětmi do bezpečí spíže nedalekého domu a nenechat se přitom chytit a sníst kocourem. Jsi člen týmu SG-2 a byl jsi poslán na akci na neznámou planetu. Prošel jsi s týmem hvězdnou bránou, ale něco se pokazilo. Dostal ses jinam než zbytek týmu. Probouzíš se v temné jeskyni. Rány ti ošetřuje medvědu podobný tvor a žvatlá něco nesrozumitelného. Asi jsi někde nechal překladač. Říkáš si: Musím se nějak dostat zpět domů, ale neznám hvězdnou adresu na Zemi! Zrovna jste se vracel do kajuty, když se loď, na jejíž palubě jste, začala potápět. Musíte se dostat přes všechny překážky na palubu. Strana 108 z 351

109 Kapitola 5: Návrh správce scénářů konkrétní hry 109 Vítej ve hře GeoCaching. Tvým úkolem je najít 3 části souřadnic, ze kterých vypočítáš souřadnice cílové oblasti. Abys ale hru dohrál, musíš tam položit 2 mince, které musíš najít cestou. Navíc se tam musíš podepsat, k čemuž budeš potřebovat pero. Právě jsi v ložnici u své milenky. Najednou slyšíš, že domů přišel její manžel. Je čas jít domů a oslavit s manželkou výročí svatby. Toto je nová, detektivní adventura. Vyřešte záhadnou krádež zlatých cihel. Byl jsi právě nespravedlivě odsouzen. Tvým úkolem je získat obnos k podplacení dozorců a uprchnout z vězení. Jack sa jedno ráno zobudil na tom najdivnejšom mieste, na okraji malého lesíku, vôbec si nepamätal, ako sa tam dostal, vlastne ani nevedel že kde je. Pomôžete mu dostať sa domov? Máš velké finanční problémy. Přitom tvůj dům sousedí s bankou, jejíž trezor je hned vedle tvého sklepa. Pokus se o víkendu banku vyloupit. Tvé město obsazují turisté jako kobylky. Je třeba se jich jednou provždy zbavit. Není zbytí. Je třeba sehnat jelena a ukázat všem, kdo je tady pánem. Na město padne děs Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Zopakujme si, co jsme v této kapitole dozvěděli a co jsme naprogramovali. Ke startovacímu projektu, který jsme shrnuli v podkapitole 2.9 Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali na straně 49, jsme přidali následující: V ukázkovém projektu máme definované tři třídy správců scénářů: Třídu RUPScenarioManagerLit využívající k definici textů literály. Třídu RUPScenarioManagerCon využívající k definici textů textové konstanty. Protože jsme se rozhodli (dobrá, nedal jsem vám na výběr, já se rozhodl) preferovat používání konstant definovaných v samostatné třídě a zjednodušit si tak opravy případných chyb, je v závěrečném projektu kapitoly definována i třída Texts s příslušnými textovými konstantami. Třídu RUPScenarioManager, která umožňuje na poslední chvíli zadat, kterou z výše jmenovaných tříd v daném běhu použijeme. Strana 109 z 351

110 Kapitola 5: Návrh správce scénářů konkrétní hry 110 Protože jsme se rozhodli (dobrá, nedal jsem vám na výběr, já se rozhodl) používat ve scénářích konstanty a zjednodušit si tak opravy případných chyb, definovali jsme i třídu s textovými konstantami třídu Texts. Definovali jsme tovární třídu, jejíž instance fungují jako tovární objekty zprostředkující získání instancí správce scénářů a vlastní hry třídu GSMFactory. Program, jehož zdrojové kódy odpovídají výše popsanému stavu vývoje aplikace, najdete v projektu A105z_ScenarioManager. Jeho diagram tříd si můžete prohlédnout na obrázku 5.1. Obrázek 5.1 Diagram tříd projektu A105z_ScenarioManager, zachycujícího stav po definici dvou verzí správců scénářů Strana 110 z 351

111 Kapitola 6: Sjednocený správce scénářů Sjednocený správce scénářů Kapitola 6 Sjednocený správce scénářů Co se v kapitole naučíte V této kapitole se seznámíme s návrhovým vzorem Strategie a ukážeme si dvě cesty, jak je možno zařídit, aby se až za chodu programu dalo rozhodnout o tom, které z možných řešení se použije. Studium kapitoly není k zvládnutí zadané úlohy nutné. Je určena spíše pro zájemce, kterým na příkladu našeho programu vysvětlím, jak je možno řešit některé problémy, s nimiž se mohu v praxi setkat. Nikomu ale tento rozbor nenutím koho nezajímá, může klidně tuto kapitolu přeskočit. V této kapitole budou ukázková řešení dále rozšiřovat projekt A105z_ScenarioManager obsahující závěrečnou podobu projektu z minulé kapitoly. 6.1 Řešený problém Když svým studentům vysvětluji rozdíly mezi správcem scénářů využívajícím literály a správcem využívajícím konstanty, většinou se rozdělí tak, že zhruba třetina dá přednost konstantám a dvě třetiny dají přednost literálům. Abych uspokojil obě skupiny, rozhodl jsem se definovat univerzálního správce, u nějž bude možno způsob definice textových konstant zvolit. Abyste pak rozdíl v používání viděli, budu v následné definici hry používat na některých místech literály (bude jich málo, ale budou) a na jiných konstanty. V této kapitole vám ukážu, jak lze takovéto dva přístupy sjednotit. S obdobnou situací se občas setkáte i v praxi, kde se při tom občas hovoří o aplikaci návrhové- Strana 111 z 351

112 Kapitola 6: Sjednocený správce scénářů 112 ho vzoru Strategie viz podšeděný rámeček. Pojďme tedy pokračovat v analýze problému s těmi, které dané řešení zajímá. Jak si můžete přečíst v rámečku, náš problém můžeme řešit aplikací návrhového vzoru Dekorátor nebo aplikací dědění. Vyzkoušíme si postupně každý z nich. Návrhový vzor Strategie (Strategy) Řadu operací lze provádět více způsoby, přičemž žádný z nich není možno prohlásit za obecně nejlepší. Výhodnost či nevýhodnost každého z nich záleží na aktuální situaci. Návrhový vzor Strategie doporučuje pro takovéto situace definovat rodinu algoritmů, každý z nich zapouzdřit do samostatného objektu a nastavit je jako záměnné. Klienti pak mohou zaměňovat používané algoritmy (volit aktuální strategii) podle okamžité potřeby. Abychom to mohli provést, tak všechny objekty reprezentující ony záměnné algoritmy musejí mít společné rozhraní, prostřednictvím něhož se na ně budou okolní objekty obracet. Jejich definiční třídy proto musejí mít společného předka nebo musejí implementovat společný interfejs. Tento požadavek můžeme řešit dvěma způsoby, a to buď aplikací dědění, anebo aplikací návrhové vzoru Dekorátor. Ti, kteří četli učebnici [7], vědí, že dědění je vlastně jenom aplikací návrhového vzoru Dekorátor zabudovanou do syntaxe daného jazyka. Umožňuje výrazně jednodušší a přehlednější zápis některých operací, ale na druhou stranu nám zužuje mantinely, v nichž se můžeme pohybovat, takže stále existují situace, kdy se použití vzoru Dekorátor výhodnější. 6.2 Použití návrhového vzoru Dekorátor Začněme univerzálnějším dekorátorem. V dalším budu předpokládat, že používáte vývojové prostředí NetBeans. Ostatní profesionální prostředí nabízejí obdobnou funkčnost, v případě jednodušších vývojových prostředí (např. BlueJ) budete muset provést příslušné operace ručně. Chcete-li si vyzkoušet dále probírané postupy na vlastním preparátu, zkopírujte svůj projekt a zkuste v něm pokračovat podle postupu postupně probíraném ve zbytku kapitoly. Strana 112 z 351

113 Kapitola 6: Sjednocený správce scénářů 113 Vytvoření dekorující třídy Víme, že správce scénářů musí být potomkem třídy AScenarioManager. Budeme proto dekorovat její instance. 1. Otevřeme třídu některého z dříve definovaných (a nyní slučovaných) správců scénářů. 2. V její hlavičce rozbalíme místní nabídku jejího rodiče třídy AScenarioManager (klepněte na její název pravým tlačítkem myši nebo na ni najeďte textovým kurzorem z klávesnice a stiskněte příslušnou klávesovou zkratku ve Windows SHIFT+F10). 3. V rozbalené místní nabídce zadejte Refactor Introduce Local Extensions. Otevře se dialogové okno na obrázku 6.1. Obrázek 6.1: Dialogové okno Introduce Local Extension 4. Jak vidíte, dialogové okno vám umožňuje změnit i projekt, strom složek i balíček nově definované třídy. Vy ale nic takového dělat nebudete a pouze změníte název třídy. Já jej nastavím na RUPScenarioManagerD. Přizpůsobení základním požadavkům Naše nově definovaná třída musí vyhovovat základním požadavkům kladeným na správce scénářů. Pojďme se tedy podívat, jak je splnit co může zůstat a co bychom měli změnit. 5. Otevřete zdrojový kód právě definované třídy. 6. První změna se bude týkat již hlavičky třídy. U mne má tvar: Strana 113 z 351

114 Kapitola 6: Sjednocený správce scénářů 114 public class RUPScenarioManagerD implements IScenarioManager My ale víme, že třída musí být potomkem třídy AScenarioManager. Upravíme ji proto do podoby public class RUPScenarioManagerD extends AScenarioManager 7. Za hlavičkou je definice statické konstanty LIMITS. Tu můžeme smazat, protože nyní tuto konstantu podědíme. 8. Totéž provedeme se statickou metodou extend. O té jsme dokonce doposud ani nehovořili, protože její definice ve třídě AScenarioManager byla vyvolána jakýmisi požadavky testovacích programů, o nichž budu hovořit až ve třetím dílu. 9. Definici atributu delegáta prozatím přeskočím a přejdu rovnou k definici konstruktoru. Protože v zadání máme předepsáno, že správce scénářů musí být definován jako jedináček 14, musí být konstruktor definován jako soukromý. Změníme tedy jeho atribut přístupu. 10. Nyní ale musíme vymyslet alternativní způsob, jak zvenku zadat, kterého z našich správců zadat jako delegáta. Optimální by asi bylo, kdybychom příslušný atribut definovali jako statický (k záměně dojít nemůže, protože třída bude mít jedinou instanci) a definovali pak statickou metodu, která umožní jej nastavit. Jednu z možných definic si můžete prohlédnout ve výpisu 6.1. Výpis 6.1: Definice statické nastavovací metody setdelegate(ascenariomanager) ve třídě RUPScenarioManagerD 1 /*************************************************************************** 2 * Nastaví budoucího delegáta, kterého bude konstruktor dekorovat. 3 * 4 originalmanager Dekorovaný správce scénářů 5 */ 6 public static void setdelegate(ascenariomanager originalmanager) 7 { 8 if (MANAGER!= null) { 9 throw new IllegalStateException( 10 "\nsprávce scénářů již byl vyžádán, " + 11 "a proto již není možno delegáta měnit"); 12 } 13 delegate = originalmanager; 14 } Ve výpisu je použit statický atribut MANAGER, který byste měli znát již z definice vašeho správce scénářů. V něm je uložen odkaz na jedináčka. Dokud nebude vytvořen jedináček, je možno požadovanou dekorovanou třídu libovolně 14 Viz pasáž Správce by měl být jedináček na straně 68. Strana 114 z 351

115 Kapitola 6: Sjednocený správce scénářů 115 měnit. Jakmile je však instance jedináčka definována, už se na jeho delegáta nesmí sáhnout. Znalci multivláknového programování by vám řekli, že tato definice není korektní, protože nepočítá s možností, že s danou třídou bude komunikovat několik vláken současně. Vhledem k tomu, že píšu učebnici pro naprosté začátečníky, kteří ještě s vlákny nepracují, tak jsem v zájmu zjednodušení programu tuto možnost zanedbal. Možné korektní řešení najdete např. v [1] v kapitole o návrhovém vzoru Jedináček (Singleton). 11. Když jsme zesoukromnili konstruktor a definovali nastavování delegáta, mohli bychom se pustit do tovární metody. Najdete ji ve výpisu Jak si můžete všimnout, pokud do okamžiku jejího volání nebyl nastaven žádný delegát, metoda nastaví jako implicitního delegáta instanci správce scénářů používajícího konstanty. Navíc, protože je delegát uložen v atributu, nepotřebujeme konstruktor s parametrem, protože si jej může z tohoto atributu vyzvednout. Výpis 6.2: Definice statické tovární metody getinstance() ve třídě RUPScenarioManagerD 1 /*************************************************************************** 1 * Vrátí správce scénářů - jedinou instanci této třídy. 2 * 3 Požadovaný správce scénářů 4 */ 5 public static RUPScenarioManagerD getinstance() 6 { 7 if (MANAGER == null) { 8 if (delegate == null) { 9 delegate = RUPScenarioManagerCon.getInstance(); 10 } 11 MANAGER = new RUPScenarioManagerD(); 12 } 13 return MANAGER; 14 } Probírka definovaných instančních metod V předchozí pasáži jsme se zabývali především metodami patřícími objektu třídy, mezi něž řadíme statické metody a konstruktor. Nyní se podíváme na metody 15 Definice tovární metody opět není korektní (viz předchozí poznámka), ale alespoň je jednoduchá. Strana 115 z 351

116 Kapitola 6: Sjednocený správce scénářů 116 instancí. Ve zdrojovém kódu je mám seřazené podle abecedy a budu je i v tomto pořadí procházet. První z nich je metoda s hlavičkou: public final Collection<String> getallscenarionames() Její definice byla korektní do chvíle, než jsme vyměnili implementaci interfejsu IScenarioManager za dědění od třídy AScenarioManager. Tato třída totiž definuje inkriminovanou metodu jako konečnou. Podíváte-li se na definice metod této třídy, zjistíte, že jsou tak definované všechny s výjimkou metody tostring(). Je to proto, že tyto metody jsou používány v testech a není žádoucí, aby nějaký aktivní student měnil ve své dceřiné třídě jejich chování. Kdyby neplatil požadavek, aby všichni správci scénářů byly potomky třídy AScenarioManager (připomínám, že byl vyvolán snahou o optimalizaci následného testování a odbourání nutnosti toho, aby studenti museli programovat operace, které toto testování umožní a usnadní), byli bychom s prací téměř hotovi. Takto musíme nejprve obejít omezení vyvolané konečností většiny zděděných metod. Má-li být naše nově definovaná třída korektním potomkem, musí svému rodiči zprostředkovat všechny informace tak, aby jeho konečné metody fungovaly správně tak, jak jsou definovány. Jinými slovy: musíme definice všech zděděných metod smazat a začít se chovat jako správný potomek. 12. Smažeme definice všech metod, které jsou v rodičovské třídě definovány jako konečné. Když se podíváte na jejich účel, zjistíte, že se většinou týkají práce se scénáři. Tu proto budeme muset v příští etapě zprostředkovat. 13. Seznam zbylých metod začíná metodami getauthorid() a getauthorname(). Ty jsme ale v pasáži Implementace interfejsu IAuthorPrototype na straně 86 řešili implementací interfejsu IAuthorRUP, což můžeme udělat i nyní a definice metod smazat. 14. Obdobně metody gethappyscenario() a getmistakescenario() jsou implicitními metodami interfejsu IScenarioManager implementovaného naší rodičovskou třídou, takže i ty můžeme smazat. 15. Jako implicitní jsou v implementovaných interfejsech definované i metody foreach(consumer<? super Scenario>) a Spliterator<Scenario> spliterator(), takže mohou následovat své smazané předchůdce. 16. Zbývají metody equals(object) a hashcode(). Ty jsou tu ale evidentně přebytečné, protože instance dané třídy má být (a je) definována jako jedináček, takže můžeme klidně převzít definice těchto metod od třídy Object. 17. Poslední se tu na nás směje metoda tostring(). Uvědomíme-li si, že obě třídy, mezi jejichž instancemi budeme přepínat, přebírají definici této metody ze Strana 116 z 351

117 Kapitola 6: Sjednocený správce scénářů 117 své rodičovské třídy, můžeme je následovat a definici metody proto také smazat. Definice konstruktoru Po předchozí razii instančních metod vám v definici třídy zbyly jenom statické metody a instanční konstruktor. Definici obou statických metod jsme již vyřešili (viz výpisy 6.1 na straně 114 a 6.2 na straně 115). Zbývá tedy správně vyřešit definici konstruktoru. Při návrhu této definice musíme mít na paměti, že nám nezůstala v ruce jediná virtuální metoda, takže musíme rodičovské třídě předat všechny potřebné informace v konstruktoru obdobně, jak to dělaly naše dvě třídy, mezi nimiž chceme přepínat. Podíváte-li se na definici jejich konstruktoru (ve výpisu 5.3 na straně 92 je na řádcích 67 až 76), zjistíte, že musíme pouze předat class-objekt třídy hry a nechat pak vytvořit jednotlivé scénáře. Obojí získáme z dekorovaných objektů. Takže: 18. Upravíme definici konstruktoru podle výpisu 6.3. Výpis 6.3: Definice bezparametrického konstruktoru ve třídě RUPScenarioManagerD 1 /*************************************************************************** 2 * Vytvoří správce scénářů, který svému rodičovskému podobjektu předá 3 * class-objekt sdružené tovární třídy a seznam scénářů 4 * obdržené od dekorovaného objektu. 5 */ 6 private RUPScenarioManagerD() 7 { 8 super(delegate.getfactoryclass ()); 9 delegate.stream() 10.forEach(this::addScenario); 11 seal(); 12 } Definice metody main(string[]) Tím je definice dekorační třídy ukončena. (Oproti své původní podobě se výrazně zjednodušila, co? ) Zbývá nám ještě vše otestovat. K tomu bude nejlepší, když: 19. Zkopírujeme na příslušné místo dekorační třídy metodu main(string[]) některé z dříve definovaných tříd správce scénářů. 20. Upravíme definici metody main(string[]) podle výpisu 6.4. (Oproti původní definici zde přibyly pouze řádky 3 až 5.) Strana 117 z 351

118 Kapitola 6: Sjednocený správce scénářů 118 Výpis 6.4: Definice metody main(string[]) ve třídě RUPScenarioManagerD 1 public static void main(string[] args) 2 { 3 setdelegate(rupscenariomanagerlit.getinstance()); 4 setdelegate(rupscenariomanagercon.getinstance()); 5 getinstance(); 6 7 //Otestuje, zda správce scénářů a jeho scénáře vyhovují požadavkům 8 MANAGER.autoTest(); 9 10 //Vypíše na standardní výstup simulovaný průběh hry 11 //odehrané podle základního úspěšného scénáře 12 MANAGER.getHappyScenario().simulate(); //Testování hry prováděné postupně obou povinných scénářů, 15 //přičemž základní úspěšný scénář se prochází dvakrát za sebou 16 // MANAGER.testGame(); //Testování hry dle scénáře se zadaným názvem 19 // MANAGER.testGameByScenarios("???"); //Odehrání hry dle scénáře se zadaným názvem 22 // MANAGER.playGameByScenario("???"); System.exit(0); 25 } 21. Postupným zakomentováním a odkomentováním příkazů na řádcích 3 a 4 prověřte, že se třída chová korektně při všech kombinacích nastavení. Její kompletní výpis neukazuji, protože jste už definice všech metod viděli. Chcete-li vidět definici třídy vcelku, podívejte se do doprovodného projektu. 6.3 Použití dědění Jak už jsem naznačil, druhou možností je použít dědění, tj. definovat původní správce scénářů jako potomky jejich společného rodiče, kterým bude naše sjednocující třída, kterou nazveme RUPScenarioManagerH. Při definici řešení využívajícího dědění využijeme svých poznatků, které jsme získali při aplikaci vzoru Dekorátor. Pojďme tedy na to. Strana 118 z 351

119 Kapitola 6: Sjednocený správce scénářů 119 Vytvoření společné rodičovské třídy 1. Protože obě řešení budou do jisté míry podobná, začneme s tím, že vytvoříme kopii před chvílí definované třídy RUPScenarioManagerD a pojmenujeme ji RUPScenarioManagerH. 2. Připomínám, že třída RUPScenarioManagerH slouží pouze jako mezičlánek umožňující výběr konečného objektu. Aby nikoho nelákalo vytvořit její instanci, deklarujeme třídu jako abstraktní. 3. Upravíme definice původních správců scénářů RUPScenarioManagerLit a RUPScenarioManagerCon tak, aby se každý z nich vydával za potomka nově definované třídy RUPScenarioManagerH. To, že do linie dědění vložíme další člen, chování instancí daných tříd neovlivní alespoň do doby, než bychom v tomto mezičlenu upravili definici některé ze zděděných metod, jenomže k tomu se nechystáme. Budeme je tedy moci nadále využívat v původní podobě. Úprava konstruktoru V obou upravených definicích se objeví syntaktická chyba, protože nová rodičovská třída definuje pouze bezparametrický konstruktor, který je navíc soukromý. Napravíme to: 4. Změníme přístupová práva konstruktoru na protected. 5. Zavedeme parametr typu Class<? extends IGame>. 6. Upravíme tělo konstruktoru tak, že pouze předá obdržený parametr konstruktoru svého rodičovského podobjektu viz výpis 6.5. Výpis 6.5: Definice instančního konstruktoru ve třídě RUPScenarioManagerH 1 /*************************************************************************** 2 * Vytvoří rodičovský podobjekt správce scénářů dceřiné třídy, 3 * který svému rodičovskému podobjektu předá class-objekt 4 * sdružené tovární třídy. 5 * 6 gameclass Class-objekt třídy, pro jejíž instance jsou určeny 7 * spravované scénáře. 8 */ 9 protected RUPScenarioManagerH(Class<? extends IGSMFactory> factoryclass) 10 { 11 super(factoryclass); 12 } Strana 119 z 351

120 Kapitola 6: Sjednocený správce scénářů 120 Definice nastavení požadovaného potomka Nyní potřebujeme zařídit, aby bylo možno zadat, který ze správců scénářů budeme používat. Vyjdeme z definice zkopírované z dekorační třídy a lehce ji upravíme. Aby byla nějaká změna, tak budeme tentokrát přísnější a neumožníme jednou nastaveného potomka měnit. Tím navíc ušetříme pomocný atribut. 7. Protože již nenastavujeme delegáta, ale použitého potomka, přejmenujeme nastavovací metodu na setchild. 8. Upravíme definici nastavovací metody podle výpisu Odstraníme nepoužitý pomocný atribut delegate. Výpis 6.6: Definice statické nastavovací metody setchild(rupscenariomanagerh) ve třídě RUPScenarioManagerH 1 /*************************************************************************** 2 * Nastaví potomka, kterého bude vracet tovární metoda. 3 * 4 childmanager Nastavovaný potomek 5 */ 6 public static void setchild(rupscenariomanagerh childmanager) 7 { 8 if (MANAGER!= null) { 9 throw new IllegalStateException( 10 "\npožadovaný potomek již byl nastaven a není možno jej měnit"); 11 } 12 MANAGER = childmanager; 13 } Definice tovární metody Nyní zbývá pouze zařídit, aby tovární metoda getinstance() předala ten správný objekt. Opět vyjdeme ze zkopírované definice. 10. Upravíme tělo statické tovární metody getinstance() podle výpisu 6.7. Výpis 6.7: Definice statické tovární metody getinstance() ve třídě RUPScenarioManagerH 1 /*************************************************************************** 2 * Vrátí správce scénářů - jedinou instanci této třídy. 3 * 4 Požadovaný správce scénářů 5 */ 6 public static RUPScenarioManagerH getinstance() 7 { 8 if (MANAGER == null) { 9 MANAGER = RUPScenarioManagerCon.getInstance(); 10 } Strana 120 z 351

121 Kapitola 6: Sjednocený správce scénářů return MANAGER; 12 } Aby mohla statická tovární metoda předka spolehlivě vracet instanci některého z potomků dané třídy, musí být bezpodmínečně dodržen princip LSP (Liskov Substitution Principle) požadující, aby instance potomka vždy dokázaly plnohodnotně a bezproblémově vystupovat v roli instancí svého předka. Definice metody main(string[]) Pokud používáte profesionální vývojové prostředí a přejmenovali jste metodu setdelegate na setchild prostřednictvím refaktorace, nebudete muset definici zkopírované metody main(string[]) měnit. Při použití BlueJ byste museli změnit název volaných metod v metodě main(string[]) ručně. Při spouštění této třídy musíme jenom myslet na to, že tato třída neumožňuje opakované nastavení potomka, takže nejméně jedeno z volání nastavovací metody setchild musí být zakomentováno. Jiná změna potřeba není. 6.4 Úprava tovární třídy V pasáži Tovární třída v doprovodném projektu na straně 106 jsme definovali možnost výběru ze dvou scénářů tak, že jsme do těla metody getscenariomanager() vložili dva příkazy a jeden z nich pak vždy zakomentovali. Teoreticky bychom mohli rozšířit definici tovární třídy obdobným způsobem, ale to bychom museli před každou změnou znovu přeložit tovární třídu. Druhou možností je definovat statickou metodu, která umožní nastavení požadovaného správce scénářů před tím, než se vytvoří příslušná instance tovární třídy. Můžeme se ale inspirovat návrhem nastavování vybraného správce v pasáži Přizpůsobení základním požadavkům na straně 113 a následnou definicí metod setdelegate(ascenariomanager) (viz výpis 6.1 na straně 114) a getinstance() (viz výpis 6.2 na straně 115) a definovat jejich ekvivalenty. Dokonce bychom mohli definovat bezparametrickou přetíženou verzi této nastavovací metody. Ta nejprve otevře dialogové okno, prostřednictvím nějž pohovoří s uživatelem a zeptá se jej, kterého správce touží nastavit. Zadaného správce pak nastaví pro následně vytvářené instance. Má-li však zůstat splněný předpoklad z pasáže Jak získat objekt třídy na straně 70, tak ještě musíme zabezpečit, že takto upravená tovární třída neumožní, aby její Strana 121 z 351

122 Kapitola 6: Sjednocený správce scénářů 122 různé instance pracovaly s různými správci scénářů. To už jsme ale dělali, takže to stačí zopakovat. Zdrojový kód výše popsané třídy (bez zakomentovaných metod čekajících na své budoucí odkomentování) si můžete prohlédnout ve výpisu 6.8. V definici si všimněte, že metoda setscenariomanager(ascenariomanager) nastavuje na řádku 98 hodnotu statického atributu futuremanager, kterou pak konstruktor zadá jako hodnotu konstanty vytvářené instance (řádek 162). Ty, kteří se teprve zahřívají v používání funkcionálních konstrukcí, bych upozornil na statickou konstantu TEXT_2_FACTORY 16 (řádek 42), která mapuje stručnou charakteristiku daného scénáře na metodu, jež má být zavolána, budeme-li chtít získat jeho instanci. Tato mapa je inicializována ve statickém konstruktoru na řádcích 65 až 73. Z mapy (přesněji z jejích klíčů) je pak vytvořeno pole charakteristik určené pro zobrazení v dialogovém okně v případě, že uživatel bude chtít zvolit scénář až za chodu. Inicializace atributu futuremanager ve statickém inicializačním bloku (řádek 79) je pouze aplikací zásady, že všechny inicializace mají být realizovány na jednom místě a nemají být rozcourány po celém zdrojovém kódu. K nastavení požadovaného slouží dvě statické metody. Ta bezparametrická definovaná na řádcích 107 až 131 umožňuje uživateli, aby si vybral požadovaného správce v dialogovém okně. Vybere-li si verzi používající dědění či dekorátor a mající explicitně nastavenou používanou třídu, ještě před závěrečným nastavením atributu futuremanager zabezpečí ono explicitní nastavení. Vlastní nastavení tohoto atributu má na starosti jednoparametrická verze metody, která před vlastním nastavením nejprve zkontroluje, jestli už nebyla vytvořena instance dané tovární třídy, protože by jinak mohla každá instance odkazovat na jiné správce. Výpis 6.8: Definice statické tovární třídy RUPGSMFactory 1 /*************************************************************************** 2 * Instance třídy {@code RUPGSMFactory } představují tovární objekty 3 * umožňujících získání instancí správce scénářů vyvíjené hry */ 6 public class RUPGSMFactory implements IGSMFactory, IAuthorRUP 7 { 8 //== CONSTANT CLASS ATTRIBUTES ================================================= 9 10 /**Označení správce scénářů používajícího konstanty. */ 11 private static final String CON = "Konstanty"; Proč se konstanta jmenuje TEXT_2_FACTORY vysvětluje poznámka na straně 158. Strana 122 z 351

123 Kapitola 6: Sjednocený správce scénářů /**Označení správce scénářů používajícího literály. */ 14 private static final String LIT = "Literály"; /**Označení správce scénářů definovaného jako dekorátor 17 * bez explicitního nastavení dekorovaného správce. */ 18 private static final String DEC = "Dekorátor bez přednastavení"; /**Označení správce scénářů definovaného jako dekorátor 21 * obalující správce používajícího konstanty. */ 22 private static final String DEC_CON = "Dekorátor + konstanty"; /**Označení správce scénářů definovaného jako dekorátor 25 * obalující správce používajícího literály. */ 26 private static final String DEC_LIT = "Dekorátor + literály"; /**Označení správce scénářů používajícího dědění 29 * bez explicitního nastavení použitého potomka. */ 30 private static final String HER = "Dědění bez přednastavení"; /**Označení správce scénářů používajícího dědění 33 * s potomkem používajícím konstanty. */ 34 private static final String HER_CON = "Dědění + konstanty"; /**Označení správce scénářů používajícího dědění 37 * s potomkem používajícím literály. */ 38 private static final String HER_LIT = "Dědění + literály"; /** Mapa převádějící textová označení jednotlivých správců scénářů 41 * na tovární metody vracející jejich instance. */ 42 private static final Map<String, Supplier<AScenarioManager>> TEXT_2_FACTORY; //Pole textových označení definovaných správců scénářů. */ 45 private static final String[] TEXTS; //== VARIABLE CLASS ATTRIBUTES ================================================= //Správce scénářů nastavovaný pro následně vytvářené tovární objekty 52 private static AScenarioManager futuremanager; //Příznak toho, že již byla vytvořena instance, a není již proto možno 55 //měnit nastavení správce scénářů. 56 private static boolean instancecreated; //############################################################################## 61 //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== static { 64 //Inicializace mapy doposud definovanými třídami správců scénářů Strana 123 z 351

124 Kapitola 6: Sjednocený správce scénářů TEXT_2_FACTORY = new LinkedHashMap<>(); 66 TEXT_2_FACTORY.put(CON, RUPScenarioManagerCon::getInstance); 67 TEXT_2_FACTORY.put(LIT, RUPScenarioManagerLit::getInstance); 68 TEXT_2_FACTORY.put(DEC, RUPScenarioManagerD ::getinstance); 69 TEXT_2_FACTORY.put(DEC_CON, RUPScenarioManagerD ::getinstance); 70 TEXT_2_FACTORY.put(DEC_LIT, RUPScenarioManagerD ::getinstance); 71 TEXT_2_FACTORY.put(HER, RUPScenarioManagerH ::getinstance); 72 TEXT_2_FACTORY.put(HER_CON, RUPScenarioManagerH ::getinstance); 73 TEXT_2_FACTORY.put(HER_LIT, RUPScenarioManagerH ::getinstance); List<String> keys = new ArrayList<>(TEXT_2_FACTORY.keySet()); 76 TEXTS = keys.toarray(new String[keys.size()]); //Nastavení implicitní hodnoty 79 futuremanager = RUPScenarioManagerCon.getInstance(); 80 } //== CLASS GETTERS AND SETTERS ================================================= /*************************************************************************** 87 * Nastaví správce scénářů, který bude vracen tovární metodou. 88 * 89 scenariomanager Nastavovaný správce scénářů 90 */ 91 public static void setscenariomanager(ascenariomanager scenariomanager) 92 { 93 if (instancecreated) { 94 throw new IllegalStateException( 95 "\nbyla již vytvořena instance tovární třídy, " + 96 "\ntakže správce scénářů již není možno měnit"); 97 } 98 futuremanager = scenariomanager; 99 } /*************************************************************************** 103 * Otevře dialogové okno a od uživatele zjistí, 104 * kterého z připravených správců scénářů chce zadat 105 * pro následně vytvářené instance. 106 */ 107 public static void setscenariomanager() 108 { 109 String selected = (String)JOptionPane.showInputDialog(null, 110 "Vyberte správce pomocí charakteristiky", 111 "Zadání správce scénářů", 112 JOptionPane.QUESTION_MESSAGE, null, 113 TEXTS, 114 TEXTS[0]); 115 switch(selected) 116 { Strana 124 z 351

125 Kapitola 6: Sjednocený správce scénářů case DEC_CON: RUPScenarioManagerD.setDelegate( 118 RUPScenarioManagerCon.getInstance()); 119 break; 120 case DEC_LIT: RUPScenarioManagerD.setDelegate( 121 RUPScenarioManagerLit.getInstance()); 122 break; 123 case HER_CON: RUPScenarioManagerH.setChild( 124 RUPScenarioManagerCon.getInstance()); 125 break; 126 case HER_LIT: RUPScenarioManagerH.setChild( 127 RUPScenarioManagerLit.getInstance()); 128 break; 129 } 130 setscenariomanager(text_2_factory.get(selected).get()); 131 } //== OTHER NON-PRIVATE CLASS METHODS =========================================== 136 //== PRIVATE AND AUXILIARY CLASS METHODS ======================================= //############################################################################## 141 //== CONSTANT INSTANCE ATTRIBUTES ============================================== /** Správce scénářů daného továrního objektu. */ 144 private final AScenarioManager scenariomanager; //== VARIABLE INSTANCE ATTRIBUTES ============================================== //############################################################################## 153 //== CONSTUCTORS AND FACTORY METHODS =========================================== /*************************************************************************** 156 * Vytvoří tovární objekt s předem zadaným správcem scénářů. 157 * Nebyl-li správce ještě zadán, použije se instance třídy 158 * {@link RUPScenarioManagerCon}. 159 */ 160 public RUPGSMFactory() 161 { 162 scenariomanager = futuremanager; 163 } //== ABSTRACT METHODS ========================================================== 168 //== INSTANCE GETTERS AND SETTERS ============================================== Strana 125 z 351

126 Kapitola 6: Sjednocený správce scénářů /*************************************************************************** 171 * Vrátí odkaz na instance správce scénářů. 172 * 173 Požadovaný odkaz 174 */ 176 public AScenarioManager getscenariomanager() 177 { 178 return scenariomanager; 179 } //== OTHER NON-PRIVATE INSTANCE METHODS ======================================== 184 //== PRIVATE AND AUXILIARY INSTANCE METHODS ==================================== //############################################################################## 189 //== NESTED DATA TYPES ========================================================= 190 } 6.5 Bez propagace do dalších kapitol V této kapitole jsme poněkud odbočili od hlavního tématu publikace, kterým je demonstrace vývoje netriviální aplikace. Účelem kapitoly bylo pouze přiblížit možné způsoby realizace návrhového vzoru Strategie a naznačit některá možná úskalí. Správce scénářů i verze tovární třídy vyvinuté v této kapitole se objeví pouze v závěrečném projektu této kapitoly a nebudou již propagovány do dalších kapitol. 6.6 Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Zopakujme si, co jsme v této kapitole dozvěděli a co jsme naprogramovali: Seznámili jsme se s návrhovým vzorem Strategie a vysvětlili si jeho základní principy. Realizovali jsme možnost výběru mezi dvěma způsoby definice správců scénářů aplikací návrhového vzoru Dekorátor. Přitom jsme řešili následující: Strana 126 z 351

127 Kapitola 6: Sjednocený správce scénářů 127 Ukázali jsme si, jak je v prostředí NetBeans možno ze známé třídy jednoduše vytvořit její dekorující třídu. Na základě třídy AScenarioManager jsme vytvořili dekorační třídu RUPScenarioManagerD. Předvedli jsme si, jak je možno definovat metodu nastavující požadovaného delegáta dekorovaný objekt. Připomněli jsme si, že zadané řešení není vláknově bezpečné, a je proto použitelné pouze tehdy, nehrozí-li jeho současné použití ve více vláknech. Definovali jsme tovární metodu, která vrací požadovaný objekt vyhovující aktuálně zvolené strategii. Vysvětlili jsme si, že definuje-li společný rodič některé metody jako finální, a nelze proto v potomkovi předat jejich provedení delegátovi, musíme toto omezení obejít. Finální rodičovské metody řešily především práci se spravovanými scénáři. Ukázali jsme si, jak v konstruktoru dekorátoru předat tyto scénáře rodičovskému podobjektu, aby pak s nimi mohl pracovat. Upravili jsme definici metody main(string[]) tak, abychom v ní mohli nastavit požadovaný způsob tvorby správce scénářů. Při realizaci druhé cesty, tj. využití dědění, jsme postupně probrali a realizovali následující: Definovali jsme novou třídu RUPScenarioManagerH jako kopii dekorační třídy RUPScenarioManagerD. Upravili jsme definice našich původních scénářů tak, že jsme jako jejich bezprostředního předka nastavili třídu RUPScenarioManagerD. Zjednodušili jsme konstruktor tak, že potomek předal předku pouze odkaz na class-objekt třídy svojí hry. Definovali jsme metodu setchild(rupscenariomanagerh) umožňující zadat, kterého z potomků bude tovární metoda vracet jako instanci dané třídy. Definovali jsme statickou tovární metodu, která zabezpečila vracení instance toho správného potomka. Při té příležitosti jsme si připomněli nutnost dodržování principu LSP. Na závěr jsme upravili definici tovární třídy tak, aby si mohl uživatel zvolit za chodu strategii definice správce scénářů. Výslednou podobu projektu, k níž jsme se propracovali na konci této kapitoly, najdete v projektu A106z_UnifiedScenarioManager. Strana 127 z 351

128 Kapitola 7: Návrh rámce pro hru Návrh rámce pro hru Kapitola 7 Návrh rámce pro hru Co se v kapitole naučíte V této kapitole se pokusíme převést verbální (slovní) zadání hry z první kapitoly do programové podoby tak, aby se požadavky tohoto zadání pokud možno přeměnily do požadavků definovaných interfejsů, a pokud to jen trochu půjde, tak rovnou do signatur, které, jak víme, dokáže zkontrolovat již překladač. Požadavky, které zůstanou na úrovni kontraktu, bude muset následně plnit testovací program. I jeho práci se však pokusíme ulehčit. 7.1 Koncepce rámce hry Pro převod verbálního zadání do programové podoby se nabízejí dvě možnosti: Jednodušším řešením je definovat prázdnou třídu nazvanou např. Game a tuto třídu postupně vylepšovat. Druhou možností je nedefinovat přímo třídu hry, ale definovat pouze interfejs nazvaný např. IGame, který bude specifikovat základní vlastnosti připravované hry a který budou implementovat třídy realizující nějakou hru. K němu by bylo vhodné přidat interfejsy dalších objektů hry a získat tak jistý rámec (framework), jemuž budou vyhovovat všechny vytvářené hry. Druhé řešení je zdánlivě složitější. Samouci, kteří si chtějí připravit jedinou hru, mohou zvolit první řešení. Ve škole se však setkáte spíše s tím druhým, protože pak může každý student definovat svoji vlastní hru, a přesto bude možno všechny odevzdané hry zpracovávat jednotným způsobem. Navíc se při jeho aplikaci můžete naučit několik dalších principů, které se vám budou v další praxi hodit. Výhodou řešení se společným rámcem je i to, že kdybychom chtěli v dalším kroku navrhnout nějaké grafické uživatelské rozhraní našich her, mohli bychom je Strana 128 z 351

129 Kapitola 7: Návrh rámce pro hru 129 navrhnout tak, aby bylo možno s jeho pomocí spustit libovolnou hru vyhovující požadavkům našeho rámce. Jak jste jistě odhadli, přikloníme se k druhému řešení. Připravíme rámec, kterému budou muset vyhovovat naše hry i doprovodné scénáře. Nebude to výhodné pouze pro učitele, ale i pro vás. Když budete připravovat svoji vlastní hru, jistě oceníte, že nemusíte vyvíjet vše od začátku, ale že pro řadu pomocných činností (např. právě pro testování) budete moci využít služeb tohoto rámce. 7.2 Objekty vystupující ve hře Pojďme se tedy zamyslet nad tím, jaké objekty budou v naší hře vystupovat, a jaké interfejsy by proto měl náš rámec obsahovat. Jeden ze způsobů návrhu doporučovaný začátečníkům je vypsat si zadání a zvýraznit v něm všechna podstatná jména. To budou kandidáti na budoucí objekty a třídy objektů. Vezmeme proto zadání z podkapitoly 1.1 Koncepce vyvíjené hry na straně 31, vyjmeme z něj upřesňující texty a zvýrazníme v něm podstatná jména: Zadání Hra probíhá ve virtuálním světě, v němž existuje několik prostorů, které spolu zadaným způsobem sousedí. Hra musí akceptovat příkaz realizující přechod z aktuálního prostoru do sousedního prostoru. V každém prostoru se mohou nacházet různé h-objekty. Některé z nich může hráč vzít, uložit je do pomyslného batohu, aby mu v budoucnu pomohly ke splnění nějakého pomocného úkolu nebo dokonce cíle celé hry. Hra musí proto akceptovat příkaz, který přesune zadaný h-objekt z aktuálního prostoru (tj. prostoru, v němž se právě nachází hráč) do batohu a příkaz, který naopak přesune h-objekt z batohu do aktuálního prostoru. Množství h-objektů, které se do batohu vejdou, je však omezené. Příkaz pro přesun h-objektu z prostoru do batohu proto nesmí vyvolat překročení kapacity batohu. Zrovna tak nesmí umožnit uložit do batohu nepřenositelný h-objekt. Definice použitých interfejsů Pojďme nyní projít ono zadání, provést analýzu nalezených podstatných jmen a rozhodnout, která z nich si zaslouží, aby je ve vytvářeném rámci zastupoval nějaký interfejs. Strana 129 z 351

130 Kapitola 7: Návrh rámce pro hru 130 Hra IGame Prvním podstatným jménem je hra. Ta je určitě nezpochybnitelným kandidátem, protože ta je tím hlavním objektem, jehož vytvoření je naším cílem. Všechny ostatní objekty budou pouze pomocné. Definujeme proto interfejs IGame, jehož instance budou představovat vytvořenou hru. Aby nebylo možno spustit současně několik her, které by se pak přetahovaly o zdroje (např. o standardní vstup a výstup), přidáme do kontraktu požadavek, aby hra byla definována jako jedináček. Svět IWorld Dalším podstatným jménem je svět, v němž se bude celá hra odehrávat. Svět pak už dále v zadání nevystupuje, ale působí dojmem objektu, který by mohl mít v programu důležitou roli, a proto definujeme interfejs IWorld, jehož instance budou představovat světy jednotlivých her. Do kontraktu opět přidáme požadavek, že i instance světa by měla být jedináček, protože hra musí probíhat pouze v jediném světě. Prostor IArea Následujícím podstatným jménem v zadání je prostor. Prostory, v nichž se hra odehrává, jsou její klíčovou součástí a hovoří se o nich i v zadání doplňujících podmínek pro tvorbu scénáře (hra musí např. obsahovat zadaný minimální počet prostorů). Je tedy více než zřejmé, že bude nanejvýš vhodné definovat interfejs IArea, jehož instance budou představovat prostory dané hry. Příkaz Akce IAction Při dalším čtení narazíme na podstatné jméno příkaz. Příkazy jsou dalšími klíčovými prvky hry, na které jsou kladeny dodatečné podmínky. Jak si ale možná vzpomenete, tak jsem vám v poznámce na stránce 33 říkal, že budu rozlišovat termíny příkaz a akce. Objekty našeho programu by ale měly spíš představovat akce, které budou schopny zareagovat na různé příkazy. Proto jsem se rozhodl pojmenovat příslušný interfejs IAction. Možná vám bude připadat divné, proč by měla být akce objektem. Uvědomte si ale, že objekty představující jednotlivé akce (přesun mezi prostory, položení, resp. zvednutí h-objektu atd.) budou uchovávat informace o tom, jak má hra zareagovat v případě, kdy hráč zadá příkaz vedoucí k realizaci dané akce. Přitom musejí umět rozumně zareagovat i v případě, kdy hráč zadá příkaz špatně. Špatně zadaný příkaz nesmí program zhroutit, ale pouze vyvolá reakci, která hráče upozorní na jeho chybu. Strana 130 z 351

131 Kapitola 7: Návrh rámce pro hru 131 Přechod Následuje podstatné jméno přechod. Tady už to není tak jasné. Mohli bychom sice požadovat vytvoření objektu definujícího přechod z prostoru do prostoru, ale na druhou stranu bychom mohli pojmout otázku těchto přechodů tak, že to, jestli je možné z jednoho prostoru přejít do druhého, je interní věci daného prostoru, a že by proto tuto informaci měl spravovat daný prostor a neměla by být uložená v nějakém veřejném objektu. Stačilo by, aby si každý prostor pamatoval prostory, do nichž je možné v daném okamžiku přejít. V zájmu maximálního zapouzdření bychom tedy tentokrát speciální veřejné objekty pro přechod z prostoru do prostoru (objekty přechodů) nevytvářeli. O tom, jestli mají vůbec nějaké vzniknout, bychom rozhodli, až budeme vytvářet třídu definující prostory. To, že jsem zde nedoporučil zavádět pro přechody jejich vlastní datový typ, ještě neznamená, že se to při trochu jiné koncepci hry nemůže hodit. V literatuře jsem se setkal s aplikacemi realizujícími hru, v níž se prochází bludištěm a v níž byly přechody docela užitečné datové objekty. H-objekt IItem Při dalším čtení přeskočíme několik prostorů (přesněji řečeno podstatných jmen prostor), o nichž už jsme rozhodli, a narazíme na podstatné jméno h-objekt. H-objekty jsou opět klíčovým prvkem hry s dalšími omezujícími podmínkami (některé musí jít zvednout a jiné ne), takže není pochyb o tom, že by měli mít v rámci (ve frameworku) svého zástupce bude jím interfejs IItem. Hráč Následuje podstatné jméno hráč. Tady asi opět trochu zaváháme. Musíme si rozmyslet, jestli pro nás hráče představuje uživatel (případně testovací program), anebo jestli se jedná opravdu o objekt hry. Musíme pročíst zadání a ujasnit si, jaká by mohla být role hráče v programu. Ze zadání vyplývá, že hráč se někde nachází a má něco v batohu. V krocích scénáře, které jsme navrhovali, jsou aktuální prostor i obsah batohu samostatné informace. Je tedy otázkou, jestli je jako samostatné informace držet i v programu jestli např. nedefinovat samostatný batoh s tím, že si pozici hráče bude pamatovat třeba svět, v němž se hráč nachází. Obě cesty jsou možné. Když jsem se o těchto možnostech rozhodoval já, dospěl jsem k závěru, že informace týkající se hráče jsou poněkud heterogenní (nesourodé) a takže by hráč buď dělal několik relativně různých věcí, anebo by fungoval jen jako přepravka pro informace získané specializovanými objekty. Rozhodl jsem Strana 131 z 351

132 Kapitola 7: Návrh rámce pro hru 132 se proto objekt hráče do hry nezavádět a využít pro získání informací o hráči specializované objekty: pro informaci o aktuálním prostoru svět a pro informaci o obsahu batohu speciální objekt reprezentující pouze batoh. Na druhou stranu, kdyby byla hra koncipována maličko jinak a mohlo ji např. hrát několik hráčů, bylo by asi výhodnější objekty hráčů zavést. Přiznávám se ale, že mne myšlenka na alternativní zpracování láká, takže není vyloučené, že v některém příštím semestru bude zvolena koncepce, v níž bude hráč vystupovat jako jeden z objektů hry. Zatím tomu ale tak není. Batoh IBag Při dalším čtení zadání narazíme vzápětí na podstatné jméno batoh. O něm jsem před chvílí hovořil, takže je vám jistě jasné, že je o něm rozhodnuto: bude jej reprezentovat samostatný objekt instance interfejsu IBag. Úkol, cíl V následujícím textu se hovoří o pomocném úkolu a o cíli celé hry. Tyto pojmy jsou však evidentně pouze doplňkové a není třeba pro ně v rámci definovat nějakou speciální reprezentaci. Opět ale platí, že by při maličko změněné koncepci hry mohlo být výhodně tyto objekty definovat. Množství, kapacita Následuje řada podstatných jmen, které se v textu již vyskytly, a na jejich výskyt jsme již zareagovali. Poslední doposud neprodiskutovaná podstatná jména jsou množství h-objektů v batohu a kapacita batohu. Obě označují totéž, přičemž se asi shodneme, že se jedná o interní informaci batohu, kterou by si měl batoh spravovat sám a není třeba pro ni definovat nějaký zvláštní datový typ batoh si může pamatovat svoji kapacitu jako číslo. 7.3 Upřesnění návrhu rámce Z předchozí analýzy nám vyplynuly interfejsy, které by měly v rámci reprezentovat objekty, které by bylo vhodné v každé hře vytvořit. Každá hra si je vytvoří po svém, ale ve frameworku budou jejich společní rodiče definovat, jaké vlastnosti mají tyto objekty mít. Pojďme si je tedy ještě jednou shrnout: IGame Hra Strana 132 z 351

133 Kapitola 7: Návrh rámce pro hru 133 IWorld IArea IBag IItem Svět, v němž hra probíhá Prostory, mezi nimiž hráč přechází Batoh na přenášené h-objekty H-objekty v prostorech IAction Objekty zodpovědné za reakci na příkazy zadávané uživatelem Pojďme nyní projít jeden interfejs po druhém a ujasněme si, co budeme vyžadovat po objektech, které daný interfejs implementují. Musíme si přitom uvědomit, že připravujeme rámec, který má na jedné straně poskytnout programátorům (= studentům) jakési mantinely, v nichž se budou pohybovat, a na druhou stranu poskytnout našemu rámci prostředky, aby mohl obdržená řešení otestovat. Naše požadavky proto nebudou řešit otázku, jak danou hru co nejlépe naprogramovat, ale prozatím jen jak danou hru co nejlépe otestovat. Začněme od konce našeho seznamu. IAction Název getname() Abychom poznali, kterou akci má hra po zadání příkazu provést a který z akčních objektů (tj. z objektů reprezentujících jednotlivé akce) máme proto pověřit realizací příslušné reakce, musíme akce pojmenovat. Po akcích proto budeme chtít implementaci metody String getname() Měli bychom si uvědomit, že jméno akce musí být jedinečné, a to i tehdy, když při porovnávání jmen akcí nebude záležet na velikosti písmen. Kontrola tohoto požadavku je do jisté míry již obsažena ve scénářích, protože každý název akce je možno použít pouze pro jednu skupinu typů akcí. Je vám ale jistě jasné, že tento požadavek se budou tvůrci her snažit dodržet i bez nabádání, protože kdyby programátor tento požadavek nedodržel, program by se mu buď výrazně zkomplikoval, anebo by vůbec nechodil. Na závěr úvah o názvech akci bych připomněl poznámku na straně 55, která hovoří o tom, že názvy objektů hry (a mezi ně patří i akce) musejí být jednoslovné. Popis getdescription() Mezi povinnými akcemi je i akce vypisující nápovědu. Ta by měla uživateli stručně popsat vlastnosti jednotlivých akcí a podobu příslušných příkazů. Nejlepší by bylo, kdyby každá akce znala svůj stručný popis a uměla jej vrátit jako reakci na zaslání příslušné zprávy. Budeme proto požadovat definici metody Strana 133 z 351

134 Kapitola 7: Návrh rámce pro hru 134 String getdescription() která vrátí stručný popis dané akce a příkazů, které ji používají (např. počet a účel požadovaných parametrů). Reakce na zadání příkazu execute(string[]) Objekty akcí slouží především k realizaci reakcí na příkazy uživatele. Měly by tedy implementovat metodu, která zpracuje uživatelem zadaný text a vrátí text, který hra vypíše jako reakci na uživatelův příkaz. Při deklaraci požadavku na danou metodu musíme mít na paměti, že některé akce budou vyžadovat parametry např. při přesunu je třeba zadat prostor, do nějž se má hráč přemístit. Musíme proto umět tyto parametry dané metodě předat. Nemá smysl, aby každá instance interfejsu IAction uměla analyzovat vstup uživatele. Výhodnější by bylo definovat někde (kde to bude, určíme až při implementaci) kód, který text zadaný uživatelem předzpracuje a rozhodne, jakou akci chtěl uživatel provést. Při té příležitosti by mohl také zjistit, jaké zadal parametry a předat je aktivované instanci akce v poli řetězců, jehož prvky by byla jednotlivá slova z příkazového řádku zadaného uživatelem. Z toho vyplývá, že signatura potřebné metody by mohla být String execute(string[] parameters) Metoda zpracuje zadané parametry, provede požadovanou akci a vrátí text, jenž uživateli předá informace o výsledku provedené akce. Teoreticky bychom ze slov příkazového řádku mohli odstranit to počáteční, které označuje vyvolávanou akci, ale to by byla zbytečná práce, protože tím nic neušetříme. Výhodnější bude předat objektu všechna slova a nastavit zpracování parametrů tak, že metoda bude vědět, že nultý parametr je názvem akce, a že jej má proto ignorovat. IItem Název getname() Každý h-objekt vystupující ve hře se musí nějak jmenovat, protože jinak bychom nemohli rozumně určit, se kterým z nich chceme pracovat. Budeme proto po něm vyžadovat implementaci metody se signaturou String getname() která vrátí název daného h-objektu. Nic přitom nebrání tomu, aby v prostoru nebo batohu bylo současně několik h-objektů se stejným názvem. Strana 134 z 351

135 Kapitola 7: Návrh rámce pro hru 135 Opět bych připomněl poznámku na straně 55 hovořící o tom, že názvy musejí být jednoslovné. Váha getweight() V zadání je navíc uvedeno, že některé h-objekty lze zvednou a přemístit do batohu a některé ne. Každý h-objekt by měl proto vědět, jestli je možno jej zvednout. Tento požadavek můžeme zobecnit a požadovat po h-objektech aby znaly svoji váhu. Program se tím nijak nezesložití, a přitom bude možno vytvářet hry, v nichž bude počet h-objektů, které se vejdou do batohu, záviset na tom, kolik tyto h-objekty váží. Kdo bude chtít zůstat pouze u počtu h-objektů, může přenositelným h-objektům přiřadit váhu 1 a nepřenositelným váhu větší, než je kapacita batohu. Budeme proto po h-objektech požadovat implementaci metody int getweight() vracející jejich váhu. Omezení povolených hodnot na celá čísla by nemělo být pro tvůrce her zásadním omezením. IBag Kapacita getcapacity() Zadání vyžaduje, aby batoh měl konečnou kapacitu. Můžeme se dohadovat o tom, jestli by bylo lepší číslo celé nebo reálné. Když si ale uvědomíme, že cílem návrhu aplikace není vytvoření nějaké superdokonalé hry, ale pouze demonstrace schopnosti studentů realizovat nějaký netriviální projekt, pak je zřejmé, že celočíselná kapacita bude zcela dostačující. Budeme proto po batohu požadovat implementaci metody int getcapacity() Jak už jsem řekl dříve, v jednodušších hrách může kapacita definovat maximální počet h-objektů, které se do batohu vejdou (pak bude mít každý přenositelný h-objekt váhu 1). V rafinovanějších hrách mohou mít h-objekty různé váhy, přičemž kapacita batohu určuje povolený součet vah h-objektů, jež se v něm právě nacházejí. Obsah getitems() Kromě toho bychom potřebovali vědět, jaké h-objekty jsou v dané chvíli v batohu, abychom mohli zkontrolovat, že se po příkazu požadujícím přidání h-objektu do batohu daný h-objekt v batohu opravdu objevil. Do interfejsu proto přidáme deklaraci metody Collection<? extends IItem> getitems() Strana 135 z 351

136 Kapitola 7: Návrh rámce pro hru 136 Připomínám, že tato deklarace vyžaduje, aby metoda vrátila kolekci h-objektů, které jsou instancemi třídy implementující interfejs IItem. IArea Název getname() I prostory budou muset mít svá jména, abychom mohli určit, do kterého z nich se chceme přesunout. I pro ně budeme proto vyžadovat implementaci metody String getname() která vrátí (jednoslovný!) název daného prostoru. Sousedé prostoru getneighbors() V zadání je navíc uvedeno, že standardní přesouvací akce (tj. ta povinná) povoluje přesun pouze do sousedního prostoru. V krocích scénáře je vždy uvedeno, se kterými prostory daný prostor v daném okamžiku sousedí. Abychom to mohli zkontrolovat, musí každý prostor umět prozradit své aktuální sousedy. Budeme po něm proto požadovat implementaci metody Collection<? extends IArea> getneighbors() která vrátí kolekci prostorů, které jsou v daném okamžiku z daného prostoru přímo přístupné zadáním příkazu standardního přesunu. H-objekty v prostoru getitems() Navíc víme, že v prostorech se mohou vyskytovat h-objekty, z nichž některé může uživatel zvednout. Každý krok scénáře zadává, které h-objekty se v daném kroku scénáře v aktuálním prostoru nacházejí, takže je potřebujeme mít možnost zkontrolovat. Budeme proto po prostorech požadovat, aby implementovaly metodu Collection<? extends IItem> getitems() která vrátí kolekci se všemi h-objekty, jež se v aktuálním prostoru právě vyskytují. IWorld Objekt představující svět naší hry bude muset na počátku hry tento svět nějak zorganizovat, tj. vytvořit jednotlivé prostory a vzájemně propojit ty sousední. Měl by tedy sloužit jako takový správce prostorů hry, který má o prostorech hry přehled. Strana 136 z 351

137 Kapitola 7: Návrh rámce pro hru 137 Kolekce prostorů hry getallareas() Vlastní vytváření a vzájemné propojování jednotlivých prostorů kontrolovat nebudeme, ale ponecháme si možnost zkontrolovat výsledek. Budeme proto požadovat implementaci metody Collection<? extends IArea> getallareas() která vrátí kolekci všech prostorů, které se ve hře vyskytují, nezávisle na tom, zda jsou hráči přístupné či ne. Otázkou přitom je, jestli máme povolit vznik a zánik prostorů v průběhu hry, anebo požadovat, aby tato kolekce byla neměnná. Pro zjednodušení dávám přednost požadavku na neměnnou kolekci. Aktuální prostor getcurrentarea() Kroky scénáře specifikují, v kterém prostoru se v daném okamžiku hráč nachází. Abychom mohli zkontrolovat korektní chování hry, musíme mít možnost tuto informaci zjistit. Otázku toho, kdy by si měl pamatovat aktuální pozici hráče, jsme rozebírali v pasáži Hráč na straně 131. Když jsme zavrhli zavedení objektu hráče (dobře, já jsem jej zavrhl), tak dalším objektem, který by nám to mohl prozradit, je právě správce všech prostorů hry. Mohli bychom proto po něm chtít, aby si udržoval i přehled o tom, ve kterém prostoru se hráč právě nachází, a na požádání nám prozradil, abychom to mohli porovnat s tím, co se dozvíme v příslušném kroku scénáře. Budeme proto požadovat implementaci metody IArea getcurrentarea() IGame A jsme ve finále budeme se rozhodovat o tom, jaké požadavky budeme klást na každou hru. Identifikace autora getauthorname(), getauthorid() Začnu tím, že si uvědomíme, že se jedná o úlohu určenou k hodnocení, a proto budeme požadovat, aby nám prozradila, kdo je jejím autorem a komu tedy připsat příslušné hodnocení. Budeme postupovat stejně jako u správce scénářů a definujeme interfejs hry jako potomka interfejsu IAuthor, který vyžaduje implementaci metod public String getauthorname() public String getauthorid() Jejich funkci (a dokonce i způsob realizace) jsme již probírali v pasáži 2.6 Jméno a ID autora na straně 42. Strana 137 z 351

138 Kapitola 7: Návrh rámce pro hru 138 Tovární třída getfactoryclass() Když už hra prozrazuje svého autora, tak by, stejně jako u správce scénářů, bylo vhodné, aby uměla prozradit i svoji tovární třídu. Budeme proto i po ní chtít, aby definovala metodu public Class<? extends IGSMFactory> getfactoryclass() která by nám např. umožnila po obdržení objektu hry získat prostřednictvím její továrny i správce jejích scénářů a hru tak jednoduše otestovat. Název getname() Každý slušný program se nějak jmenuje. U her bývá navíc jejich název součástí marketingové kampaně, a je proto vytvářen tak, aby budoucí hráče vhodně navnadil. I po těchto hrách proto budeme chtít, aby měly přidělené své jméno, a budeme po nich proto vyžadovat implementaci metody se signaturou String getname() která vrátí název příslušné hry. Oproti ostatním názvům nám však nebude vadit, když tento název bude obsahovat více slov, protože se s ním v programu nebude pracovat nijak jinak, než že se někde zobrazí. Kolekce přípustných akcí getallactions() Pro účely testování by nám hra měla umět prozradit, jaké příkazy je možno během hry zadat, abychom mohli zkontrolovat, že žádná z akcí vyžadovaných správcem scénářů nechybí. Budeme proto po ní chtít, aby definovala metodu Collection<? extends IAction> getallactions() která vrátí kolekci se všemi akcemi, které je možné dané hře vyvolat. Protože však víme, že každá hra musí být schopna reagovat na startovací příkaz, jehož názvem je prázdný řetězec, tak dovolíme, aby tento příkaz v obdržené kolekci chyběl. Získání světa hry getworld() Abychom mohli zkontrolovat, že stav hry po provedení příkazu odpovídá stavu požadovaného ve scénáři, potřebujeme zjistit aktuální prostor, který je nám schopen prozradit své h-objekty i sousedy (viz pasáž IArea na straně 136). Aktuální prostor umíme zjistit od objektu reprezentujícího svět hry. K tomu ale potřebujeme získat svět hry. Budeme proto po hře chtít, aby definovala metodu IWorld getworld() která vrátí objekt reprezentující svět hry. Strana 138 z 351

139 Kapitola 7: Návrh rámce pro hru 139 Získání obsahu batohu getbag() Součástí informací o stavu hry po provedení příkazu jsou i údaje o h-objektech uložených v batohu. Potřebovali bychom proto umět získat batoh, který nám svůj obsah prozradí. Budeme proto po hře chtít, aby implementovala metodu: IBag getbag() která vrátí objekt reprezentující batoh. Informace o živosti hry isalive() Výše probrané metody nám umožní získat kompletní přehled o aktuálním stavu hry. Jenomže k tomuto přehledu nám ještě něco chybí, a to informace o tom, jestli hra zrovna běží, anebo na své spuštění teprve čeká. Přitom je jedno, jestli se jedná o její první spuštění, anebo už ji někdo hrál, ukončil a ona čeká na další spuštění. K tomuto účelu požádáme hry o implementaci metody boolean isalive() která bude vracet true případě, že hra je právě hraná, a false případě, kdy na své spuštění čeká. Povinné akce getbasicactions() Vrátím se ještě k zadání. To říká, že hra má implementovat sadu šesti povinných akcí: spuštění hry, přesun do zadaného sousedního prostoru, zvednutí a položení h-objektu, nápovědu a předčasné ukončení. Z nich má ale pouze akce pro spuštění hry zadáno, jak se bude jmenovat (její název je prázdný řetězec). Názvy ostatních akcí jsou zcela na libovůli tvůrce dané hry. Kdybychom znali názvy těchto akcí, mohli bychom leccos otestovat i bez znalosti scénářů. Doplníme proto interfejs o požadavek na definici metody: BasicActions getbasicactions() která vrátí názvy zbylých pěti povinných akcí. Přesněji: metoda vrací přepravku typu BasicActions, jejíž konstruktor má rozhraní popsané dokumentačním komentářem ve výpisu 7.1: Výpis 7.1: Popis rozhraní konstruktoru instancí třídy BasicActions 1 /*************************************************************************** 2 * Vytvoří přepravku uchovávající názvy akcí, 3 * které musí být implementovány ve všech hrách. 4 * Názvy těchto akcí musí být jednoslovné. 5 * 6 move Název akce pro přesun z místnosti do místnosti 7 putdown Název akce pro položení h-objektu 8 pickup Název akce pro zvednutí h-objektu 9 help Název akce pro získání nápovědy Strana 139 z 351

140 Kapitola 7: Návrh rámce pro hru end Název akce pro ukončení hry 11 */ 12 public BasicActions(String move, String putdown, String pickup, 13 String help, String end) Externí ukončení hry stop() Požadavek, který jsme ještě nevzali zcela do úvahy, byla možnost zadat příkaz pro předčasné ukončení hry. Když má mít tuto možnost hráč, měl by ji mít i program, který s hrou komunikuje. Doplníme proto interfejs o deklaraci metody void stop() jejímž zavoláním předčasně ukončíme případnou rozjetou hru. Přidáme k tomu dodatečné pravidlo, že pokud někdo zavolá tuto metodu v okamžiku, kdy hra neběží, tak se nic nestane, tj. je povoleno ukončovat již ukončenou hru. Hra tedy nemusí testovat, jestli se jí někdo nesnaží ukončit v době, kdy neběží. Provedení příkazu executecommand(string) Celou dobu, co se bavíme o interfejsu IGame, hovoříme pouze o podpůrných metodách. Zatím jsme ale nevzali do úvahy hlavní účel hry: akceptovat příkazy hráče a posílat mu zprávy popisující reakce programu. To teď na závěr napravíme a doplníme interfejs o deklaraci metody String executecommand(string command) Ta převezme příkaz uživatele, nechá jej zpracovat a vrátí textový řetězec s odpovědí uživateli. 7.4 Doplnění společných rodičů Projděme si nyní naše předchozí požadavky a zamysleme se nad tím, jestli bychom nemohli pro některé z navrhovaných datových typů definovat společného rodiče. Při bližším zkoumání uvidíme dva skupiny kandidátů. Pojmenované objekty interfejs INamed Akce, prostory i h-objekty v prostorech musejí mít přiřazena jména. Všechny tři interfejsy bychom proto mohli definovat jako potomky společného rodiče, kterého můžeme nazvat INamed a který bude deklarovat metodu String getname() Strana 140 z 351

141 Kapitola 7: Návrh rámce pro hru 141 Připomínám, že v pasáži Jednoslovnost názvů na straně 55 byla vyhlášena povinnost definovat všechny názvy jako jednoslovné, přesněji jako texty neobsahující mezery. Víceslovné názvy je proto třeba zapsat jako jedno slovo např. s využitím znaků podtržení. Vědomí, že všechny instance tohoto interfejsu znají svůj název, umožňuje definovat metody, jejichž činnost bude s tímto názvem nějak souviset, a které by proto mohly pracovat s libovolným pojmenovaným objektem. Pomocné statické metody geto(string,?) Můžeme např. očekávat, že pojmenované objekty budou uložené v nějakém kontejneru, v němž je bude identifikovat právě jejich název. Bylo by proto vhodné mít k dispozici metody, které to umí. Interfejs INamed proto definuje trojici statických metod, které to dokáží. Jejich signatury jsou (vynechávám modifikátory public a static): <E extends INamed> Optional<E> geto(string member, E[] array) <E extends INamed> Optional<E> geto(string member, Collection<E> collection) <E extends INamed> Optional<E> geto(string member, Stream<E> stream) Jak vidíte, tyto metody se liší pouze tím, kde objekt se zadaným názvem hledají: jedna jej hledá v poli, druhá v kolekci a třetí jej hledá mezi objekty přitékajícími datovodem. Všechny hledají podle názvu bez rozlišování velikosti znaků a všechny pak vracejí objekt typu Optional<E> do nějž je nalezený objekt zabalen, přičemž typový parametr E označuje typ objektů v prohledávaném kontejneru. Podrobnosti o tomto datovém typu si můžete přečíst v podšeděném bloku Datový typ Optional<E>. Strana 141 z 351

142 Kapitola 7: Návrh rámce pro hru 142 Datový typ Optional<E> Jednou z nejčastěji se vyskytujících nechtěných chyb je vyhození výjimky NullPointerException ve chvíli, kdy v proměnné, která má obsahovat odkaz na objekt, jemuž chcete poslat nějakou zprávu, najdete pouze prázdný odkaz null. Na této chybě je nepříjemné, že dopředu nevíte, zda je zda onen prázdný odkaz jednou z množných hodnot, anebo vznikl pouze opomenutím programátora přiřadit někam požadovanou hodnotu. Postupně se proto prosazuje snaha používání prázdného odkazu zrušit. Řada programovacích jazyků dokonce ani možnost zadat prázdný odkaz neumožňuje. Pro usnadnění definice některých operací, u nichž je třeba reagovat na to, jestli je předán odkaz na objekt nebo prázdný odkaz zavedla Java 8 datový typ Optional<E>. Objekty typu Optional<E> jsou ve skutečnosti obalové objekty, v nichž může být zabalena buď požadovaná hodnota typu E, anebo nic. Současný trend je takový, že prázdný odkaz (null) má být opravdu pouze příznakem opomenutí programátora. Všude jinde, kde se dříve používal prázdný odkaz jako jedna z možných hodnot, se nyní doporučuje používat typ Optional<E>. Toto doporučení se budu držet i v tomto textu. V doprovodných programech ke svým učebnicím a výukovým kurzům se snažím tuto zásadu dodržovat. Zpočátku tím ale docházelo ke zmatení, protože narazil-li student při čtení doprovodného programu na volání metody get(), případně getxxx(?), kde Xxx označuje objekt, který metoda vrací, nečekal, že by metoda mohla vrátit místo klasického objekt nebo null objekt typu Optional, z nějž je třeba vrácený objekt nejprve vyloupnout. V reakci na tyto problémy jsem zavedl názvovou konvenci, podle níž jsou metody vracející objekty zabalené do objektů typu Optional pojmenovány geto, resp. getoxxx (v této učebnici se setkáte s metodami getoitem(string) a getoroom(string)). Prefix o dávám i názvům proměnných, které obsahují odkaz na objekt typu Optional. V této učebnici se proto setkáte s proměnnými oitem, oroom apod. Kontejnery h-objektů interfejs IItemContainer Batoh i prostory mohou obsahovat h-objekty. Oba bychom tedy mohli prohlásit za speciální případy obecného kontejneru h-objektů a definovat pro ně společného rodiče IItemContainer, který bude deklarovat jejich společnou metodu Collection<? extends IItem> getitems() Strana 142 z 351

143 Kapitola 7: Návrh rámce pro hru Doplnění uživatelského rozhraní Zatím jsme uvažovali pouze o vlastní hře. Jenomže aby se taková hra mohla hrát, je potřeba definovat i uživatelské rozhraní, které zprostředkuje komunikaci programu s uživatelem. Tak, jako jsme definovali požadavky na vytvářenou hru, měli bychom definovat požadavky i na uživatelské rozhraní, které umožní hrát danou hru i řadovému uživateli (a nejen našemu testovacímu programu). Pro začátek nebudeme přemýšlet o tom, jak bychom mohli správnou funkci vytvořeného uživatelského rozhraní automaticky otestovat, a budeme požadovat pouze to, aby umělo danou hru spustit. V dalším kole mu možná přidáme i nějakou inteligenci, kterou bychom mohli otestovat, ale to prozatím necháme na ono případné další kolo a zůstaneme u požadavku na spuštění hry. Budeme ale chtít, aby toto uživatelské rozhraní umělo spustit libovolnou hru vyhovující našemu rámci, takže po něm budeme požadovat implementaci dvou metod: první bude bezparametrická a bude spouštět vlastní hru autora, druhá bude mít jeden parametr předávající rozhraní instanci hry, která se má spustit. Budeme v něm tedy deklarovat metody se signaturami: void startgame() void startgame(igame game) Lze přitom očekávat, že bezparametrická verze pouze získá instanci hry vytvořené autorem rozhraní a předá ji v parametru jednoparametrické verzi. Pokud ale autor hry přidal do svého výtvoru nějaké speciální funkce překračující povinnou funkčnost požadovanou rámcem (frameworkem), může definovat uživatelské rozhraní tak, aby v případě, že se hraje jeho hra, využívalo jejích nadstandardních vlastností. 7.6 Diagram tříd balíčku game_txt Všechny interfejsy rámce hry, o nichž jsme v předchozím textu hovořili, jsou definovány v balíčku game_txt, přesněji eu.pedu.adv15p_fw.game_txt. Diagram tříd tohoto balíčku se zobrazenými vzájemnými závislostmi jednotlivých interfejsů si můžete prohlédnout na obrázku 7.1. Nebudete-li lpět na zobrazení všech závislostí a vystačíte se zobrazením vztahů předek potomek, tak se diagram zjednoduší. Tuto zjednodušenou podobu najdete na obrázku 7.2. Strana 143 z 351

144 Kapitola 7: Návrh rámce pro hru 144 Obrázek 7.1 Diagram tříd balíčku game_txt se zobrazenými vzájemnými závislostmi Obrázek 7.2 Zjednodušený diagram tříd balíčku game_txt zobrazující pouze vztahy rodič potomek Strana 144 z 351

145 Kapitola 7: Návrh rámce pro hru Diagram tříd v balíčku empty_classes Z předchozího je zřejmé, že jednotlivé hry se sice budou lišit, ale na druhou stranu v jejich definicích bude také hodně společného. Známe-li rámec hry, můžeme téměř s jistotou předpokládat, jaké třídy v budoucích projektech vzniknou. Budou se sice různě jmenovat, ale budou mít hodně společného. Pro usnadnění vývoje studentských aplikací je proto v rámci připraven balíček eu.pedu.adv15p_fw.empty_classes, v němž jsou definovány třídy, které mohou sloužit jako kostry jednotlivých tříd, které budou součástí vytvářené aplikace. Jednoduchý diagram tříd tohoto balíčku si můžete prohlédnout na obrázku 7.3. Obrázek 7.3 Jednoduchý diagram tříd balíčku empty_classes 7.8 K čemu je dobrý rámec hry Rámec hry slouží pro mnohé jako pomůcka k usnadnění vývoje programu, ale pro některé je to spíše donucovací prostředek, který je alespoň trochu nutí navrhnout program tak, jak se program navrhovat má. Vezměme si např. objekty akcí. Značná část studentů, kteří před příchodem do kurzu již programovali (a jsou často přesvědčeni o své programátorské genialitě) vyřeší zpracování příkazu uživatele jednou obludnou metodou, která zanalyzuje příkazový řádek, a podle toho, co se v něm dočte, příslušně zareaguje. Žádné objekty akcí k takovémuto řešení nepotřebují, tak proč by je zaváděli. Obdobně pracují i s h-objekty v místnostech. Proč zavádět objekt, který ví, co je zač a jaké má vlastnosti. Stačí, když část kódu zodpovědná za přesouvání h-objektů do batohu bude vědět, jak se jmenují nepřenositelné h-objekty (těch je méně), a část kódu zodpovědná za vybírání h-objektů z ledničky bude vědět, jak se jmenují h-objekty, které jsou alkoholickými nápoji. Strana 145 z 351

146 Kapitola 7: Návrh rámce pro hru 146 Zkuste ale do takto zpracovaného programu zanést nějaké vylepšení, anebo opravu. Neustále budete zakopávat o to, že jste někde přehlédli nějakou skrytou závislost. Setkal jsem se už s několika nešťastníky, kteří nastoupili do firmy po nějakém takovémto géniovi, jenž doprovodil program do dále neupravitelného stavu, a pak pro jistotu odešel pracovat do jiné firmy. Když dostanete za úkol pokračovat ve vývoji takto spatlaného programu, rychle pochopíte, proč je vhodné se při vývoji programu řídit zásadami moderního programování. 7.9 Shrnutí co jsme se v kapitole dozvěděli Zopakujme si, co jsme v této kapitole dozvěděli. Protože značnou část kapitoly tvoří výčet požadavků na hru a její objekty, který nechci opakovat, tak celé opakování shrnu do několika málo klíčových bodů: Definovali jsme základní požadavky na hru a objekty, které ji tvoří. Převedli jsme tyto požadavky na požadavky interfejsů rámce (frameworku), kterému bude muset daná hra vyhovovat. Strana 146 z 351

147 Kapitola 8: Začínáme tvořit vlastní hru Začínáme tvořit vlastní hru Kapitola 8 Začínáme tvořit vlastní hru Co se v kapitole naučíte V této kapitole začneme vyvíjet aplikaci, která by odpovídala rámci navrženému v kapitole minulé. Současně si ukážeme, jak při tomto vývoji využít toho, že už máme předem připravené testy. V této kapitole se vrátíme k projektu A105z_ScenarioManager obsahujícímu závěrečnou podobu projektu z kapitoly 5 Návrh správce scénářů konkrétní hry a další ukázková rozšíření a vylepšení budeme aplikovat na něj. 8.1 Výběr metody návrhu Jak už jsme si říkali v základním kurzu, při návrhu programu můžeme obecně postupovat dvěma způsoby: Při návrhu metodou shora dolů (top-down design), při němž nejprve navrhneme hlavní program (byť zpočátku nebude zcela funkční), a ten pak postupně doplňováním jednotlivých chybějících částí upřesňujeme a zdokonalujeme, až nakonec získáme kompletní řešení. Při návrhu metodou zdola nahoru (bottom-up design) naopak nejprve vytvoříme základní stavební kameny, z nich pak vytvoříme větší a větší celky, až nakonec získáme celý výsledný program. Každý z uvedených postupů má své výhody a nevýhody, takže se občas zvolí hybridní metoda, při níž postupujeme chvíli shora dolů a chvíli zdola nahoru tak, abychom maximálně využili výhod každé z těchto metod a naopak pokud možno eliminovali její nevýhody. Strana 147 z 351

148 Kapitola 8: Začínáme tvořit vlastní hru 148 Návrh adventury postupem zdola nahoru si můžete prohlédnout na mých stránkách s animacemi. Koncepce rámce sice tehdy byla maličko jiná, ale bylo to opravdu jen maličko. Protože jsem postup zdola nahoru již předváděl, zkusíme to nyní právě obráceně: půjdeme shora dolů. Navrhneme třídu celé hry a k ní budeme postupně přidávat součásti tak, jak nás o to bude žádat testovací program. 8.2 Začínáme s třídou hry Zde začíná další část vašeho úkolu. Opět doporučuji zkopírovat svůj projekt (to abyste se měli k čemu vrátit, kdyby se vám něco nepovedlo) a pokračujte podle postupu probíraném postupně ve zbytku kapitoly. Začneme vytvářet vlastní třídu hry. Můžete vše sledovat a zkoušet realizovat ekvivalentní kód i ve své aplikaci. Začneme zkopírováním šablony hry z balíčku empty_classes. 1. Zkopírujte do svého balíčku třídu EmptyGame, která slouží jako šablona třídy hry. 2. Třídu ihned přejmenujte tak, aby název třídy odpovídal tématu hry. (Moje demonstrační hra se odehrává ve služebním bytě, a proto třídu nazvu RUPAppartmentGame.) 3. Upravte úvodní dokumentační komentář tak, že z něj smažete odstavce týkající se vývoje a vhodně upravíte texty tak, aby výstižně hovořily o vámi vytvářené třídě. 4. Pokud vám refaktorační modul při kopírování třídy automaticky vložil (refaktorační modul jej tam jednou vloží a jindy ne) příkaz import eu.pedu.adv15p_fw.empty_classes.*; tak jej smažte. Při pokusu o překlad této třídy zjistíte, že vám překladač hlásí, že se mu hlavička třídy nelíbí, a to hned z několika důvodů. (V profesionálních vývojových prostředích dokonce nebudete ani muset překládat, protože vás na tuto chybu upozorní již editor.) Překladač nezná třídu ANamed, kterou zkopírovaná třída prohlašuje za svého předka. Strana 148 z 351

149 Kapitola 8: Začínáme tvořit vlastní hru 149 Překladač nezná interfejs IAuthorPrototype, o němž vaše třída prohlašuje, že jej implementuje. Vaše třída neimplementuje některé metody vyžadované interfejsem IGame, k jehož implementaci se třída hlásí. Pojďme vyřešit jeden problém po druhém. 8.3 Předek ANamed Vzpomeňte si na diagram tříd balíčku game_txt (najdete jej na obrázku 7.1 na straně 144). V něm je definován interfejs INamed, který je společným předkem interfejsů IAction, IArea, IGame a IItem. Z toho vyplývá, že instance všech těchto čtyř interfejsů jsou pojmenované, tj. mají mít definovanou metodu getname(). Prázdné třídy jsou definovány tak, že třídy implementující tyto interfejsy mají definovaného společného třídního předka, jímž je abstraktní třída ANamed. Její konstruktor převezme ve svém parametru od svých potomků jejich název, a zapamatuje si jej. Třída pak pro své instance definuje vedle povinné metody getname() ještě metodu tostring(), která vrátí textový řetězec s názvem dané instance, což se může v řadě případů hodit. 5. Zkopírujte třídu ANamed do svého balíčku. Překladač ji automaticky pochopí jako předka před chvílí definované třídy hry. Nicméně se mu za chvíli nebude líbit, že konstruktor instancí tohoto předka vyžaduje předání parametru s názvem instance, což naše hra nečiní. Předejděte jeho budoucím stížnostem a: 6. Upravte tělo konstruktoru tak, aby konstruktoru svého rodičovského podobjektu předal název vaší hry. Podobu konstruktoru v ukázkové aplikaci si můžete prohlédnout ve výpisu 8.1. Výpis 8.1: Definice konstruktoru instancí třídy RUPApartmentGame 1 /*************************************************************************** 2 * Soukromý konstruktor definující jedinou instanci třídy hry. 3 */ 4 private RUPApartmentGame() 5 { 6 super("ukázková hra: Byt s inteligentní ledničkou"); 7 } Strana 149 z 351

150 Kapitola 8: Začínáme tvořit vlastní hru Interfejs IAuthorPrototype Překladač dále tvrdí, že nezná interfejs IAuthorPrototype. Tohoto problému se ale umíte snadno zbavit: 7. Nahraďte deklaraci implementace interfejsu IAuthorPrototype deklarací implementace vašeho autorského interfejsu, o němž jsme hovořili v podkapitole 2.6 Jméno a ID autora na straně 42 (v doprovodném příkladu je to interfejs IAuthorRUP). Tím by měly problémy s hlavičkou skončit. 8.5 Class-objekt tovární třídy Hlavička je sice vyřešena, ale chyba zůstává hned v první deklaraci, kterou je definice statické konstanty FACTORY_CLASS. Překladač hlásí, že třídu EmptyGSMFactory nezná. To to napravte 8. Nahraďte v deklaraci atributu FACTORY_CLASS název tovární třídy EmptyGSMFactory názvem vaší tovární třídy (v demonstračním doprovodném programu je to třída RUPGSMFactory). 9. V deklaraci atributu FACTORY_CLASS odstraňte v typovém parametru žolík 17 a změňte typový parametr na název vaší tovární třídy 18. V doprovodném projektu pak deklarace získá tvar private final static Class<RUPGSMFactory> FACTORY_CLASS = RUPGSMFactory.class; 10. Obdobně změňte i typ návratové hodnoty metody getfactoryclass(). V doprovodném projektu bude mít hlavička metody tvar: public Class<RUPGSMFactory> getfactoryclass() 8.6 Aktivace tovární metody Když už jsme u té tovární třídy, tak bychom ji měli upozornit na to, že třída hry již existuje. 17 Jako žolík (anglicky wild card) je v deklaracích typových parametrů generických typů a metod označován znak? (otazník). 18 Odstraňováním žolíků ve třídách potomků se podrobněji zabývá podšeděný blok Změna typu návratové hodnoty na straně 181 Strana 150 z 351

151 Kapitola 8: Začínáme tvořit vlastní hru Otevřete zdrojový kód své tovární třídy a odkomentujte v něm tělo metody getgame(). Název fiktivní třídy GameClass nahraďte názvem té svojí v mém případě názvem RUPAppartmentGame. 12. Změňte typ návratové hodnoty dané metody na typ svojí třídy hra. V doprovodném projektu tak tato metoda získá tvar z výpisu 8.2. Výpis 8.2: Definice metody getgame() ve třídě RUPGSMFactory 1 /*************************************************************************** 2 * Vrátí odkaz na instanci textové verze hry. 3 * 4 Požadovaný odkaz 5 */ 7 public RUPAppartmentGame getgame() 8 { 9 return RUPAppartmentGame.getInstance(); 10 } 8.7 Vyvíjíme podle TDD Nebudeme se nyní snažit nějak bádat nad tím, co bychom měli a neměli naprogramovat nejdříve a rozhodneme se, že pojedeme přesně podle metodiky TDD (viz pasáž Programování řízené testy na straně 51) a necháme dirigovat vývoj naší aplikace výsledky provedených testů. 13. Zaběhněte na konec zdrojového kódu správce scénářů a v metodě main(string[]) zakomentujte příkaz na spuštění autotestu a příkaz na spuštění simulace (máte-li jej odkomentovaný). 14. Odkomentujte následující příkaz na spuštění testu hry, tj. příkaz MANAGER.testGame(); Od této chvíle můžete začít vyvíjenou hru testovat. 15. Spusťte třídu svého správce scénářů (používáte-li BlueJ, spusťte její metodu main(string[])). První spuštění testu hry skončí podle očekávání vyhozením výjimky. Podívejme se na chybový výpis. Před vlastním testem hry se nejprve otestuje její správce scénářů. Tento test již známe jeho zpráva odpovídá té z výpisu 5.4 na straně 97. Hlášení pak pokračuje zprávou o výsledku testu vlastní hry. Začátek této zprávy testovacího programu (najdete jej ve výpisu 8.3) obsahuje pomocné informace doplněné případným chybovým hlášením. Strana 151 z 351

152 Kapitola 8: Začínáme tvořit vlastní hru 152 Výpis 8.3: Začátek chybového hlášení při prvním spuštění hry oznamující nedotaženou definici metody isalive() ve třídě RUPAppartmentGame 1 ############################################################################# 2 Správce scénářů úspěšně PROŠEL autotestem 3 ############################################################################# 4 ##### 5 ##### Test zadání: Základní verze 6 ##### 7 ############################################################################# 8 ##### Hra: Ukázková hra: Byt s inteligentní ledničkou 9 ##### Autor: RUP15P PECINOVSKÝ Rudolf 10 ##### Scénář: _HAPPY_ 11 ##### Třída hry: eu.pedu.adventure15p.rupapartmentgame 12 ##### Čas: Mon Aug 10 12:28:49 CEST ############################################################################# 14 ===== Test prováděných operací ===== ===== Konec testu prováděných operací ===== 17 ############################################################################# 18 ##### Hra: Ukázková hra: Byt s inteligentní ledničkou 19 ##### Autor: RUP15P PECINOVSKÝ Rudolf 20 ##### Scénář: _HAPPY_ 21 ##### Třída hry: eu.pedu.adventure15p.rupapartmentgame 22 ##### Čas: Mon Aug 10 12:28:49 CEST ############################################################################# Ukončení bylo způsobeno vyhozením výjimky: 26 java.lang.unsupportedoperationexception: 27 Metoda ještě není hotova. 28 at eu.pedu.adventure15p.rupappartmentgame.isalive(rupappartmentgame.java:114) 29 at eu.pedu.adv15p_fw.test_util.tgametests.tgameruntester.verifyisalive( TGameRunTester.java:254) 30 atd. Z chybového hlášení je vše jasné: na řádcích 19 až 22 výpisu 8.3 se dovídáme, že ve třídě RUPAppartmentGame byla na řádku 114 (u vás se může řádek lišit) vyvolána výjimka UnsupportedOperationException, ohlašující, že ještě není hotova definice metody isalive(?), která žadateli prozradí, jestli je hra běžící, anebo jenom čekající na spuštění. Nezbývá, než definici metody opravit. Výpis obsahu zásobníku návratových adres neuvádí parametry metod, při jejichž volání došlo k uvedené výjimce. Proto budu při odkazu na metody uvedené v tomto výpisu používat vždy místo seznamů typů parametrů pouze otazník, i když budu vědět, jaké parametry daná metoda má. Strana 152 z 351

153 Kapitola 8: Začínáme tvořit vlastní hru 153 Prozatím je hra ve výrobě, tak zvolíme cestu nejmenšího odporu a definujeme metodu tak, že bude vracet vždy false oznamující, že hra zatím neběží. Až se dostaneme do stavu, že se hra spustí, metodu znovu upravíme. 16. Nahraďte v těle metody isalive() příkaz vyhazující výjimku příkazem vracejícím hodnotu false. Definice metody isalive() proto bude prozatím vypadat podle výpisu 8.4. Výpis 8.4: Definice metod isalive() ve třídě RUPAppartmentGame 2 public boolean isalive() 3 { 4 return false; 5 } 17. Znovu spusťte správce scénářů. Běh programu opět (a opět podle očekávání) skončí vyhozením výjimky viz výpis 8.5, v němž je, jak jsem sliboval, vynechána počáteční informace o autorovi, hře a prověřovaném scénáři. Výpis obsahu zásobníku jsem tentokrát opsal celý, protože klíčová informace je až v druhé polovině. Příště budu zkracovat i ten. Výpis 8.5: Klíčová část hlášení o průběhu testu po dokončení definice metody isalive() ve třídě RUPAppartmentGame oznamující nedotaženou definici metody executecommand(?) ve třídě RUPAppartmentGame 1 Ukončení bylo způsobeno vyhozením výjimky: 2 eu.pedu.adv15p_fw.test_util.common.testexception: 3 Při vykonávání příkazu: 4 vyhodila hra výjimku UnsupportedOperationException 5 at eu.pedu.adv15p_fw.test_util.tgametests.tgameruntester.verifyscenariostep( TGameRunTester.java:275) 6 at eu.pedu.adv15p_fw.test_util.tgametests.tgameruntester.executescenario( TGameRunTester.java:141) 7 at eu.pedu.adv15p_fw.test_util.tgametests.tgametester.testgamebyscenario(tgametester.java:176) 8 at eu.pedu.adv15p_fw.test_util.tgametests.tgametester.testgamebyscenarios(tgametester.java:158) 9 at eu.pedu.adv15p_fw.test_util.tgametests.tgametester.testgame(tgametester.java:141) 10 at eu.pedu.adv15p_fw.scenario.ascenariomanager.testgame(ascenariomanager.java:411) 11 at eu.pedu.adventure15p.rupscenariomanagercon.main(rupscenariomanagercon.java:642) 12 at eu.pedu.adventure15p._108z.main(_108z.java:25) 13 Caused by: java.lang.unsupportedoperationexception: 14 Metoda ještě není hotova. 15 at eu.pedu.adventure15p.rupapartmentgame.executecommand(rupapartmentgame.java:191) 16 at eu.pedu.adv15p_fw.test_util.tgametests.tgameruntester.verifyscenariostep( TGameRunTester.java:270) more Strana 153 z 351

154 Kapitola 8: Začínáme tvořit vlastní hru 154 Z výpisu vyčteme, že (řádky 1 až 3): Ukončení bylo způsobeno vyhozením výjimky: Při vykonávání příkazu:. test už chtěl spustit startovací akci, tj. akci, jejímž názvem je prázdný řetězec. (Aby se snadno poznalo, jestli zobrazovaný text není prázdný řetězec, uzavírají jej testy našeho rámce do francouzských uvozovek [např. «text»]. Prázdný řetězec se pak zobrazí.) Na začátku výpisu zásobníku odkazů se dozvíme, že výjimku vyhodila metoda verifyscenariostep(?) ve třídě TGameRunTester, která je součástí frameworku. Nebudete-li chtít analyzovat framework, tak vám tato informace moc nepomůže. Naštěstí se ale dále ve výpisu dá zjistit, co bylo příčinou vyhození této výjimky. Jak byste si měli pamatovat z výkladu práce s výjimkami, příčinu standardně oznamuje řádek začínající textem Caused by:. Ten najdeme ve výpisu 8.5 na řádku 13 (aby se vám lépe hledal, tak jsem jej zvýraznil). Zde (resp. na následujících řádcích) se také dozvíme, že iniciační výjimka byla vyhozena opět ve třídě RUPAppartmentGame, a to na řádku 191 v metodě executecommand(?). A už je to jasné musíme upravit definici metody executecommand(string). 8.8 Kdo má mít na starosti zpracování příkazů Tady se ale musíme zamyslet: opravdu by zadané příkazy měla zpracovávat třída hry? Tři skupiny objektů Měli bychom si uvědomit, že objekty vyskytující se v programech bychom mohli rozdělit do tří skupin: Objekty, jejichž hlavním úkolem je oprašovat nějaká data. Sem bychom mohli zařadit např. různé kontejnery (přepravky, pole, kolekce, ). Objekty, jejichž hlavním úkolem je něco spočítat nebo jinak zpracovat. Sem bychom mohli zařadit např. přesouvač z našich hrátek s autíčky z mých učebnic základů programování. Objekty, jejichž hlavním úkolem je organizovat spolupráci ostatních objektů. Jestli si vzpomínáte na návrhový vzor Prostředník, tak to je takový typický organizátor, který zprostředkovává komunikaci ostatních objektů. V každé jenom trochu větší aplikaci by měl být nějaký organizátor. Většinou jich najdete hned několik. V naší aplikaci si o roli hlavního organizátora říká objekt Strana 154 z 351

155 Kapitola 8: Začínáme tvořit vlastní hru 155 hry. Měli bychom jej proto definovat tak, aby se soustředil na delegování různých činností na specializované objekty, které pak danou činnost provedou. Jak jsou na tom příkazy/akce? Zpracování příkazů bude v naší hře důležitá činnost, kterou by měl mít na starosti nějaký specializovaný objekt. Tento objekt by se pak mohl současně starat o objekty jednotlivých akcí a spravovat je. Jak už jsme si řekli na počátku kapitoly 4.1 Interfejs IScenarioManager na straně 67, jakmile se v programu vyskytne větší množství nějakých objektů, tak by v něm měl existovat i nějaký správce, který se o tyto objekty stará. Proto bychom měli v naší aplikaci definovat specializovaného správce akcí. Pojďme se proto podívat do rámce, co vše by naše akce měly umět, a pokusme se z toho odvodit požadavky na jejich správce. Víme, že každá akce má umět prozradit svůj název a popis. Kromě toho musí umět realizovat požadovanou činnost. Realizovaná činnost se akci od akce podstatně liší. Prozrazování názvu a popisu je však pro všechny stejné. Bylo by proto vhodné je delegovat na nějakého společného rodiče všech akcí. Tomu by každá akce prozradila svůj název, přičemž popis a definici přístupových metod, které tyto informace na požádání vracejí, by ponechala na tomto společném rodiči. Definice by tak mohla být na jediném místě a všechny akce by tyto metody sdílely. Z principu dědění vyplývá, že při tvorbě každé akce by se musel nejprve vyrobit její rodičovský podobjekt. Všechny akce by tedy volaly společný konstruktor rodičovského podobjektu. To je přesně to místo, kde by se případný správce mohl o objektech, jejichž správu má na starosti, dozvědět vše potřebné hned při jejich vzniku. Od těchto úvah je již krůček k tomu, aby se správcem akcí stal objekt jejich rodičovské třídy, tj. aby rodičovská třída spravovala všechny vytvořené instance svých potomků. Třída si zřídí nějaký kontejner (případně několik kontejnerů), do nějž (do nichž) bude ukládat vše, co bude o svých svěřencích potřebovat vědět. Rodičovský konstruktor akcí, který je jednou z metod tohoto společného rodiče, tak můžeme jednoduše pověřit tím, aby naplnil připravený kontejner (připravené kontejnery) potřebnými údaji. Strana 155 z 351

156 Kapitola 8: Začínáme tvořit vlastní hru 156 Dva druhy správců Naše úvahy o správci akcí a před tím o správci scénářů bychom mohli zobecnit. Při úvahách o tom, jak definovat správce objektů si můžeme vybrat ze dvou možností: Definovat správce jako instanci speciální jednoúčelové třídy. Ve většině případů je vhodné definovat takovéhoto správce jako jedináčka. Tak jsme to učinil např. s naším správcem scénářů. Znáte-li moje začátečnické učebnice, tak v nich je podobně definován i správce plátna. Definovat jako správce objekt třídy, jejíž instance je třeba spravovat. Tak jsme definovali správce akcí, i když ten ve skutečnosti spravuje instance svých potomků. Nicméně, jak víme z úvodního kurzu, instance potomků jsou současně instancemi své rodičovské třídy, takže se tím původní charakteristika vlastně nemění. Obě řešení mají své výhody a nevýhody. Správce jako instance samostatné třídy Výhodou správce definovaného jako instance nějaké třídy je to, že jej můžeme uchovávat v proměnné a předávat jej tak tomu, kdo jej bude potřebovat. Mnozí jeho uživatelé tak ani nemusejí vědět, kdo přesně je správcem je to objekt, který nějakým způsobem obdrželi. Nevýhodou takto definovaného správce je naopak to, že musíme definovat dva nové objekty (třídu a její instanci), přičemž k řádné implementaci vzoru Jedináček musí třída navíc splňovat několik dodatečných podmínek. Další nevýhodou je to, že takto definovaný správce nemá přístup k soukromým členům spravovaných instancí, takže musíme některé z nich buď zveřejnit, anebo zprostředkovat nějaký náhradní mechanismus. Správce jako objekt třídy Výhodou správce definovaného jako objekt třídy, jejíž instance spravuje, je to, že jeho objekt nemusíme definovat, protože už existuje a stačí jej pouze něco naučit (tj. definovat metody a atributy potřebné k vykonávání správy). Další výhodou je to, že má přístup i k soukromým složkám svých instancí, i když v případě potomků jenom k těm, které jsou umístěny v rodiči správci. Nevýhodou takto definovaného správce je to, že jej nemůžeme umístit do proměnné a svobodně předávat mezi objekty. Všichni, kdo se na něj potřebují obracet, proto musejí přesně vědět, kdo to je. (Teoreticky bychom se na něj mohli obracet přes jeho class-objekt, ale tím se komplikují jiné věci.) Strana 156 z 351

157 Kapitola 8: Začínáme tvořit vlastní hru Definice správce akcí třídy AAction Po těchto úvahách půjdeme definovat třídu AAction, která bude rodičovskou třídou všech akcí. Naštěstí pro nás má framework její šablonu připravenou v balíčku empty_classes. 18. Zkopírujte do svého balíčku třídu EmptyAAction a hned ji přejmenujte odstraňte z jejího názvu slovo Empty 19 (za chvíli beztak prázdná nebude). 19. Pokud po otevření zdrojového kódu třídy v okně editoru zjistíte, že vám refaktorační modul vložil do zdrojového kódu příkaz import eu.pedu.adv15p_fw.empty_classes.*; tak jej smažte. Není důvod mít nadále s balíčkem šablon prázdných tříd cokoliv společného. (Příště už na to upozorňovat nebudu. Předpokládám, že na to zvládnete myslet sami.) Třída EmptyAAction má již definovanou výchozí verzi konstruktoru, která od potomka převezme jeho název a popis. Název předá konstruktoru svého rodičovského podobjektu, jenž je instancí třídy ANamed (jak jsme si řekli, ta zabezpečí definici metody getname()). Popis si zapamatuje a na požádání vrátí jako hodnotu metody getdescription(), jejíž definice je ve zkopírované třídě již připravena. Modifikátor přístupu Vraťme se ale ke třídě AAction. Při prohlídce jejího zdrojového kódu uvidíte, že třída není definována jako veřejná. Je to proto, že slouží pouze pro vnitřní potřebu hry, a nikdo cizí se na proto nemá mít možnost obracet. (Problematice správného nastavení přístupových práv je věnován podšeděný blok Přístupová práva a jejich vlastnosti.) 19 V dalším textu budu předpokládat, že i ve vašem projektu se tato třída jmenuje AAction. Strana 157 z 351

158 Kapitola 8: Začínáme tvořit vlastní hru 158 Přístupová práva a jejich vlastnosti V základním kurzu jsme to probírali několikrát, ale některé věci je třeba občas připomenout. Jednou z nejvíce oceňovaných vlastností objektově orientovaného programování je jeho schopnost zapouzdřit do objektu data a kód, který s nimi pracuje, a současně skrýt před zbytkem světa detaily implementace. Občas je ale potřeba některým objektům část těchto detailů zveřejnit, a proto OOP jazyky nabízejí možnost nastavit několik úrovní přístupových práv. Programátor by měl proto vždy dbát na to, aby jím nastavená přístupová práva byla co nejpřísnější a aby přístup k implementačním detailům byl povolen co neužší skupině objektů. Některé studenty ale mate skutečnost, že instance neveřejné (a někdy dokonce i soukromé) třídy má veřejné metody, k nimž mohou cizí objekty přistupovat. Je to proto, že tyto instance nevystupují vůči okolí (tj. vůči objektům, které tyto metody volají) jako instance dané třídy, ale jako instance rozhraní deklarovaného některým z předků (interfejsem nebo rodičovskou třídou) jejich mateřské třídy v našem případě jako instance rodičovské třídy ANamed, nebo jako instance prarodičovské třídy Object, nebo jako instance interfejsu IAction, nebo jako instance jeho rodičovského interfejsu INamed. Pokud tedy třída přebíjí některou z veřejných virtuálních metod některého ze svých předků, musí tuto metodu deklarovat jako veřejnou, protože jinak by se její instance nemohly plnohodnotně vydávat za instance příslušného předka, čímž by byla porušena zásada LSP (podrobněji se o ní dočtete v podšeděném bloku LSP (Liskov Substitution Principle) Substituční princip Liskové na straně 244). Kromě toho by překladač označil deklaraci takovéto metody jako syntaktickou chybu, protože přístupová právy metody potomka nesmí být přísnější než práva odpovídající metody předka. Konstruktor Třída má již definovanou výchozí verzi konstruktoru, která od potomka převezme jeho název a popis. Název předá konstruktoru svého rodičovského podobjektu, jenž je instancí třídy ANamed (jak jsme si řekli, ta zabezpečí definici metody getname()). Popis si zapamatuje a na požádání vrátí jako hodnotu metody getdescription(), jejíž definice je ve zkopírované třídě již připravena. Neimplementovaná abstraktní metoda V definici zkopírované třídy je ještě abstraktní metoda s hlavičkou Strana 158 z 351

159 Kapitola 8: Začínáme tvořit vlastní hru 159 abstract public String execute(string... arguments); zděděná od implementovaného interfejsu IAction. O tu se nyní starat nebudeme, o její definici se musejí postarat potomci, každý po svém. Nad definicí této metody začneme přemýšlet až v okamžiku, kdy budeme definovat jednotlivé akce. Teoreticky by tu tato deklarace vůbec nemusela být abstraktní třída nemusí deklarovat abstraktní metody zděděné od svých předků, pokud se je rozhodne neimplementovat. Deklarace zděděné, neimplementované abstraktní metody je však užitečnou připomínkou toho, že tato metoda na svoji definici teprve čeká a že ji všichni konkrétní potomci dané třídy musejí definovat. Příprava definice správce S třídou jsme se již seznámili, takže bychom měli začít přemýšlet nad tím, jak vše připravit k tomu, aby objekt třídy mohl pracovat jako správce instancí své třídy. Pojďme se nejprve zamyslet nad tím, co k tomu bude potřebovat. Začneme odhadem jeho budoucí činnosti: 1. Vše začne tím, že správce převezme text příkazu, jenž byl zadán uživatelem. 2. Tento text bude potřeba rozdělit na slova. 3. Zanalyzuje první slovo, které je klíčové, protože určuje, kterou akci chce uživatel provést. 4. Zjistí, který objekt akce je třeba o zpracování daného příkazu požádat. 5. Zavolá metodu dané akce zodpovědnou za zapracování příkazu, předá jí obdržené informace a další zpracování příkazu nechá na oslovené akci. Z předchozího vyplývá, že pro zajištění bodu 4 by bylo vhodné mít k dispozici pomocný objekt, kterému předáme název akce a on nám vrátí objekt (akci), který bude mít na starosti zabezpečení té správné reakce. Takovýto nástroj ale známe je jím mapa. Klíčem bude název příkazu a hodnotou pak bude odkaz na objekt dané akce. 20. Definujte ve třídě AAction statickou konstantu NAME_2_ACTION 20 definující mapu s typovými parametry String (název akce) a AAction (objekt reprezentující danou akci), tj. vložte do sekce statických konstant deklaraci /** Mapa zprostředkovávající převod názvu akce na její instanci. */ private static final Map<String, AAction> NAME_2_ACTION; 20 Teoreticky ji můžete pojmenovat po svém, ale protože se na ni budu v dalším textu odvolávat, doporučuji použít doporučený název. Strana 159 z 351

160 Kapitola 8: Začínáme tvořit vlastní hru 160 Ve svých programech jsem pro názvy map zavedl konvenci převzatou od vášnivých pisatelů SMS a zkratkovitých mailů. Číslice 2, která je v ní použita, symbolizuje stejně vyslovované anglické slovo to. Název naší konstanty bychom tedy mohli číst name to action a přeložit si jej tak, že převádí jméno na akci. V případě, že daná mapa představuje statickou konstantu, která se podle konvencí píše velkými písmeny, je dvojka od svých sousedů oddělena podtržítky jako v případě identifikátoru NAME_2_ACTION. Jedná-li se o běžnou proměnnou, je identifikátor bez podtržítek např. name2action. Mapu můžeme definovat jako konstantu, protože se sice bude měnit její obsah, ale vlastní mapu nikdo vyměňovat nebude. (Je to stejné, jako když chodíte s taškou na nákup: nákup se mění, ale taška zůstává stále táž.) Konstruktor akce (přesněji konstruktor jejího rodičovského podobjektu typu AAction) pak do této mapy při každé konstrukci nového objektu uloží položku, kde klíč bude tvořit název akce a hodnotu odkaz na vytvářenou akci. Úprava definice konstruktoru akcí Pojďme se tedy podívat na konstruktor rodičovských podobjektů jednotlivých akcí. Říkali jsme si, že má při konstrukci akce vždy uložit do mapy NAME_2_ACTION dvojici tvořenou názvem akce a odkazem na danou akci. Při práci s názvy však musíme dávat pozor na velikost písmen. Lze odhadnout, že někteří uživatelé budou při zadávání názvů akcí dávat přednost malým písmenům a jiní velkým. Měli bychom se proto rozhodnout o přesné podobě klíče a tomu pak později přizpůsobit zadávání a vyhledávání těchto názvů. Já jsem se rozhodl, že v klíčích budu používat pouze malá písmena. Před uložením klíče do mapy jej proto vhodné nejprve převést jeho znaky na vybranou velikost písmen. Rozhodnete-li se, že v mapě budou názvy akcí malými písmeny, pak pro vložení dvojice [název, akce] do mapy použijete příkaz (nacházíme se ve třídě AAction, kde this představuje instanci dané akce): NAME_2_ACTION.put(name.toLowerCase(), this); Tato definice ještě není dokonalá. Dokumentace totiž říká, že při vložení dvojice [klíč; hodnota] do mapy vrátí metoda put() minulou hodnotu přiřazenou danému klíči. Vrátí-li null, je vše v pořádku. Vrátí-li něco jiného, znamená to, že jsme někde udělali chybu a snažíme se do mapy vložit druhou akci se stejným názvem. Definici bychom proto měli upravit tak, aby při vložení již existujícího názvu konstruktor vyhodil výjimku. Definici takto upraveného konstruktoru si můžete prohlédnout ve výpisu 8.6. Strana 160 z 351

161 Kapitola 8: Začínáme tvořit vlastní hru 161 Výpis 8.6: Upravená definice konstruktoru instancí třídy AAction 1 /*************************************************************************** 2 * Vytvoří rodičovský podobjekt vytvářené akce. 3 * 4 name Název vytvářené akce = text, který musí hráč zadat 5 * jako počáteční slovo zadávaného příkazu 6 description Stručný popis vytvářené akce 7 */ 8 AAction(String name, String description) 9 { 10 super(name); 11 this.description = description; 12 AAction previous = NAME_2_ACTION.put(name.toLowerCase(), this); 13 if (previous!= null) { 14 throw new IllegalArgumentException( 15 "\nakce s názvem «" + name + "» byl již vytvořena"); 16 } 17 } 21. Upravte ve třídě AAction její konstruktor instancí do podoby ekvivalentní s definicí ve výpisu 8.6. Konstruktor správce a vytvoření jednotlivých akcí Konstruktor jsme sice připravili, ale prozatím jsme neuvažovali o tom, kdo jednotlivé akce vytvoří, tj. kdo zkonstruuje objekty jednotlivých akcí. Nejlepším vykonavatelem takovéhoto úkolu by měl být jejich pozdější správce. Před chvílí jsme si ale řekli, že správcovstvím pověříme objekt třídy AAction. Nyní je třeba se rozhodnout, kdy se tento objekt o vytvoření příslušných akcí postará a jak to naprogramujeme. Nejlepší by bylo, kdyby se o vytvoření jednotlivých spravovaných objektů postaral už při svém vzniku, aby jej pak o to později nemusel nikdo žádat. Jinými slovy, definice jednotlivých akcí by měla být součástí konstruktoru daného objektu. A protože je tento objekt objektem třídy, umístíme vytváření nových akcí do konstruktoru třídy AAction, tj. do jejího statického inicializačního bloku. Zatím ještě sice žádné akce nevytváříme, ale můžeme si už připravit statický inicializační blok. V něm pak podle zásady prezentované v podšeděném bloku Kde inicializovat inicializujeme konstantu NAME_2_ACTION. 22. Připravte ve třídě AAction (v sekci vyhrazené pro konstruktor třídy) statický inicializační blok a vložte do něj příkaz, jenž vloží do konstanty NAME_2_ACTION prázdnou mapu (viz výpis 8.7). Strana 161 z 351

162 Kapitola 8: Začínáme tvořit vlastní hru 162 Výpis 8.7: Výchozí podoba statického inicializačního bloku ve třídě AAction spolu s jeho umístěním do správné sekce 1 //############################################################################## 2 //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== 3 4 static { 5 NAME_2_ACTION = new HashMap<>(); 6 } //== CLASS GETTERS AND SETTERS ================================================= Kde inicializovat Konstantám se musí jejich hodnota přiřadit buď v deklaraci, anebo v konstruktoru. V dřívějších dobách se doporučovalo inicializovat konstanty hned při jejich deklaraci samozřejmě pouze pokud to bylo možné. Když nebylo možné inicializovat příslušný atribut v deklaraci, inicializoval se v konstruktoru. V současné době sílí mezi programátorskými guru přesvědčení, že není vhodné inicializovat atributy pokud možno v deklaraci, protože pak jsou jejich inicializace rozesety na více místech zdrojového kódu a programátor o nich ztrácí přehled. Poslední dobou se proto prosazuje zásada: Vystačíme-li s prázdným tělem konstruktoru, můžeme jednotlivé atributy inicializovat v jejich deklaracích. Všechny deklarace atributů mají být beztak pohromadě, takže pak budou pohromadě i jejich inicializace. Jakmile však potřebujeme inicializovat libovolný atribut v těle konstruktoru (a teď je jedno, jestli se jedná o konstruktor třídy nebo konstruktor instancí), tak by se měly všechny deklarace příslušných (tj. třídních či instančních) atributů přestěhovat do odpovídajícího konstruktoru. Předchozí zásada se samozřejmě nevztahuje na situace, kdy v době konstrukce objektu stále ještě nemáme dost podkladů pro inicializaci některého atributu a pro jeho inicializace musíme definovat nějakou separátní metodu, kterou nemusí být jen nastavovací přístupová metoda (setr), ale může to být i jednoúčelová inicializační metoda, jakou byla např. metoda setdelegate(ascenariomanager) ve výpisu 6.1 na straně 114, anebo metoda setchild(rupscenariomanagerh) ve výpisu 6.6 na straně 120. Strana 162 z 351

163 Kapitola 8: Začínáme tvořit vlastní hru 163 Úprava metody execute(string) ve třídě hry Z našeho budování správce akcí se nějak vytratilo, že jsme to vše rozpoutali jako reakci na chybové hlášení z výpisu 8.5 na straně 153, z nějž jsme se dozvěděli, že ve třídě hry ještě není korektně definováno tělo metody execute(string), která má na starosti reakci na zadané příkazy. V pasáži Tři skupiny objektů na straně 154 jsme si řekli, že objekt hry by měl vystupovat jako organizátor, který rozděluje práci mezi podřízené objekty. Podřízeným objektem zodpovědným za zpracování příkazu má být správce akcí, tj. objekt třídy AAction, kterou jsme začali budovat. Na něj by proto měla hra zpracování příkazu delegovat. Z toho vyplývá i následující krok: 23. Vraťte se do třídy hry a upravte definici těla metody executecommand(string) tak, že zodpovědnost za správnou reakci na zadaný příkaz deleguje na správce akcí, tj. na třídu AAction. Metoda ve třídě hry tedy získá podobu z výpisu 8.8. Výpis 8.8: Definice metody executecommand(string) ve třídě RUPAppartmentGame 1 /*************************************************************************** 2 * Zpracuje zadaný příkaz a vrátí text zprávy pro uživatele. 3 * Vlastní zpracování příkazu ale deleguje na správce akcí, 4 * kterým je objekt třídy {@link AAction}. 5 * 6 command Zadávaný příkaz 7 Textová odpověď hry na zadaný příkaz 8 */ 10 public String executecommand(string command) 11 { 12 return AAction.executeCommand(command); 13 } 8.10 Definice metody pro provádění příkazů Překladač prozatím ještě třídu hry nepřeloží, protože volaná metoda správce akcí executecommand(string) ještě neexistuje. Jdeme to tedy napravit. Abyste měli život co nejjednodušší, začněte následovně: 24. Zkopírujte definici metody executecommand(string) ze třídy hry do třídy AAction do sekce ostatních nesoukromých metod třídy, tj. do sekce other nonprivate class methods. To samozřejmě překladači stačit nebude, protože jste zkopírovali instanční metodu a příkaz na řádku 12 ve výpisu 8.8 jasně volá metodu třídy. Navíc je metoda Strana 163 z 351

164 Kapitola 8: Začínáme tvořit vlastní hru 164 uvozena označující, že metoda přebíjí odpovídající metodu předka. Abyste překladač uklidnili, tak: 25. Smažte ve třídě AAction před hlavičkou metody řádek s 26. Přidejte zkopírované metodě ve třídě AAction modifikátor static. Tím z ní uděláte řádnou metodu třídy a překladač se uklidní. Dva režimy zpracování příkazů Překladač se sice uklidnil, ale náš problém to ještě neřeší. Hra delegovala zodpovědnost na správce akcí, ale nyní bychom měli začít přemýšlet, jak definovat tělo metody tak, aby opravdu řešilo daný problém. Když se nad touto problematikou na chvíli zamyslíte, brzy si uvědomíte, že program pracuje ve dvou režimech: jinak se chová, když hra neběží, a jinak, když hra běží. Když hra neběží, tak očekává, že bude zadán prázdný příkaz k odstartování hry. Jakékoliv jiné zadání považuje za špatné. Když hra běží, očekává, že bude zadán příkaz vyvolávající některou z definovaných akcí. Zadání libovolného jiného příkazu včetně prázdného je opět špatně. Tomu by měla odpovídat i definice metody zpracovávající zadaný příkaz. To je jeden pohled. Stejně dobře se ale můžeme rozhodovat i podle toho, zda byl či nebyl zadán prázdný příkaz. V případě zadání prázdného příkazu (a obdobně v případě zadání příkazu tvořeného pouze bílými znaky) máme opět dvě možnosti: Pokud hra neběží, tak se hra inicializuje, odstartuje a metoda vrátí uvítací zprávu. Pokud hra běží, metoda vrátí zprávu o chybně zadaném příkazu. V případě zadání neprázdného příkazu se opět musíme rozhodnout podle toho, zda hra běží či neběží: Pokud hra neběží, tak metoda vrátí zprávu o chybně zadaném příkazu. Pokud hra běží, analyzuje se zadaný příkaz. Je-li počáteční slovo příkazu názvem známé akce, předá se zpracování příkazu příslušné akci a vrátí se od ní odpověď. Není-li počáteční slovo příkazu názvem některé známé akce, metoda vrátí zprávu o chybně zadaném příkazu. Strana 164 z 351

165 Kapitola 8: Začínáme tvořit vlastní hru 165 Obě řešení jsou více méně ekvivalentní. Můžete si hodit kostkou či mincí a vyberete postup podle toho, co padne. Mně padla jednička, takže jsem vybral to první. K jeho zpracování musím nejprve korektně vyřešit metodu zjišťující, zda naše hra právě běží, tj. metodu isalive() ve třídě hry. Metoda isalive() ve třídě hry Tuto metodu jsme již definovali (viz výpis 8.4 na straně 153), ale jak si jistě vzpomenete, byla to pouze prozatímní definice. Jistě se mnou budete souhlasit, že ten, kdo se první dozví o tom, že se hra rozběhla, resp. že hra skončila, je správce akcí. Nebylo by asi moudré, kdyby měl vždy předávat informaci o zahájení či ukončení hry instanci hry, které by se pak na počátku metody executecommand(string) na tento stav ptal. Daleko výhodnější bude, když si bude správce pamatovat vše sám, a pokud náhodou hru někdo o tuto informaci požádá, tak hra deleguje požadavek na svého správce akcí, který ji tuto informaci poskytne. Upravte definici metody isalive() ve třídě hry do podoby ve výpisu 8.9. Výpis 8.9: Upravená definice metody isalive() ve třídě RUPAppartmentGame 2 public boolean isalive() 3 { 4 return AAction.isAlive(); 5 } Metoda isalive() ve správci akcí AAction Nyní budeme postupovat obdobně, jako jsme před chvílí postupovali s metodou executecommand(string): 27. Zkopírujte metodu isalive() do třídy správce akcí, tj. do třídy AAction do sekce přístupových metod objektu třídy, tj. do sekce class getters and setters. 28. U zkopírované metody odstraňte 29. U zkopírované metody doplňte modifikátor static. Tato metoda by měla zveřejňovat informaci, kterou je třeba si někde pamatovat nejlépe v nějakém atributu. Při každém startu hry uložíme do daného atributu hodnotu true (hra běží) a po ukončení hry do něj uložíme hodnotu false. Přitom bude jedno, zda hra skončila přirozeně nebo vynuceně. Využijeme toho, že v Javě se atribut může jmenovat stejně jako metoda, protože to, že voláme metodu, překladač snadno pozná podle závorek za jejím názvem. Strana 165 z 351

166 Kapitola 8: Začínáme tvořit vlastní hru 166 Nesmíme jenom zapomenout na to, že tento atribut je určen pro správce akcí, tj. pro objekt třídy, a proto musí být definován jako atribut třídy: 30. Ve třídě AAction definujte v sekci proměnných atributů třídy, tj. v sekci variable class attributes, atribut isalive. Hra musí v každém okamžiku vědět, jestli právě běží či neběží. Proto by tento atribut měl mít od počátku platnou hodnotu. Při vytváření objektu se sice všechny neinicializované atributy vynulují, takže by ten náš získal hodnotu false, která se nám zrovna hodí, ale přesto bych jej doporučil inicializovat. Tím zvýrazníme, že jsme na jeho inicializaci nezapomněli a že jeho implicitní počáteční hodnota není jenom náhodou správně. Moderní trendy programování doporučují inicializovat všechny atributy na jednom místě zdrojového kódu. Protože jsme již ve třídě AAction vytvořili statický inicializační blok, umístíme inicializaci atributu do něj. 31. Vložte do statického konstruktoru (statického inicializačního bloku) příkaz přiřazující atributu isalive hodnotu false. Definice metody isalive() ve správci akcí je soukromou záležitostí aplikace, takže by metoda neměla být definována jako veřejná, ale jako soukromá v balíčku (package private). Deklarace atributu isalive i jeho inicializace a definice metody isalive() jsou triviální, ale pokud si je chcete prohlédnout, najdete je ve výpisu 8.10 na straně 169. Tady bych chtěl ještě jednou upozornit na oblíbenou chybu, kdy voláte bezparametrickou metodu, zapomenete napsat prázdné závorky a strašně se divíte, že překladač tvrdí, že takovou proměnnou nezná. Před chvílí jste viděli proč: identifikátor, který není následován kulatými závorkami, je považován za název proměnné či konstanty a naopak identifikátor následovaný kulatými závorkami, je považován za název metody. Definice metody executecommand(string) Nyní se můžeme konečně definovat metodu executecommand(string). Její definice bude jednoduchá: Pokud hra ještě neběží, začne metoda řešit start hry. Aby se nám kód metody příliš nenatahoval, pověříme tím samostatnou metodu, jíž předáme zadaný příkaz. Metoda má na starosti odstartování hry, takže ji nazveme startgame. Strana 166 z 351

167 Kapitola 8: Začínáme tvořit vlastní hru 167 Pokud hra již běží, jedná se pravděpodobně o zadání dalšího příkazu. Jeho provedením opět pověříme samostatnou metodu. Metoda bude mít na starosti zpracování běžného příkazu, takže ji nazveme executecommoncomand. Před vlastní rozhodování bychom mohli ještě doplnit příkaz, který ze zadaného příkazu odstraní případné počáteční a koncové mezery 21. Tato znaky jsou pro uživatele do jisté míry neviditelné a mohl by je proto do zadávaného příkazu vložit omylem. Bylo by škoda, kdyby kvůli těmto omylem vloženým znakům hra začala tvrdit, že uživatel zadal neznámý příkaz. Tuto čistící akci není moudré odkládat, protože pak bychom ji museli provést v obou právě volaných metodách. Příkaz pro odstranění případných počátečních a koncových mezer proto zadáme ještě před tím, než se začneme rozhodovat, zda se hra bude startovat, anebo zda se má provést řadový příkaz. 32. Pokuste se samostatně definovat metodu executecommand(string). Svoje řešení můžete porovnat se vzorovým, které najdete ve výpisu 8.10 na straně 169 na řádcích 67 až Metoda pro spuštění hry startgame(string) Metoda pro spuštění hry musí nejprve zjistit, jestli byl zadán správný startovací příkaz. Při definici metody execute(string) jsme již ze zadávaného příkazu odstranili případné počáteční a závěrečné bílé znaky, čímž jsme uživateli odpustili případné zapomenuté mezery, tabulátory apod. Nyní by už měl být korektním zadávaným příkazem pouze prázdný řetězec. Bude-li tedy příkazem opravdu prázdný řetězec, můžeme korektně odstartovat hru a vrátit řetězec se standardním uvítáním. Tento textový řetězec převezmeme z definice startovního kroku ve správci akcí. Nebude-li zadaným příkazem prázdný řetězec, pošleme uživateli zprávu, která jej upozorní, že hru lze odstartovat pouze prázdným příkazem. Podobu této zprávy definuje počáteční krok chybového scénáře, který právě tuto situaci testuje. (Připomínám, že primárně pracuji se správcem scénářů využívajícím konstanty.) 33. Pokuste se samostatně definovat metodu startgame(string). Svoje řešení můžete porovnat se vzorovým, které najdete ve výpisu 8.10 na straně 169 na řádcích 114 až Oficiálně se sice říká počáteční a koncové bílé znaky, ale ve skutečnosti jsou to všechny znaky s kódem menším než 0x0020, což je kód mezery. Strana 167 z 351

168 Kapitola 8: Začínáme tvořit vlastní hru Metoda pro zpracování běžného příkazu executecommoncomand(string) Zpracování běžného příkazu bude trochu složitější, protože nejprve musíme v zadaném textu najít název zadávané akce. To učiníme nejsnáze tak, že rozdělíme zadávaný text na slova. První slovo bude název zadávané akce, další slova budou parametry. Rozdělení příkazového řádku na slova K tomuto účelu lze využít metody split(string), kterou mají ve svém portfoliu instance třídy String. Parametrem metody je regulární výraz specifikující text, který odděluje části textu. Metoda vrací pole textových řetězců ( stringů ) oddělených textem zadaným v parametru. Musíme si nejprve ujasnit, jakou podobu by měl mít onen regulární výraz. Pokud by byla jednotlivá slova příkazu oddělena vždy jednou mezerou, stačilo by jako regulární výraz zadat mezeru. Jakmile by však bylo mezi dvěma slovy příkazu více mezer, metoda by nám v poli vrátila i prázdné řetězce představující text mezi těmito mezerami (protože mezi nimi není nic, vrátí prázdný řetězec). Budeme-li však chtít být na uživatele hodní a velkoryse mu odpustit, že tu a tam oddělil jednotlivá slova více než jednou mezerou, musíme jako regulární výraz zadat text ".+", anebo ještě lépe text "\\s+" 22. Nazveme-li parametr představující analyzovaný příkaz command a rozhodneme-li se uložit výsledek do lokální proměnné nazvané words, bude mít příkaz podobu: String[] words = command.split("\\s+"); Měli bychom ale myslet na to, že uživatel může příkaz zadávat velkými i malými písmeny. Když už jsme se jednou rozhodli, že budeme vše převádět na malá písmena (viz pasáž Úprava definice konstruktoru akcí na straně 160), měli bychom tak učinit i nyní, aby nám pak velikost písmen nedělala problémy při porovnávání textů. Upravíme proto příkaz do podoby: String[] words = commandline.tolowercase().split("\\s+"); 22 Výklad práce s regulárními výrazy najdete na mých stránkách v samostatném dokumentu na adrese Strana 168 z 351

169 Kapitola 8: Začínáme tvořit vlastní hru 169 Převedení počátečního slova na akci Příkazový řádek máme tedy rozdělen. Zbývá tedy určit akci specifikovanou prvním (pardon nultým) slovem. K tomuto účelu jsme ale v pasáži Příprava definice správce na straně 159 zavedli statický atribut NAME_2_ACTION (viz výpis 8.6 na straně 161) s mapou pro převod názvů akcí na akce. Zadáme-li této mapě název akce, mapa nám vrátí objekt, který je schopen daný příkaz zpracovat (přesněji měl by být). Pokud mapa zná zadaný název a vrátí příslušnou akci, máme vyhráno. Zavoláme její metodu execute(string...) a ta nám vrátí odpověď, kterou předáme volající metodě. Pokud mapa zadaný název akce nezná, vrátí null. V takovém případě musíme oznámit uživateli, že zadal neznámý příkaz. Podíváme se proto do správce scénářů, najdeme mezi kroky chybového scénáře krok testující reakci na neznámý příkaz, zjistíme, jak má vypadat text reakce na neznámý příkaz a příslušný text vrátíme jako funkční hodnotu. 34. Pokuste se samostatně definovat metodu executecommoncomand(string). Svoje řešení můžete porovnat se vzorovým, které najdete ve výpisu 8.10 na řádcích 87 až Prozatímní podoba definice třídy AAction První verzi třída AAction tedy máme definovanou. Podobu zdrojového kódu, k níž jsme dospěli, si můžete prohlédnout ve výpisu Ve výpisu je oddělena část zabývající se definicemi vlastností a schopností správce akcí (tj. objektu třídy řádky 16 až 124) od části zabývající se definicemi vlastností a schopností vytvářených instancí, přesněji rodičovského podobjektu vytvářených instancí (řádky 125 až 201). Připomínám, že objekt třídy slouží jako správce instancí dceřiných tříd, přičemž tyto instance budou představovat jednotlivé akce vaší hry, tj. objekty, které budou umět reagovat na příslušné zadávané příkazy. Výpis 8.10: Prozatímní podoba definice třídy AAction 1 /******************************************************************************* 2 * Třída {@code AAction} je společným rodičem všech tříd, jejichž instance 3 * mají na starosti interpretaci příkazů zadávaných uživatelem hrajícím hru. 4 * Název spouštěné akce bývá většinou první slovo řádku zadávaného 5 * z klávesnice a další slova pak bývají interpretována jako parametry. 6 * <p> 7 * Můžete ale definovat příkaz, který odstartuje konverzaci 8 * (např. s osobou přítomnou v místnosti) a tím přepne systém do režimu, Strana 169 z 351

170 Kapitola 8: Začínáme tvořit vlastní hru * v němž se zadávané texty neinterpretují jako příkazy, 10 * ale předávají se definovanému objektu až do chvíle, 11 * kdy uživatel rozhovor ukončí a objekt rozhovoru přepne hru zpět 12 * do režimu klasických příkazů. 13 */ 14 abstract class AAction extends ANamed implements IAction 15 { 16 //== CONSTANT CLASS ATTRIBUTES ================================================= /** Mapa zprostředkovávající převod názvu akce na její instanci. */ 19 private static final Map<String, AAction> NAME_2_ACTION; //== VARIABLE CLASS ATTRIBUTES ================================================= /** Uchovává informaci o tom, zda se hra právě hraje 26 * nebo jen čeká na spuštění. */ 27 private static boolean isalive = false; //############################################################################## 32 //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== static { 35 NAME_2_ACTION = new HashMap<>(); 36 } //== CLASS GETTERS AND SETTERS ================================================= /*************************************************************************** 43 * Vrátí informaci o tom, je-li hra aktuálně spuštěná. 44 * 45 Je-li hra spuštěná, vrátí {@code true}, 46 * jinak vrátí {@code false} 47 */ 48 public static boolean isalive() 49 { 50 return isalive; 51 } //== OTHER NON-PRIVATE CLASS METHODS =========================================== /*************************************************************************** 58 * Zpracuje zadaný příkaz a vrátí text zprávy pro uživatele. 59 * 60 command Zadávaný příkaz Strana 170 z 351

171 Kapitola 8: Začínáme tvořit vlastní hru Textová odpověď hry na zadaný příkaz 62 */ 63 public static String executecommand(string command) 64 { 65 command = command.trim(); 66 String answer; 67 if (isalive) { 68 answer = executecommoncomand(command); 69 } 70 else { 71 answer = startgame(command); 72 } 73 return answer; 74 } //== PRIVATE AND AUXILIARY CLASS METHODS ======================================= /*************************************************************************** 81 * Zjistí, jaká akce má být zadaným příkazem aktivována, 82 * a jedná-li se o známou akci, provede ji. 83 * 84 command Zadávaný příkaz 85 Odpověď hry na zadaný příkaz 86 */ 87 private static String executecommoncomand(string command) 88 { 89 String[] words = command.tolowercase().split("\\s+"); 90 String acttionname = words[0]; 91 AAction action = NAME_2_ACTION.get(acttionName); 92 String answer; 93 if (action == null) { 94 answer = Texts.zNEZNÁMÝ_PŘÍKAZ; 95 } 96 else { 97 answer = action.execute(words); 98 } 99 return answer; 100 } /*************************************************************************** 104 * Ověří, jestli je hra spouštěna správným (= prázdným) příkazem, 105 * a pokud ano, spustí hru. 106 * 107 command Příkaz spouštějící hru 108 Odpověď hry na zadaný příkaz 109 */ 110 private static String startgame(string command) 111 { 112 String answer; Strana 171 z 351

172 Kapitola 8: Začínáme tvořit vlastní hru if (command.isempty()) { 114 answer = Texts.zCELÉ_UVÍTÁNÍ; 115 } 116 else { 117 answer = Texts.zNENÍ_START; 118 } 119 return answer; 120 } //############################################################################## 125 //== CONSTANT INSTANCE ATTRIBUTES ============================================== /** Stručný popis dané akce. */ 128 private final String description; //== VARIABLE INSTANCE ATTRIBUTES ============================================== //############################################################################## 137 //== CONSTUCTORS AND FACTORY METHODS =========================================== /*************************************************************************** 140 * Vytvoří rodičovský podobjekt vytvářené akce. 141 * 142 name Název vytvářené akce = text, který musí hráč zadat 143 * jako počáteční slovo zadávaného příkazu 144 description Stručný popis vytvářené akce 145 */ 146 AAction(String name, String description) 147 { 148 super(name); 149 this.description = description; 150 AAction previous = NAME_2_ACTION.put(name.toLowerCase(), this); 151 if (previous!= null) { 152 throw new IllegalArgumentException( 153 "\nakce s názvem «" + name + "» byl již vytvořena"); 154 } 155 } //== ABSTRACT METHODS ========================================================== /*************************************************************************** 162 * Metoda realizující reakci hry na zadání daného příkazu. 163 * Počet parametrů je závislý na konkrétní akci, 164 * např. akce typu <i>konec</i> a <i>nápověda</i> nemají parametry, Strana 172 z 351

173 Kapitola 8: Začínáme tvořit vlastní hru * akce typu <i>jdi</i> a <i>seber</i> mají jeden parametr 166 * akce typu <i>použij</i> muže mít dva parametry atd. 167 * 168 arguments Parametry příkazu akce; 169 * jejich počet muže byt pro každou akci jiný, 170 * ale pro všechna spuštění stejné akce je stejný 171 Text zprávy vypsané po provedeni příkazu 172 */ 174 abstract 175 public String execute(string... arguments) 176 ; //== INSTANCE GETTERS AND SETTERS ============================================== /*************************************************************************** 183 * Vrátí popis příkazu s vysvětlením jeho funkce 184 * a významu jednotlivých parametru. 185 * 186 Popis příkazu 187 */ 189 public String getdescription() 190 { 191 return description; 192 } //== OTHER NON-PRIVATE INSTANCE METHODS ======================================== 197 //== PRIVATE AND AUXILIARY INSTANCE METHODS ==================================== //############################################################################## 202 //== NESTED DATA TYPES ========================================================= 203 } 8.14 Test Vše máme připraveno, můžeme spustit test, tj. můžeme spustit třídu správce scénářů. Jak jistě očekáváte, opět obdržíme chybové hlášení počátek zprávy o vyhozené výjimce najdete ve výpisu Je vidět, že test zase o kousek popošel. Strana 173 z 351

174 Kapitola 8: Začínáme tvořit vlastní hru 174 Výpis 8.11: Klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy AAction oznamující obdržení špatné informace o živosti hry 1 Ukončení bylo způsobeno vyhozením výjimky: 2 eu.pedu.adv15p_fw.test_util.common.testexception: 3 Hra tvrdí, že neběží, i když má právě běžet 4 at eu.pedu.adv15p_fw.test_util.common.atester.error_t(atester.java:216) 5 at eu.pedu.adv15p_fw.test_util.common.atester.error(atester.java:201) 6...atd. Jak se z výpisu dozvíme, test zarazilo to, že Hra tvrdí, že neběží, i když má právě běžet Aktualizace informace o živosti hry Na to, jestli hra běží či neběží, se testovací program ptá hry voláním metody isalive() (viz pasáž IGame na straně 137). V pasáži Metoda isalive() ve třídě hry na straně 165 jsme ale zodpovědnost za zprostředkování této informace delegovali na správce akcí, kterým je objekt třídy AAction. V něm jsme však vše definovali jenom provizorně s tím, že to opravíme, až na to testovací program přijde. Právě na to došlo. Při zpracování startovacího příkazu jsme např. nezměnili stav atributu isalive na true. To bychom měli napravit. Do kódu, který spouští hru (v mém programu je to metoda startgame(string) ve třídě AAction), proto přidáme příkaz isalive = true; Tak si správce akcí zapamatuje, že hra byla odstartována, a až se jej bude příště někdo ptát, bude v obraze Opakování testu Chyba byla (snad) opravena, takže znovu spustíme test. Jak už jste si jistě zvykli, výsledkem bude opět chybové hlášení. Počátek jeho zprávy o vyhozené výjimce najdete ve výpisu Tentokrát se test zarazil na tom, že ve třídě hry ještě není plnohodnotně definována metoda getworld(?), která má za úkol vrátit objekt reprezentující svět hry. Tomu se budeme věnovat v příští kapitole. Výpis 8.12: Klíčová část hlášení o průběhu testu po úpravě metody startgame(string) ve třídě AAction oznamující nedokončenou metodu getworld(?) ve třídě RUPAppartmentGame 1 Ukončení bylo způsobeno vyhozením výjimky: 2 java.lang.unsupportedoperationexception: Strana 174 z 351

175 Kapitola 8: Začínáme tvořit vlastní hru Metoda ještě není hotova. 4 at eu.pedu.adventure15p.rupappartmentgame.getworld(rupappartmentgame.java:173) 5 at eu.pedu.adv15p_fw.test_util.tgametests.tgamesteptester.verify( TGameStepTester.java:136) 6... atd Co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Zopakujme si, co jsme v této kapitole dozvěděli a co jsme naprogramovali. Ke stavu vývoje projektu, který jsme shrnuli v podkapitole 5.10 Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali na straně 109, jsme přidali následující: Zkopírovali jsme do svého balíčku třídu EmptyGame a zkopírovanou třídu přejmenovali (v doprovodném projektu na RUPApartmentGame). Ve třídě jsme řešili následující: Upravili jsme typ atributu FACTORY_CLASS odkazující na class-objekt tovární třídy aplikace. Změnili jsme typový parametr generické třídy Class<?> specifikující typ návratové hodnoty metody getfactoryclass(). Definovali jsme tělo metody isalive(). Definovali jsme tělo metody executecommand(string command). Zbylé metody prozatím stále vyhazují UnsupportedOperationException, takže překladač je přeloží, ale metody ještě nesplňují požadovaný kontrakt. Počítáme však s tím, že je časem doplníme. Zkopírovali jsme do svého balíčku třídu EmptyGame a zkopírovanou třídu přejmenovali na AAction. Instance této třídy (přesněji instance jejích potomků) budou představovat jednotlivé akce a objekt třídy pak bude představovat správce těchto akcí. Ve třídě jsme řešili následující: Definovali jsme statickou konstantu NAME_2_ACTION jako mapu, která umožní převádět názvy akcí na objekty, jež budou mít na starosti zpracování příslušných akcí. Definovali jsme statickou proměnnou isalive, v níž si bude objekt třídy pamatovat, zda hra právě běží. Definovali jsme zárodek konstruktoru třídy (statického inicializačního bloku), v němž jsme inicializovali oba deklarované statické atributy. Definovali jsme statickou metodu isalive(), která vrací informaci o tom, zda hra právě běží. Strana 175 z 351

176 Kapitola 8: Začínáme tvořit vlastní hru 176 Definovali jsme statickou metodu executecommand(string), která má na starosti zpracování příkazů uživatele. Definovali jsme soukromou statickou metodu startgame(string), která má na starosti zpracování příkazů uživatele v situaci, kdy hra neběží a soukromou statickou metodu executecommoncomand(string), která má na starosti zpracování příkazů uživatele v situaci, kdy hra běží. Definovali jsme tělo konstruktoru instancí, které pro každou vytvářenou instanci zanese příslušné informace do mapy NAME_2_ACTION. Zkopírovali jsme do projektu třídu ANamed, která bude společným rodičem všech pojmenovaných objektů. V tovární třídě jsme odkomentovali tělo metody getgame() a upravili její tělo. Ve třídě správce scénářů jsme v metodě main(string[]) zakomentovali příkazy pro test správce a simulaci běhu hry podle scénáře a odkomentovali test hry. Program, jehož zdrojové kódy odpovídají výše popsanému stavu vývoje aplikace, najdete v projektu A108z_ActionManager. Jeho diagram tříd si můžete prohlédnout na obrázku 8.1. Obrázek 8.1 Diagram tříd projektu A108z_ActionManager zachycujícího stav poté, co byly definovány kostry tříd reprezentujících hru a správce akcí Strana 176 z 351

177 Kapitola 9: Vytváříme svět hry Vytváříme svět hry Kapitola 9 Vytváříme svět hry Co se v kapitole naučíte V této kapitole budeme pokračovat s vývojem hry. Tentokrát se budeme věnovat světu hry. Stále budeme používat osvědčený postup, při němž vždy spustíme testy a pak se budeme snažit odstranit chybu, na níž nás testy upozorní. To nás postupně dovede k tomu, že vytvoříme celý svět naší hry. V této kapitole se dozvíte další kroky na cestě k vyřešení vašeho úkolu, kterým je naprogramování plnohodnotné hry. Opět vám doporučuji zkopírovat svůj projekt, abyste se měli k čemu vrátit, kdyby se vám něco nepovedlo. Pak pokračujte podle postupu postupně definovaném v číslovaných odstavcích této kapitoly. V této kapitole budeme pokračovat v práci s projektem obsahujícím závěrečnou podobu projektu, k níž jsme se dopracovali v předchozí kapitole, tj. s projektem A108z_ActionManager. 9.1 Analýza chybového hlášení Analýza chybového hlášení ve výpisu 8.12 na straně 174 nám prozradí, že ve třídě RUPAppartmentGame byla na řádku 173 (tj. v metodě getworld(?)) vyhozena výjimka UnsupportedOperationException. Podíváme-li se na odkazované místo, zjistíme, že metoda getworld má stále ještě prozatímní tělo vyhazující výjimku. UnsupportedOperationException. 1. Nahraďte příkaz k vyhození výjimky slovem return. Strana 177 z 351

178 Kapitola 9: Vytváříme svět hry 178 Tím sice do programu zanesete syntaktickou chybu, ale na druhou jednoduše označkujete místo, kam se budete chtít vrátit, až budete mít svět hry připraven, a kde budeme chtít program dotáhnout. Pojďme se tedy podívat na to, jak bychom takový svět hry naprogramovali. 9.2 Svět hry jako správce prostor hry Než se pustíme do vytváření třídy světa hry, měli byste si v pasážích Svět IWorld na straně 130 a IWorld na straně 136 připomenout, jaký má být účel tohoto objektu v našem programu. Tehdy jsme si řekli, že instance této třídy by měla být jedináček, který bude sloužit jako správce prostorů světa hry. Bude nám umět prozradit, jaké prostory ve hře vystupují a ve kterém z nich se právě nachází hráč. Takže už to víme a můžeme jít tvořit- 9.3 Vytvoření třídy světa hry (Apartment) Začátek vytváření třídy světa hry bude obdobný jako při definice tříd správce scénářů, třídy vlastní hry a třídy správce akcí: ve zdrojových kódech rámce: 2. Z balíčku empty_classes a zkopírujte třídu EmptyWorld obsahující předpřipravenou kostru vytvářené třídy světa hry. 3. Zkopírovanou třídu přejmenujte tak, aby odpovídala duchu vaší hry. Světem hry v doprovodných projektech je služební byt. Měl bych proto tuto třídu pojmenovat OfficialApartment. Pro zkrácení ale vypustím informaci o tom, že byt je služební, a třídu nazvu jednoduše Apartment. Zachovejte se obdobně. 4. Z dokumentačního komentáře třídy odstraňte obecné nápovědné texty a doplňte jej texty odpovídajícími vaší hře. Jedináček Kostra třídy již počítá s tím, že instance reprezentující svět hry bude definována jako jedináček, a proto má již definovaný statický atribut SINGLETON uchovávají odkaz na tohoto jedináčka, tovární metodu getinstance() vracející tento odkaz a soukromý konstruktor, který je prozatím prázdný. Strana 178 z 351

179 Kapitola 9: Vytváříme svět hry 179 Přístupová práva Třída deklaruje, že implementuje interfejs IWorld a definuje proto kostry jím deklarovaných metod. Jejich těla bychom měli nyní definovat, protože je zřejmé, že je bude testovací program volat (k čemu by jinak instanci světa chtěl, že?). Všimněte si, že ani tato třída není deklarována jako veřejná. Hra má být sice schopna předat volajícímu objektu její instanci, ale požadavek je, aby mu předala instanci interfejsu IWorld, který tato třída implementuje. I zde platí, že vrácená instance nevystupují vůči okolí jako instance své mateřské třídy, ale jako instance interfejsu implementovaného její třídou. Proto nevadí, že třída je pro žadatele nepřístupná. Důležité je, že je pro něj přístupný interfejs, za jehož instanci se obdržený objekt vydává. (Tuto problematiku jsem již rozebíral v podšeděném bloku Přístupová práva a jejich vlastnosti na straně 158.) Definice metody getallareas() Přejděme k definicím jednotlivých metod Jako první je ve zdrojovém kódu uvedena metoda getallareas(), která má vrátit kolekci všech prostorů hry. Těžko ale můžeme vytvářet kolekci prostorů, když ještě nemáme definovánu třídu prostorů. Je proto třeba nejprve definovat tu. 9.4 Třída prostorů hry (Room) Třídu prostorů začneme vytvářet standardním způsobem: zkopírováním její předlohy EmptyArea z balíčku empty_classes: 5. Z balíčku empty_classes a zkopírujte třídu EmptyArea obsahující předpřipravenou kostru vytvářené třídy prostorů hry. 6. Zkopírovanou třídu přejmenujte tak, aby odpovídala duchu vaší hry. V doprovodných projektech jsou prostory jednotlivé místnosti služebního bytu (a lednička, ale to je nestandardní prostor, takže jej při výběru názvu nebudu uvažovat). Pojmenuji proto tuto třídu Room. Zachovejte se obdobně. 7. Z dokumentačního komentáře třídy odstraňte obecné nápovědné texty a doplňte jej texty odpovídajícími vaší hře. Opět si můžete všimnout, že třída není veřejná, protože je určená pouze pro vnitřní potřebu hry. Budou-li její instance komunikovat s okolím, tak pouze jako instance interfejsu IArea. Strana 179 z 351

180 Kapitola 9: Vytváříme svět hry 180 Kostra mateřské třídy prostorů je definována jako potomek třídy ANamed. Tuto třídu jste již v minulé kapitole do svého projektu zkopírovali, takže by vše mělo být hned od počátku v pořádku. Začínáme s úpravami Před započetím jejích úprav bych opět doporučil se nejprve podívat na pasáže Prostor IArea na straně 130 a IArea na straně 136, abyste si připomněli, jaké vlastnosti jsme prostorům naplánovali, když jsme probírali návrh rámce hry. Zde se dozvíte, že každý prostor musí mít své jméno, musí znát své aktuální sousedy a musí vědět, jaké se v něm právě nacházejí h-objekty. Používáte-li profesionální vývojové prostředí (např. NetBeans), tak si při kopírování třídy všimne, že v cílovém balíčku již existuje třída se stejným názvem, jako má rodičovská třída kopírované třídy. Bude se vás proto ptát, zda budete chtít tuto rodičovskou třídu importovat. Nenechte se tím zmást a import odmítněte. Zkopírovaná třída pak bez problémů akceptuje jako svého rodiče třídu, kterou najde v balíčku, kam jste ji zkopírovali. V kostře třídy jsou předpřipravené implementace dvou abstraktních metod definovaných v implementovaném interfejsu. Tyto metody mají za úkol poskytnout na požádání název prostoru (v naší hře místnosti), kolekci jeho aktuálních sousedů a kolekci h-objektů, které se v daném prostoru právě nacházejí. Tyto informace jednoznačně charakterizují daný prostor a jeho aktuální stav. Aby instance mohla tyto informace předat, musí je nejprve znát. Nejlepší místo, kde můžeme každému prostou předat informace o jeho výchozím stavu, jsou parametry jeho konstruktoru. Pojďme tedy nejprve definovat konstruktor. Konstruktor prostoru Konstruktor našich místností by tedy měl mít tři parametry, v nichž bychom mu předali název místnosti, její počáteční sousedy a počáteční h-objekty. Bohužel, to se nám nemůže podařit. Není problém prozradit vytvářené místnosti její název a nemělo by být ani těžké dodat počáteční h-objekty. Problémem ale jsou sousedé dané místnosti. Představte si, že vytváříme první místnost, která má sousedit s druhou a třetí. Potřebovali bychom proto předat konstruktoru první místnosti odkaz na druhou a třetí místnost. Tyto místnosti ale ještě neexistují (vytváříme teprve tu první), takže nemá smysl uvažovat o nějakém odkazu. Strana 180 z 351

181 Kapitola 9: Vytváříme svět hry 181 My ale můžeme tento problém obejít tak, že konstruktoru prostorů nepředáme odkazy na sousední prostory, ale pouze názvy těchto prostorů. Budeme pak počítat s tím, že budeme umět prostor v pravý okamžik požádat, aby tyto názvy převedla na příslušné objekty prostorů. Pokud se budeme chtít obdobně zachovat i k h-objektům, mohla by hlavička konstruktoru být ve tvaru: Room(String name, String[] neighbornames, String... itemnames) Možná některé z vás udiví, proč předávám názvy sousedů jinak než názvy h-objektů. Je to proto, že uvedu-li za názvem typu tří tečky, stanou se všechny následující parametry prvky jednorozměrného pole. Já však při předávce hodnot parametru nemusím toto pole deklarovat, ale mohu pouze vyjmenovat jeho prvky. O definici potřebného pole se postará překladač. Bohužel, takto mohu zadat pouze poslední parametr, takže předchozí parametr musím explicitně deklarovat jako jednorozměrné pole. Možná se někteří další budou ptát, proč zde používám pole, když jsem již několikrát řekl, že pole se postupně přestávají používat a jsou nahrazována kolekcemi. To je sice pravda, ale stále existuje řada situací, kdy je práce s poli jednodušší, aniž by přitom hrozilo nebezpečí zneužití jejich neschopnosti ochránit uložená data a absence některých dalších vlastností. Jednou z takových situací je předání skupiny dat metodě. Z předchozího rozboru proto pro vás vyplývá další bod na cestě k vytvoření mateřské třídy vašich prostorů: 8. Upravte ve vaší třídě hlavičku konstruktoru podle výše popsaného vzoru, tj. definujte konstruktor jako tříparametrický s parametry typu String, String[] a String... Parametry konstruktoru jsme tedy vymysleli, ale na konstruktoru nyní je, aby si je zapamatoval. Pro každou hodnotu, kterou si potřebujeme zapamatovat, musíme definovat atribut, do nějž ji uložíme. O uložení názvu vytvářeného prostoru se postará rodičovský konstruktor. Zbylé dva však musíme vyřešit sami. Oba dva přitom můžeme definovat jako konstanty, jejichž hodnota se nebude měnit. Takže: 9. Deklarujte atributy pro uložení názvu daného prostoru a názvy jeho výchozích sousedů a v něm se nacházejících h-objektů. Jako inspirace vám může sloužit výpis 9.1. Strana 181 z 351

182 Kapitola 9: Vytváříme svět hry 182 Připomeneme-li si zásadu probíranou v podšeděném bloku Kde inicializovat na straně 162, tak další krok bude: 10. Upravte tělo konstruktoru tak, aby inicializoval právě deklarované atributy. Výpis 9.1: Definice konstruktoru instancí třídy Room a jím inicializovaných atributů 1 //== CONSTANT INSTANCE ATTRIBUTES ============================================== 2 3 /** Názvy sousedů prostoru na počátku hry. */ 4 private final String[] neighbornames; 5 6 /** Názvy h-objektů v prostoru na počátku hry. */ 7 private final String[] itemnames; //== CONSTUCTORS AND FACTORY METHODS =========================================== /*************************************************************************** 14 * Vytvoří nový prostor se zadaným názvem a 15 * zadanými názvy jeho počátečních sousedů a h-objektů. 16 * 17 name Název daného prostoru 18 neighbornames Názvy sousedů prostoru na počátku hry 19 itemnames Názvy h-objektů v prostoru na počátku hry 20 */ 21 Room(String name, String[] neighbornames, String... itemnames) 22 { 23 super(name); 24 this.neighbornames = neighbornames; 25 this.itemnames = itemnames; 26 } Metoda getneighbors() Definice metody getneighbors() bude nepatrně složitější. První věc, kterou musíme vyřešit, je získání kolekce, kterou máme vrátit. Jediné, co máme zatím k dispozici, je pole názvů počátečních sousedů. Prostor ale potřebuje kolekci aktuálních sousedů, ne jen jejich názvů. Tu si musí prostor za prvé někde vyrobit a za druhé si ji musí průběžně pamatovat. Jak už jsem mnohokrát řekl: máme-li si něco pamatovat, potřebujeme pro to deklarovat příslušný atribut. Takže: 11. Pro zapamatování si aktuálních sousedů definujte konstantní atribut uchovávající kolekci prostor. V doprovodném projektu má tato deklarace tvar: Collection<Room> neighbors; Strana 182 z 351

183 Kapitola 9: Vytváříme svět hry Doplňte do těla konstruktoru příkaz inicializujícím tuto kolekci prázdným seznamem typu ArrayList. Změna typu návratové hodnoty Původní hlavička metody getneighbors() deklarovaná v implementovaném interfejsu má tvar: public Collection<? extends IArea> getneighbors() Požaduje tedy vrácení kolekce objektů implementujících interfejs IArea. Pokud by však vaše prostory měli nějakou dodatečnou schopnost, pro kterou interfejs IArea nemá deklarovanou odpovídající metodu (např. by váš prostor věděl, jestli je právě zamořen jedovatým plynem) nemohli byste se jej na to přímo zeptat, ale museli byste objekt z kolekce nejprve přetypovat na typ vašich prostorů, a teprve pak se jej na danou informaci zeptat. V praxi proto bývá zvykem upravit v potomcích tyto deklarace tak, aby přímo označovaly typ vracené hodnoty (samozřejmě pokud jej známe). Pokud bych tedy ve své třídě upravil hlavičku této metody do tvaru public Collection<Room> getneighbors() požadavky implementovaného interfejsu bych neporušil. Metoda by stále vracela instanci typu IArea či některého z jeho potomků, protože moje místnosti tento interfejs implementují, takže jejich kolekce požadavkům interfejsu vyhovuje. Já bych ale věděl, že vrací právě moje prostory s jejich rozšířenými schopnostmi a jako s takovými bych s nimi mohl pracovat. Ve výše uvedeném případě jsme z typového parametru vracených kolekcí odstranili žolík. To ale obecně není jediná možná úprava. Obecně vyhovuje jakákoliv úprava typu návratové hodnoty metody potomka, která nekoliduje s deklarací této metody v typu předka. Bude-li proto předek deklarovat jako typ návratové hodnoty např. Object, může potomek tuto deklaraci upřesnit a deklarovat např. String. Touto cestou se vydáme i myl 13. Doplňte konstantní atribut reprezentující kolekci sousedů daného prostoru zabalenou využitím metody unmodifiablecollection(collection) (podrobnosti o tomto tématu najdete v podšeděném bloku Zabezpečení nezměnitelnosti kontejneru). (V doprovodném projektu se tento atribut jmenuje exportedneighbors.) 14. Doplňte tělo konstruktoru příkazem pro inicializaci tohoto atributu. 15. Upravte definici těla metody getneighbors() tak, že bude vracet tento před chvílí definovaný atribut. 16. Upravte hlavičku metody getneighbors() z původního Strana 183 z 351

184 Kapitola 9: Vytváříme svět hry 184 public Collection<? extends IArea> getneighbors() na tvar nepoužívající v typovém parametru žolík. V doprovodném projektu má hlavička této metody tvar: public Collection<Room> getneighbors() O účelu a možnostech takovýchto úprav se dočtete v podšeděném bloku Změna typu návratové hodnoty. Metoda getitems() Obdobně bychom to mohli udělat i s metodou getitems(). Problémem ale je, že tato metoda má vracet kolekci h-objektů v daném prostoru, a my jsme ještě mateřskou třídu těchto h-objektů nedefinovali. Musíme to nejprve napravit. Strana 184 z 351

185 Kapitola 9: Vytváříme svět hry 185 Zabezpečení nezměnitelnosti kontejneru Jedním z důvodů, proč se upouští od používání polí, je skutečnost, že u pole není možno zabezpečit, aby objekt, kterému nějaké pole předáme, obsah tohoto pole nějak neovlivnil. U kolekcí a map to ale zabezpečit lze. V těle této třídy jsou dvě metody vracející kolekce: metoda getneighbors() vracející kolekci sousedů a metoda getitems() vracející kolekci h-objektů. Tyto metody jsou veřejně dostupné, takže o danou kolekci může požádat kdokoliv. Proto bychom měli metody naprogramovat tak, abychom zabezpečili, že se žadatel nebude moci v obdržené kolekci (a obecně v kontejneru) hrabat a nějak nám ji (jej) změnit, čímž by mohl nepříjemně ovlivnit další chování hry 23. Existují dva způsoby, jak se s tímto problémem vypořádat: Místo originálního kontejneru vrátíme jeho kopii. Vrátíme kontejner zabalený do objektu, který jeho obsah změnit nedovolí. Obecně není možno říci, která metoda je lepší. Potřebujeme-li vrátit kontejner definující aktuální stav nezávisle na tom, jak se v budoucnu změní, musíme vrátit kopii. Pokud na nezávislosti obsahu kontejneru na případných budoucích změnách nezáleží (anebo jejich respektování dokonce vyžadujeme), bývá výhodnější zvolit druhou možnost, která je obecně efektivnější. Standardní knihovna k tomuto účelu nabízí sadu metod definovaných ve třídě java.util.collections Metody se jmenují unmodifiablexyz, kde za Xyz dosazujeme požadovaný druh kontejneru: Collection, Set, List, Map atd. Metodě předáte příslušný kontejner, ona vám vrátí objekt, v němž je tento kontejner zabalený, a vy pak tento objekt předáte žadateli místo požadovaného. My už jsme ji jednou použili, i když jsem vám o tom tehdy neřekl. Pokud si vzpomenete na výpis 3.1 na straně 56, tak tam je na řádcích 115 a 117 podobný problém řešen. Je tu ale přece jenom jeden drobný rozdíl. Ve zmíněném programu jsou zabaleny kolekce, jejichž obsah se již dále nemění. Zde bychom balili kolekce, jejichž obsah se měnit bude. Máme na výběr ze dvou možností: Při každé žádosti kolekci znovu zabalit. Uložit si zabalenou kolekci a při další žádosti tuto kolekci vrátit. Vzhledem k tomu, že obalový objekt pouze filtruje přístupy k původní kolekci, ale vlastní obsahu kolekce nijak neovlivňuje, tak se v něm všechny změny obalované kolekce ihned projeví. 23 Abych pravdu řekl, testovací program dokonce prověřuje, jestli autor hry nebyl tak lehkomyslný, že žadateli předal přímo svoji kolekci, na jejímž správném obsahu je chod hry životně závislý. Strana 185 z 351

186 Kapitola 9: Vytváříme svět hry Třída h-objektů v prostorech (Item) Vytvoření třídu h-objektů v prostorech vytvořte standardním způsobem: 17. Z balíčku empty_classes v rámci zkopírujte třídu EmptyItem. 18. Zkopírovanou třídu vhodně přejmenujte. (V doprovodných programech se tato třída jmenuje Item.) 19. Z dokumentačního komentáře třídy odstraňte obecné nápovědné texty a doplňte jej texty odpovídajícími vaší hře. Každý rok se objeví nějaký student, který podlehne svodům jednoduchosti úprav a při přejmenovávání zkopírované třídy EmptyItem pouze odmaže slovo Empty. Tím vytvoří třídu se stejným názvem, jako má společný prapředek všech objektových datových typů, čímž překladač dokonale zmate (přiznejme si, že spíš zmate sám sebe) a nestačí se pak divit, proč to či ono nejde přeložit. Nepřidávejte se k této skupině lenochů a přejmenujte svoji třídu nějak inteligentněji. Před započetím úprav třídy h-objektů bych opět doporučil se nejprve podívat na pasáže H-objekt IItem na straně 131 a IItem na straně 134 a připomenout si v nich, jaké vlastnosti a schopnosti h-objektů jsme naplánovali. Zde se dozvíte, že h-objekt musí znát svůj název a svoji váhu. Kromě toho bych radil si přečíst také podšeděný blok Kdo si má co pamatovat, v němž si připomenete (protož doufám, že jste se s nimi již v předchozím výkladu setkali) některé ze zásad moderního programování. Strana 186 z 351

187 Kapitola 9: Vytváříme svět hry 187 Kdo si má co pamatovat Tady bych znovu připomněl obsah podkapitoly 7.8 K čemu je dobrý rámec hry na straně 145. Řadu programátorů opravdu napadne, že je zbytečné, aby si všechny h-objekty (včetně např. televize) pamatovaly, zda nejedná o alkoholický nápoj, když by přece stačilo, aby si těch pár názvů pamatovala lednička. Pravda, pro to, aby lednička poznala, jestli má s hráčem zapříst rozhovor o jeho věku, by to opravdu stačilo. Porušili by tím ale programátorskou zásadu, že by každý h-objekt měl být plně zodpovědný za sebe a svůj stav. Definujeme-li program tak, že budou informace o stavu h-objektu (např. jestli je daný h-objekt alkoholickým nápojem) rozcourány po programu, budete mít velké problém v okamžiku, kdy se dozvíte, že musíte některé vlastnosti programu změnit. Veškeré informace o stavu h-objektu by měly být součástí daného h-objektu a neměly by být separátně ukládány nikde jinde. Připomínám tedy, že byste měli všechny datové typy navrhovat tak, aby každý h-objekt měl vždy všechny informace o svém stavu, a aby nikdo neměl šanci měnit stav h-objektu za jeho zády, tj. jinak než zavoláním některé z metod h-objektu (přesnější by bylo: posláním zprávy h-objektu). Tyto informace si ale nemusíte pamatovat jen v atributech. V ukázkové hře bychom např. mohli definovat pro alkoholické nápoje dceřinou třídu třídy obecných h-objektů a to, jestli je daný h-objekt alkoholickým nápojem by se mohlo poznat podle toho, je-li instancí dané třídy. Na druhou stranu je třeba si uvědomit, že uchování informace o vlastnosti prostřednictvím příslušnosti k třídě je neměnné. Třídu h-objektu není možno za chodu programu změnit. Pokud by se ve hře měl využít fakt, že z nápoje po čase či po zahřátí alkohol vyprchá, museli bychom zůstat u uložení hodnoty v atributu. Mají-li h-objekty několik různých vlastností ovlivňujících jejich chování Zkušenější studenti se občas snaží navrhnout komplikovanější hry, jejichž návrh by pro ně byl jistou výzvou. V těchto hrách se občas vyskytne řada h-objektů s různými vlastnostmi. V takovém případě nebývá rozumné nastavit všem h-objektům všechny odpovídající atributy, protože by jich bylo moc a u většiny h-objektů by většina z nich byla nevyužitá. Jako výhodnější řešení se v takových případech ukazuje definovat jako atribut objektu mapu, v níž budou klíče představovat názvy jednotlivých vlastností a hodnoty budou hodnoty. Zjišťovací přístupové metody (getry) pak budou mít parametr typu String, v němž volající program zadá název vlastnosti, jejíž hodnotu zjišťuje. Obdobně přibyde parametr i v případných nastavovacích metodách tam budeme určovat, čí hodnotu nastavujeme. Strana 187 z 351

188 Kapitola 9: Vytváříme svět hry 188 Název a váha/přenositelnost jako parametry konstruktoru Má-li h-objekt znát svůj název a váhu, měli bychom obě tyto informace předat jeho konstruktoru v parametru. V některých z vašich her bude třeba předat i další údaje např. v ukázkovém doprovodném programu musí o sobě každý h-objekt vědět, jestli se nejedná o alkoholický nápoj. H-objekty v demonstrační hře mají tři charakteristiky: název, váhu a alkoholičnost. Název si za nás pamatuje rodičovský podobjekt (třída h-objektů je potomkem třídy ANamed), takže musíme definovat atributy pro zbylé charakteristiky. O sadě potřebných charakteristik pro vaši hru se musíte rozhodnout i vy: 20. Definujte atributy pro charakteristiky potřebné ve vaší hře. Atributy, jejichž hodnota se v průběhu hry nebude měnit, definujte jako konstanty. Každý takovýto údaj musíme konstruktoru předat. Vyhradit pro každý údaj jeho vlastní parametr nemusí být vždy to nejvýhodnější řešení. Někdy se může stát, že separátní předání takovýchto údajů program spíše znepřehlední, a že v daném případě bude v zájmu zpřehlednění programu výhodnější, zvolit nějakou alternativní metodu. Proto jsem se rozhodl vám jeden takovýto alternativní způsob předání údajů předvést. Alternativní možnosti předání údajů Ve většině her nebývá váha h-objektu primární údaj. Podle zadání stačí, když se všechny h-objekty rozdělí na přenositelné a nepřenositelné. Váhu jsme do rámce přidali jenom proto, abychom poněkud rozšířili možnosti těm, kteří chtějí vymyslet něco rafinovanějšího. Přiznejme si, že většina her vystačí s binární informací přenositelný nepřenositelný. Pro takovouto jednoduchou informaci nepotřebujeme zvláštní parametr. Můžeme ji předávat i tak, že před vlastní název h-objektu vložíme speciální předponu (prefix) tvořenou znakem, který bude charakterizovat některé charakteristiky vytvářeného h-objektu, a teprve další znaky budou definovat jeho skutečný název. Tím se trochu zjednoduší volání konstruktorů h-objektů všem bude stačit jenom jeden parametr, kterým bude název h-objektu doplněný prefixem jednoznačně charakterizujícím jeho přenositelnost. Pokud byste dělili h-objekty na více kategorií, mohl by tento prefix mít i další významy. V ukázkové hře definované v doprovodném programu, je inteligentní lednička, která z uložených h-objektů vyčleňuje alkoholické nápoje. Protože se o alkoholických nápojích ví, že jsou přenositelné, tak by znak označující, že daný h-objekt je alkoholický nápoj, označoval současně i to, že je přenositelný. Tento prefix by tak mohl v případě potřeby charakterizovat i váhu h-objektu. Např. v našem doprovodném programu bychom mohli definovat, že pro zvednu- Strana 188 z 351

189 Kapitola 9: Vytváříme svět hry 189 tí ledničky a televizoru potřebuje hráč obě ruce, kdežto pro zvednutí ostatních přenositelných h-objektů mu stačí ruka jedna. Počet potřebných rukou by tak představoval charakteristiku odpovídající váze h-objektu. Po těchto úvahách bychom mohli definovat následující podoby znaku, který bychom vkládali jako prefix (předponu) vlastního názvu h-objektu: '#' Jedná se o nepřenositelný h-objekt. '@' Jedná se o alkoholický nápoj (ten je automaticky přenositelný s váhou 1). '2' Jedná se o h-objekt, který je třeba vzít do obou rukou, a má tedy váhu 2. '1' Jedná se o běžný h-objekt, který není alkoholickým nápojem ani h-objektem, k jehož zvednutí jsou potřeba obě ruce, tj. jedná se o h-objekt s váhou 1. Teoreticky bychom poslední podobu znaku mohli vypustit a chování definovat slovy: je-li počátečním znakem cokoliv jiného. To, že i pro standardní h-objekt byl definován konkrétní prefix, má tři důvody: Předejde se tak chybám z překlepů, při nichž někdo zapomene před název h-objektu potřebný prefix přidat. Nebudeme muset hlídat, aby název nějakého h-objektu náhodou nezačínal některým z prefixů. Budeme mít otevřený prostor pro případné přidání dalších druhů h-objektů, až budeme později chtít svoji hru nějak vylepšit, protože nebudeme muset hledat pro prefix znak, kterým žádný název nezačíná. Proč kódovat dodatečné údaje do prefixu Některé z vás nyní možná napadne, jestli zaváděním prefixů názvů program zbytečně nekomplikujeme. Jestli by nakonec nebylo jednodušší vytvořit buď konstruktor se všemi potřebnými parametry, anebo sadu konstruktorů, které budou vždy definovat jakousi podmnožinu parametrů, přičemž ve svém těle zavolají svého kolegu, kterému doplní zbývající parametry. V případě ukázkové hry bychom např. mohli definovat: Jednoparametrický přebírající pouze název pro standardní přenositelné h-objekty. Dvouparametrický konstruktor, jehož druhý parametru bude celé číslo představující váhu, přičemž nepřenositelné h-objekty by mohly mít např. váhu 0 (to abychom nemuseli psát celý název konstanty s jejich váhou). Dvouparametrický konstruktor s druhým parametrem typu boolean, který bude informace o alkoholičnosti. Strana 189 z 351

190 Kapitola 9: Vytváříme svět hry 190 Tříparametrický konstruktor, pro nějž by jeho výše zmínění kolegové připravili zbylé parametry a zodpovědnost za vytvoření instance na něj jednoduše delegovali příkazem this(?). Při definici třídy h-objektů vypadají obě popsaná řešení s přímo zadávanými dodatečnými hodnotami jednodušeji, jenomže je třeba si uvědomit, jak se tyto h-objekty budou v aplikaci konstruovat. Při současném přístupu s prefixem stačí konstruktoru prostoru předat pole prefixem ozdobených názvů h-objektů s tím, že si tyto h-objekty daný prostor zkonstruuje ve vhodnou chvíli sám. V opačném případě bychom museli při konstrukci každého prostoru předat zkonstruovat všechny jeho h-objekty a předat jeho konstruktoru zkonstruované h-objekty. Domnívám se, že byste při použití verze nepoužívající kódovaný prefix brzy zjistili, že vás to neustále opakované konstruování každého h-objektu v každém prostoru obtěžuje, a že je pro vás předávání údajů prostřednictvím prefixu jednodušší 24. Zveřejnění prefixů Při definici znaků s významy prefixů bychom si měli uvědomit, že se budou používány na více místech: za prvé v těle konstruktoru vytvářejícího daný h-objekt a za druhé v místě, kde se bude daný h-objekt definovat. Není vhodné, aby si programátor musel pamatovat, jaký znak je pro ten který příznak použit a už vůbec není vhodné, aby se tyto znaky objevovaly někde roztroušené po programu. Ze základního kurzu byste si měli pamatovat klíčovou poučku, že magické hodnoty jsou v programu zakázané. To, která znak reprezentuje kterou vlastnost h-objektu, jsou právě ony magické hodnoty. Kdybychom je náhodou museli někdy později měnit (např. proto, že by se některý z nich měl stát součástí názvu nově přidaného h-objektu), museli bychom najít všechny jejich výskyty v programu a všude je správně změnit. Jak jistě tušíte, správně řešení je definovat tyto znaky jako nesoukromé statické konstanty, které budou ostatní programy importovat. Nemusíme je deklarovat jako veřejné, protože nám stačí, když budou dostupné pouze ostatním třídám hry, a ty jsou ve stejném balíčku. Předchozí úvahy vedou k dalšímu úkolu na cestě k vybudování vaší vlastní hry: 24 Druhou možností samozřejmě je, že byste se ve své hře omezili na nezbytně nutný počet h-objektů v krajním případě by vám měly stačit čtyři. Strana 190 z 351

191 Kapitola 9: Vytváříme svět hry Navrhněte sadu prefixů, které budou charakterizovat vlastnosti h-objektů ve vaší hře. 22. Navrhněte sadu soukromých statických konstant typu char, které budou reprezentovat jednotlivé hodnoty prefixu. Vlastní definice Když si u h-objektů pamatujeme jejich váhu, tak bychom si měli ještě ujasnit, budeme charakterizovat jejich nepřenositelnost. V našem případě je to jednoduché: přenositelné h-objekty budou mít váhu 1, resp. 2, nepřenositelné musí mít větší váhu, než bude kapacita batohu, což je 2 (hráč má dvě ruce). Nejlepší by bylo definovat váhu všech nepřenositelných h-objektů jako číslo, které spočteme tak, že zvětšíme maximální kapacitu batohu. Kapacita batohu je ale věcí batohu, a ten jsme ještě nedefinovali. Máme tedy dvě možnosti: Vytvořit třídu batohu a definovat v ní kapacitu její budoucí instance. Zvolit pro kapacitu nepřenositelných h-objektů nějakou dostatečně velkou hodnotu, kterou kapacita batohu s nejvyšší pravděpodobností nepřekročí např. polovinu největšího celého čísla (hodnotu Integer.MAX_VALUE / 2). Pokud vás zajímá, proč jenom polovinu a proč ne maximální hodnotou celou, tak bych vám připomněl h-objekt Základy informatiky a v němž jste probírali vnitřní reprezentaci čísel. Z ní vyplývá, že když k největšímu celému číslu připočteme jedničku, dostaneme nejmenší celé číslo. Když bychom pak k dosavadní váze obsahu batohu připočetli toto číslo, tak bychom dostali záporný výsledek. Abychom ve svém programu nemuseli na tuto alternativu myslet, je vhodné zvolit hodnotu, která nám zmíněné potenciální problémy nepřinese. Předběžné vytvoření třídy batohu Předchozí úvahy vedou k doporučení dát přednost hodnotě přímo odvozené z kapacity batohu. Proto: 23. Zkopírujeme z balíčku empty_classes šablonovou třídu EmptyBag. 24. Zkopírovanou třídu vhodně přejmenujte. Ve hře v doprovodných programech jsou batohem hráčovy ruce. V těchto programech se proto třída batohu jmenuje Hands. Strana 191 z 351

192 Kapitola 9: Vytváříme svět hry V této třídě definujte statickou celočíselnou konstantu s přístupem v rámci balíčku (package private). Konstantu pojmenujte CAPACITY. 26. Konstantě přiřaďte počáteční hodnotu odpovídající kapacitě vyžadované vašimi scénáři. Jak jsem již řekl, v naší ukázkové hře jsou batohem ruce, takže kapacita tohoto batohu je logicky 2. Dokončení konstruktoru Takže už známe kapacitu batohu, takže můžeme jednoduše specifikovat, kolik budou vážit nepřenositelné h-objekty. Proto: 27. Ve třídě vašich h-objektů definujte statickou konstantu (v ukázkové aplikaci jsem ji nazval HEAVY), kterou inicializujete hodnotou o jedničku větší, než je kapacita batohu. Nyní už známe vše pro to, abychom mohli dokončit definici těla konstruktoru, který na základě hodnoty prefixu přiřadí h-objektům odpovídající hodnoty jejich atributů. 28. Dokončete definici těla konstruktoru. Přístupové metody Vedle názvu h-objektu, jehož zpřístupnění řeší rodičovská třída ANamed je ve vaší třídě h-objektů definován nejméně jeden další atribut. Všichni máte definovanou povinnou váhu h-objektu, ale vzhledem k navržené hře mohou mít vaše h-objekty i další atributy obdobné atributu alkoholičnosti v ukázkové hře. 29. Pro každý atribut vašich h-objektů definujte odpovídající zjišťovací metodu (getr). 30. Máte-li vaši hru navrženou tak, že se atributy vašich h-objektů mohou v průběhu hry měnit, nastavte pro odpovídající atributy i příslušnou nastavovací metodu (setr). Zdrojový kód ukázkové hry v doprovodném projektu Po všech těchto úvahách by měla být definice třídy h-objektů jasná. Ti, kteří mají ještě nějaké nejasnosti, si ve výpisu 9.2 mohou pro inspiraci prohlédnout definici třídy Item v doprovodném projektu k této kapitole. Strana 192 z 351

193 Kapitola 9: Vytváříme svět hry 193 Výpis 9.2: Definice třídy Item, jejíž instance představují h-objekty 1 /******************************************************************************* 2 * Instance třídy {@code Item} přestavují h-objekty v prostorech. 3 * H-objekt <i>lednička</i> je v této hře současně prostorem. 4 */ 5 class Item extends ANamed implements IItem 6 { 7 //== CONSTANT CLASS ATTRIBUTES ================================================= 8 9 /** Váha nepřenositelných h-objektů. */ 10 private static final int HEAVY = Hands.CAPACITY + 1; /** Příznak standardního přenositelného h-objektu. */ 13 static final char STANDARD = '1'; /** Příznak nutnosti použít ke zvednutí h-objektu obě ruce. */ 16 static final char DOUBLE_HAND = '2'; /** Příznak nepřenositelnosti h-objektu. */ 19 static final char NOT_MOVABLE = '#'; /** Příznak alkoholického nápoje. */ 22 static final char ALCOHOL = '@'; //== VARIABLE CLASS ATTRIBUTES ================================================= //############################################################################## 31 //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== 32 //== CLASS GETTERS AND SETTERS ================================================= 33 //== OTHER NON-PRIVATE CLASS METHODS =========================================== 34 //== PRIVATE AND AUXILIARY CLASS METHODS ======================================= //############################################################################## 39 //== CONSTANT INSTANCE ATTRIBUTES ============================================== /** Váha h-objektu. */ 42 private final int weight; /** Zda se jedná o alkoholický nápoj. */ 45 private final boolean isalcoholic; //== VARIABLE INSTANCE ATTRIBUTES ============================================== 50 Strana 193 z 351

194 Kapitola 9: Vytváříme svět hry //############################################################################## 54 //== CONSTUCTORS AND FACTORY METHODS =========================================== /*************************************************************************** 57 * Vytvoří h-objekt se zadaným názvem a dalšími zadanými vlastnostmi. 58 * Tyto dodatečné vlastnosti se zadávají prostřednictvím prefixu, 59 * kterým je první znak zadaného názvu. 60 * Vlastní název h-objektu tvoří až zbylé znaky.<br> 61 * znaky prefixu mají následující význam:<br> 62 * 63 name Název vytvářeného h-objektu 64 */ 65 Item(String name) 66 { 67 super(name.substring(1)); boolean alcoholic = false; 70 int estimatedwight = 1; 71 char prefix = name.charat(0); 72 switch (prefix) 73 { 74 case STANDARD: 75 break; case DOUBLE_HAND: 78 estimatedwight = 2; 79 break; case NOT_MOVABLE: 82 estimatedwight = HEAVY; 83 break; case ALCOHOL: 86 alcoholic = true; 87 break; default: 90 throw new RuntimeException( 91 "\nneznámá hodnota prefixu: «" + prefix + '»'); 92 } 93 weight = estimatedwight; 94 isalcoholic = alcoholic; 95 } //== ABSTRACT METHODS ========================================================== 100 //== INSTANCE GETTERS AND SETTERS ============================================== /*************************************************************************** Strana 194 z 351

195 Kapitola 9: Vytváříme svět hry * Vrátí váhu h-objektu, resp. charakteristiku jí odpovídající. 104 * H-objekty, které není možno zvednout, 105 * mají váhu větší, než je kapacita batohu. 106 * 107 Váha h-objektu 108 */ 110 public int getweight() 111 { 112 return weight; 113 } /*************************************************************************** 117 * Vrátí informaci o tom, jedná-li se o alkoholický nápoj. 118 * 119 Jedná-li se o alkoholický nápoj, vrátí {@code true}, 120 * jinak vrátí {@code false} 121 */ 122 public boolean isalcoholic() 123 { 124 return isalcoholic; 125 } //== OTHER NON-PRIVATE INSTANCE METHODS ======================================== 130 //== PRIVATE AND AUXILIARY INSTANCE METHODS ==================================== //############################################################################## 135 //== NESTED DATA TYPES ========================================================= 136 } 9.6 Dokončení definice třídy prostorů (Room) Třídu h-objektů v místnostech máme definovanou, můžeme se tedy vrátit do třídy prostorů a doplnit Room a definovat pro místnost kolekci items, která bude obsahovat h-objekty, jež se v ní nacházejí. Vzápětí definujeme i tělo metody getitems(), která bude tuto kolekci vracet. Přesněji: bude vracet tuto kolekci zabalenou do h-objektu, který nedovolí změnit její obsah. Jak to udělat jsme si to ukazovali v pasáži Metoda getneighbors() na straně 182. Jak si jistě domyslíte, tělo metody getitems() bude obdobou těla metody getneighbors(). Strana 195 z 351

196 Kapitola 9: Vytváříme svět hry 196 Jak už jsem mnohokrát řekl: máme-li si něco pamatovat, potřebujeme pro to deklarovat příslušný atribut. Takže: 31. Pro zapamatování si h-objektů aktuálně přítomných v daném prostoru definujte konstantní atribut uchovávající kolekci h-objektů. V doprovodném projektu má tato deklarace tvar: Collection<Item> items; 32. Doplňte do těla konstruktoru příkaz inicializující tuto kolekci prázdným seznamem typu ArrayList. 33. Doplňte atribut reprezentující kolekci h-objektů v daném prostoru zabalenou využitím metody unmodifiablecollection(collection) (podrobnosti o tomto tématu najdete v podšeděném bloku Zabezpečení nezměnitelnosti kontejneru na straně 185). V ukázkové aplikace v doprovodném programu se tento atribut jmenuje exporteditems. 34. Doplňte tělo konstruktoru příkazem pro inicializaci tohoto atributu. 35. Upravte definici těla metody getitems() tak, že bude vracet tento před chvílí definovaný atribut. 36. Upravte hlavičku metody getitems() z původního public Collection<? extends IItem> getitems() na tvar nepoužívající v typovém parametru žolík. V doprovodném projektu má hlavička této metody tvar: public Collection< Item > getitems () Domnívám se, že všechny návody jak to či ono řešit, byly již dostatečně podrobně probrány, takže neuvádí inspirační výpis ekvivalentu dané třídy z ukázkové třídy v doprovodném projektu. Zájemci si jej zdrojový kód třídy Room prohlédnout přímo v daném projektu. 9.7 Dokončení definice třídy světa hry (Apartment) Dokončili jsme definici třídy prostorů (v ukázkové hře Room) a jejích povinných metod, takže se můžeme konečně vrátit k definici třídy světa hry (v ukázkové hře třída Apartment), jejíž instance funguje jako správce všech místností v bytě. Strana 196 z 351

197 Kapitola 9: Vytváříme svět hry 197 Metoda getallareas() Ve třídě světa hry zůstalo nedokončené tělo metody getallareas(). Dostáváme se do situace, kterou jsme již dvakrát řešili ve třídě prostorů hry. Její řešení máme již nacvičené: 37. Definujte kolekci h-objektů, které si má vytvářený správce pamatovat zde to bude kolekce prostorů. Protože se kolekce nebude v průběhu hry měnit, deklarujte ji jako konstantu. V doprovodném projektu je deklarována příkazem: private final Collection<Room> rooms; 38. Vložte do těla konstruktoru příkaz inicializujícím tuto kolekci prázdným seznamem typu ArrayList. 39. Doplňte atribut reprezentující kolekci sousedů daného prostoru zabalenou využitím metody unmodifiablecollection(collection) (podrobnosti o tomto tématu najdete v podšeděném bloku Zabezpečení nezměnitelnosti kontejneru). (V doprovodném programu se tento atribut jmenuje exportedrooms.) 40. Upravte definici konstruktoru, aby tuto kolekci vracel. 41. Upravte definici těla metody getallareas() tak, že bude vracet tento před chvílí definovaný atribut. 42. Změňte deklaraci hlavičky metody getallareas() z původního public Collection<? extends IArea> getallareas() na tvar nepoužívající v typovém parametru žolík. V doprovodném projektu má hlavička této metody tvar: public Collection<Room> getallareas() Doplnění konstruktoru Při té příležitosti bychom si mohli uvědomit, že kolekce je zatím prázdná a že by konstruktor svět hry mohl být tím správným místem pro vytvoření jednotlivých místností. Bylo by tedy vhodné doplnit tedy příkazy, které vytvoří jednotlivé prostory a vloží každý z nich právě vytvořené kolekce. Parametry konstruktorů vytvářených místností přitom převezmeme z definic testovacích kroků ve správci scénářů. První výskyt daného prostoru ve scénáři vždy obsahuje názvy výchozích sousedů a h-objektů v daném prostoru. Při definici prostorů nesmíme zapomenout ani na nestandardní prostory (truhly, batohy, desky stolu, ), o nichž jsem hovořil v podkapitole 1.1 Koncepce vyvíjené hry na straně 31 a mezi něž v ukázkové hře patří lednice. Strana 197 z 351

198 Kapitola 9: Vytváříme svět hry 198 Z předchozího vyplývá dalším krok na vaší cestě k vytvoření vlastní hry. Tímto krokem je: 43. Doplňte tělo konstruktoru instancí správce hry o definice všech prostorů vaší hry a jejich začlenění do příslušné kolekce. Upravenou definici konstruktoru pro ukázkovou hru spojenou s procházením služebního bytu si můžete prohlédnout ve výpisu 9.3. Všimněte si, že názvy h-objektů předávané konstruktoru místností sčítám s prefixem (předponou) definujícím příznak specifikující typ h-objektu (hovořil jsem o něm v pasáži Alternativní možnosti předání údajů na straně 188). Výpis 9.3: Upravená definice konstruktoru instancí třídy Apartment 1 /*************************************************************************** 2 * Soukromý konstruktor definující jedinou instanci správce prostorů. 3 * V rámci definice správce vytvoří i všechny prostory hry. 4 */ 5 private Apartment() 6 { 7 rooms = new ArrayList<>(); 8 exportedrooms = Collections.unmodifiableCollection(rooms); 9 10 rooms.add(new Room(PŘEDSÍŇ, 11 new String[] {LOŽNICE, OBÝVÁK, KOUPELNA}, 12 STANDARD+BOTNÍK, STANDARD+DEŠTNÍK)); 13 rooms.add(new Room(LOŽNICE, 14 new String[] {PŘEDSÍŇ}, 15 NOT_MOVABLE+POSTEL, NOT_MOVABLE+ZRCADLO, 16 STANDARD+ŽUPAN)); 17 rooms.add(new Room(OBÝVÁK, 18 new String[] {PŘEDSÍŇ, KUCHYŇ}, 19 DOUBLE_HAND+TELEVIZE)); 20 rooms.add(new Room(KOUPELNA, 21 new String[] {PŘEDSÍŇ}, 22 STANDARD+BRÝLE, STANDARD+ČASOPIS, 23 NOT_MOVABLE+UMYVADLO)); 24 rooms.add(new Room(KUCHYŇ, 25 new String[] {OBÝVÁK, LOŽNICE}, 26 DOUBLE_HAND+LEDNIČKA, STANDARD+PAPÍR)); 27 rooms.add(new Room(LEDNIČKA, 28 new String[] {}, 29 ALCOHOL+PIVO, ALCOHOL+PIVO, ALCOHOL+PIVO, 30 STANDARD+SALÁM, STANDARD+HOUSKA, 31 ALCOHOL+VÍNO, ALCOHOL+RUM)); Strana 198 z 351

199 Kapitola 9: Vytváříme svět hry 199 Metoda getcurrentarea() Přesuneme se na další metodu getcurrentarea(). Ta má vrátit prostor, v němž se hráč právě nachází. Ten bychom si opět měli pamatovat. 44. Do sekce instančních proměnných definujte atribut private Room currentarea; 45. Do těla metody getcurrentarea() vložte příkaz, který bude hodnotu tohoto atributu vracet. 46. Změňte typ její návratové hodnoty na typ prostorů vaší hry. V posledním bodu se jedná opět pouze o upřesnění datového typu specifikovaného rodičem. Třída vašich prostorů implementuje interfejs IArea, takže se její instance mohou vydávat za instance tohoto interfejsu a překladač nebude vůči vašemu upřesnění nic namítat. Výsledná podoba metody ve třídě Apartment ukázkové hry v doprovodných programech je výpisu 9.4. Výpis 9.4: Upravená definice metody getcurrentarea() ve třídě Apartment 2 Room getcurrentarea() 3 { 4 return currentarea; 5 } 9.8 Úprava metody getworld() ve třídě hry Náš zásobník odskoků a návratů se již téměř vyčerpal. Pokud někoho zarazil termín zásobník, tak bych jen připomněl, že v základním kurzu jsme si říkali, že zásobník je jedna z datových struktur a jedním z jeho příkladů je zásobník návratových adres. My jsme se nyní chovali jako on: Z úprav třídy hry jsme odskočili na vytvoření třídy světa hry z ní jsme odskočili na vytvoření třídy prostorů, z ní na vytvoření třídy h-objektů, z ní na vytvoření třídy batohu. Po vytvoření základu batohu jsme mohli dokončit třídu h-objektů, po jejím dokončení jsme mohli dokončit třídu prostorů, po jejím dokončení jsme mohli dokončit třídu světa hry Strana 199 z 351

200 Kapitola 9: Vytváříme svět hry 200 a teprve po jejím dokončení se můžeme konečně vrátit zpět do třídy hry a dokončit v ní rozpracovaný příkaz. 47. Vraťte se do třídy hry a dokončete příkaz return, který jsme vkládali v podkapitole 9.1 Analýza chybového hlášení na straně Změňte typ návratové hodnoty metody na typ objektu reprezentujícího svět vaší hry. (Odpovídající metodu z ukázkové hry najdete ve výpisu 9.5.) Výpis 9.5: Upravená definice metody getworld() ve třídě RUPApartmentGame 1 /*************************************************************************** 2 * Vrátí odkaz na svět, v němž se hra odehrává. 3 * 4 Svět, v němž se hra odehrává 5 */ 7 public Apartment getworld() 8 { 9 return Apartment.getInstance(); 10 } 9.9 Test Tak jsme doplnili vše, čeho jsme si všimli. Určitě jsme ještě řadu věcí přehlédli, ale na ty nás upozorní testy. Spustíme tedy test. Když test nyní spustí hru, tak ta se již doopravdy spustí, vypíše úvodní zprávu, ale pak se opět zasekne a testovací program vypíše chybové hlášení viz výpis 9.6. Z něj se dozvíme, že: Svět hry nevrátil aktuální prostor. Je tedy stále ještě co napravovat, a tomu se budeme věnovat v následující kapitole. Výpis 9.6: Klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy Appartment oznamující nevrácení aktuálního prostoru 1 Ukončení bylo způsobeno vyhozením výjimky: 2 eu.pedu.adv15p_fw.test_util.common.testexception: 3 4 ============================================================================= 5 6 Po zadání příkazu hra odpověděla: Vítáme vás ve služebním bytě. Jistě máte hlad. 9 Najděte v bytě ledničku - tam vás čeká svačina Nacházíte se v místnosti: Předsíň 12 Sousedé: [Ložnice, Obývák, Koupelna] 13 H-objekty: [Botník, Deštník] Strana 200 z 351

201 Kapitola 9: Vytváříme svět hry Batoh: [] Při vyhodnocování odpovědi se objevil problém:svět hry nevrátil aktuální prostor 17 at eu.pedu.adv15p_fw.test_util.tgametests.tgamesteptester. describeerror(tgamesteptester.java:460) 18 at eu.pedu.adv15p_fw.test_util.tgametests.tgamesteptester. checkstate(tgamesteptester.java:200) 9.10 Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Ke stavu vývoje projektu, který jsme shrnuli v podkapitole 8.17 Co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali na straně 175, jsme přidali následující: Vložením nekompletního příkazu return do metody getworld() ve třídě hry jsme označili místo, kam se musíme po dokončení světa hry vrátit a program finálně upravit. Definovali jsme třídu, jejíž instance reprezentuje svět hry a slouží zároveň jako správce jednotlivých prostor. (V ukázkovém příkladu se jedná o třídu Apartment, jejíž instance reprezentuje služební byt.) Třídu jsme definovali jako soukromou v rámci balíčku, aby s ní objekty, které nejsou součástí daného projektu (a tím i balíčku), mohli pracovat jen způsobem specifikovaným v implementovaném interfejsu IArea. Definovali třídu, jejíž instance představují prostory hry. (V ukázkovém příkladu se jedná o třídu Room, jejíž instance reprezentují pokoje služebního bytu.) Ve třídě představující prostory hry jsme dále připravili: Upravili jsme hlavičku konstruktoru instancí tak, aby v parametrech převzal název vytvářeného prostoru, názvy jeho výchozích sousedů a názvy h-objektů, jež se v něm nacházejí na počátku hry. Deklarovali jsme atributy pro uložení převzatých polí. Tyto atributy jsme inicializovali. Definovali jsme a v konstruktoru inicializovali konstantu pro uložení kolekce aktuálních sousedů. V hlavičce metody getneighbors() jsme upravili typ návratové hodnoty, na kolekci instancí právě definovaného typu. Definovali jsme atribut pro výše zmíněnou kolekci sousedů zabalenou do objektu, který nedovolí změnu jejího obsahu. Strana 201 z 351

202 Kapitola 9: Vytváříme svět hry 202 Upravili jsme tělo metody getneighbors() tak, aby vracela tento atribut. Definovali jsme třídu, jejíž instance představují h-objekty, které se mohou vyskytovat v jednotlivých prostorech a/nebo v batohu. (V ukázkovém příkladu je to třída Item, jejíž instance reprezentují h-objekty v pokojích služebního bytu.) Při přípravě třídy h-objektů jsme řešili: Definovali jsme sadu atributů, které dostatečně popisují vlastnosti h-objektu v dané hře. Seznámili jsme se s alternativním způsobem zadávání dalších vlastností vytvářených h-objektů prostřednictvím prefixů jejich názvu. Vysvětlili jsme si, proč by mohl být tento způsob zadávání hodnot ve vaší hře výhodnější. Specifikovali jsme sadu prefixů definujících možné kombinace hodnot dodatečných vlastností. Zavedli jsme soukromé statické konstanty odpovídající jednotlivým hodnotám prefixu. Definovali jsme konstruktor, který zpracuje zadaný název včetně jeho prefixu. Definovali jsme přístupové metody vracející hodnoty definovaných atributů. Definovali jsme předběžnou verzi třídy batohu. (V ukázkovém příkladu se tato třída jmenuje Hands, protože představuje ruce hráče.) Po této odbočce jsme pokračovali v definici třídy prostorů (v ukázkovém příkladu třída Room): Definovali jsme a v konstruktoru inicializovali konstantu pro uložení kolekce h-objektů aktuálně se vyskytujících v daném prostoru. V hlavičce metody getitems() jsme upravili typ návratové hodnoty, na kolekci instancí našich prostorů. Definovali jsme atribut pro výše zmíněnou kolekci h-objektů zabalenou do objektu, který nedovolí změnu jejího obsahu. Upravili jsme tělo metody getitems() tak, aby vracela tento atribut. Poté jsme pokračovali v úpravách třídy světa hry (v ukázkovém programu třída Apartments). Definovali a inicializovali jsme kolekci spravovaných prostorů. Definovali jsme atribut pro výše zmíněnou kolekci spravovaných prostorů zabalenou do objektu, který nedovolí změnu jejího obsahu. Strana 202 z 351

203 Kapitola 9: Vytváříme svět hry 203 Upravili jsme tělo metody getallareas() tak, aby vracela tento atribut. Změnili jsme deklaraci hlavičky této metody tak, aby deklaroval vrácení kolekce našich prostorů. Doplnili jsme do konstruktoru příkazy pro vytvoření všech prostorů hry. Definovali jsme proměnný instanční atribut currentarea, v němž si bude správce světa hry pamatovat aktuální prostor, tj. prostor, v němž se právě nachází hráč. Definovali jsme tělo metody getcurrentarea(), která vrátí aktuální prostor. Program, jehož zdrojové kódy odpovídají výše popsanému stavu vývoje aplikace, najdete v projektu A109z_Apartment. Jeho diagram tříd si můžete prohlédnout na obrázku 9.1. Obrázek 9.1 Diagram tříd projektu A109z_Apartment zachycujícího stav poté, co byly definovány kostry tříd reprezentujících svět hry, prostory, h-objekty a batoh Strana 203 z 351

204 Kapitola 10: Dotahujeme svět hry Dotahujeme svět hry Kapitola 10 Dotahujeme svět hry Co se v kapitole naučíte Svět hry je sice vytvořen včetně třídy definující jím spravované prostory a h-objekty nacházející se v těchto prostorech, ale k tomu, aby se hra řádně rozběhla, se testům stále něco nelíbí. V této kapitole odstraníme poslední problémy a hru vítězně rozběhneme. V této kapitole budeme pokračovat v práci s projektem obsahujícím závěrečnou podobu projektu, k níž jsme se dopracovali v předchozí kapitole, tj. s projektem A109z_Apartment Úvahy o inicializaci Po spuštění testu na konci minulé kapitoly vypsal testovací program chybové hlášení, jehož část byla zobrazena ve výpisu 9.6 na straně 200. Jak už jsme si řekli, test nám oznamuje, že Svět hry nevrátil aktuální prostor. Metodu getcurrentarea(), která má aktuální prostor vrátit, jsme sice v pasáži Metoda getcurrentarea() na straně 199 definovali, ale nijak jsme přitom dále nerozebírali, jak zařídíme, aby ten v atributu currentarea opravdu byla požadovaná informace. Na počátku hry je aktuální prostor známý je definován ve startovacím kroku. Měli bychom jej tedy při startu hry nastavit. Jeho budoucími změnami pak pověříme příkazy, které jej budou měnit. Pojďme se tedy zamyslet nad optimální definicí počátečního nastavení. Prostory jsme sice vyráběli v konstruktoru třídy Apartment. Tam ale nemůžeme počáteční prostor nastavovat, protože jej musíme nastavit při každém spuštění Strana 204 z 351

205 Kapitola 10: Dotahujeme svět hry 205 hry. Ono se na počátku hry musí nastavovat více věcí správce prostorů např. musí zařídit, aby všechny prostory znaly své počáteční sousedy a h-objekty. Jako rozumné řešení se jeví definovat inicializační metodu, kterou by volal kód spouštějící hru. Protože inicializovat něco na počátku hry nebude určitě muset jenom správce prostorů, ale i jiné objekty, doporučoval bych následující řešení: 1. Ve správci akcí (třída AAction) definujte (prozatím prázdnou) metodu initialize(). 2. Tuto metodu zavolejte z místa, kde se startuje hra v ukázkovém programu je to metoda startgame(string). Optimální by bylo vložit volání metody před příkaz nastavující atribut isalive na true, protože hra by měla správně obživnout až poté, co budou všechny potřebné objekty inicializovány. Do této metody budeme přidávat volání stejnojmenných metod všech objektů, které budou potřebovat na počátku hry něco inicializovat. Nyní potřebujeme inicializovat svět hry, takže: 3. Do těla této metody přidejte zavolání metody initialize() správce prostorů. Podobu metod startgame(string) a initialize() ve třídě AAction v ukázkové hře si můžete prohlédnout ve výpisu Výpis 10.1: Upravená definice metody startgame(string) ve třídě AAction spolu s nově definovanou metodou initialize() 1 private static String startgame(string command) 2 { 3 String answer; 4 if (command.isempty()) { 5 initialize(); 6 answer = Texts.zCELÉ_UVÍTÁNÍ; 7 isalive = true; 8 } 9 else { 10 answer = Texts.zNENÍ_START; 11 } 12 return answer; 13 } /*************************************************************************** 17 * Metoda postupně inicializuje všechny klíčové objekty hry. 18 */ 19 private static void initialize() 20 { 21 Apartment.getInstance().initialize(); 22 } Strana 205 z 351

206 Kapitola 10: Dotahujeme svět hry Inicializace světa hry a správce prostorů Správce akcí jsme tedy zvládli, nicméně v definici jeho metody initialize() jsme volali metodu initialize() světa hry (viz výpis 10.1), kterou jsme však ještě nedefinovali. Pojďme se tedy přesunout do třídy světa hry (a současně správce jejích prostorů) a vše napravit. 4. Ve své třídě, jejíž (jediná) instance představuje svět hry (v ukázkové hře třída Appartment), definujte (prozatím prázdnou) metodu initialize(). Nastavení výchozího prostoru Chybové hlášení z posledního testu nám připomnělo (viz výpis 9.6 na straně 200), že jsme ještě neinicializovali atribut currentarea, v němž má být uložen aktuální prostor, tj. prostor, v němž se v daném okamžiku nachází hráč. Problém je, že jsme si tento prostor při konstrukci světa nezapamatovali. Máme tedy dvě možnosti: Vydolovat tento prostor z kolekce prostorů (v pasáži Pojmenované objekty interfejs INamed na straně 140 jsme se dozvěděli, že interfejs INamed nám k tomu poskytuje potřebnou metodu). Zavést atribut (nejlépe konstantní), v němž bychom si pamatovali výchozí prostor, a příslušně upravit definici konstruktoru. Já bych dal přednost druhé variantě, takže: 5. Definujte ve třídě světa hry konstantní atribut pro výchozí prostor hry. (V ukázkové hře se jmenuje startingroom.) 6. Upravte definici konstruktoru tak, aby se v ní tento atribut inicializoval ještě před tím, než začnete vytvářet prostory a vkládat je do mapy. 7. V příkazu vkládajícím výchozí prostor do mapy nahraďte dosavadní volání konstruktoru předáním před chvílí inicializovaného atributu. V ukázkové aplikaci je to vyřešeno následovně: startingroom = new Room(PŘEDSÍŇ, new String[] {LOŽNICE, OBÝVÁK, KOUPELNA}, STANDARD+BOTNÍK, STANDARD+DEŠTNÍK); rooms.add(startingroom); 8. Do těla metody initialize() vložte příkaz, který tento výchozí prostor nastaví jako aktuální prostor, tj. jako hodnotu proměnné currentarea. Strana 206 z 351

207 Kapitola 10: Dotahujeme svět hry 207 Vyvolání inicializace jednotlivých prostorů Když už jsme u té inicializace, tak bychom si měli uvědomit, že objekt světa hry slouží současně jako správce prostorů. Měl by proto inicializovat také všechny spravované prostory. Když tyto prostory ve svém konstruktoru vytvářel, tak každému prozradil názvy jeho počátečních sousedů a h-objektů. Nyní je ta správná chvíle k tomu, aby prostory tuto informaci použily a také se inicializovaly. 9. V metodě initialize() světa hry vložte před nastavení aktuálního prostoru kód, který postupně požádá všechny prostory, aby se inicializovaly, tj. zavolá jejich metodu initialize(). Metoda tak může získat tvar podobný metodě z výpisu Výpis 10.2: Definice metody initialize() ve třídě Apartment 1 /*************************************************************************** 2 * Metoda inicializující svět hry. 3 * Nejprve inicializuje všechny prostory 4 * a pak nastaví výchozí aktuální prostor. 5 */ 6 void initialize() 7 { 8 rooms.foreach(room::initialize); 9 currentarea = startingroom; 10 } 10.3 Inicializace jednotlivých prostorů Situace se opakuje. Předchozí podkapitolu jsme opět skončili definicí příkazu, jímž jsme volali metodu, kterou jsme ještě nedefinovali. Její definice už bude o něco složitější. Začneme tím, nad čím není třeba přemýšlet: 10. Definujte ve třídě prostorů metodu initialize(). Prázdnou verzi metody máme. Nyní musíme vymyslet, jak co nejlépe definovat její tělo. Prostory si při své konstrukci zapamatovaly pole s názvy počátečních sousedů a pole s názvy počátečních h-objektů. Při inicializaci by proto měly na základě těchto informací naplnit kontejner objekty reprezentujícími tyto sousedy a další kontejner naplnit výchozími h-objekty. Aby program zůstal co nejpřehlednější, definujeme pro každou z těchto činností samostatnou metodu, kterou bude inicializační metoda volat. Pro vás z toho plnou úkoly: Strana 207 z 351

208 Kapitola 10: Dotahujeme svět hry Definujte ve třídě prostorů (prozatím prázdnou) metodu initializeitems(). 12. Definujte ve třídě prostorů metodu initializeneighbors(). 13. Doplňte do těla metody initialize() volání každé z těchto metod (viz výpis 10.3). Výpis 10.3: Definice metody initialize() ve třídě Room 1 /*************************************************************************** 2 * Metoda inicializující daný prostor. 3 * Na základě konstruktorem zapamatovaných jmen 4 * inicializuje sousedy daného prostoru a přítomné h-objekty. 5 */ 6 void initialize() 7 { 8 initializeitems(); 9 initializeneighbors(); 10 } Nastavení h-objektů uložených v místnosti na počátku hry Začneme s vytvořením kolekce uložených h-objektů. Jejich názvy jsou uloženy v atributu-poli itemnames, takže je z něj stačí jeden po druhém brát, vytvořit za pomoci konstruktoru příslušný h-objekt a uložit jej do odpovídající kolekce. Měli bychom ale myslet na to, že tato kolekce může obsahovat h-objekty z minulého běhu hry, takže bychom při dalším startu hry pouze přidávali další h-objekty k těm, které již v dané kolekci jsou. Jinými slovy: nesmíme zapomenout kolekci nejprve vymazat, a teprve pak ji začít plnit. Prostory si při své konstrukci zapamatovaly pole s názvy počátečních sousedů, resp. počátečních h-objektů. Při inicializaci by měly na základě těchto informací naplnit pole s objekty reprezentujícími tyto sousedy. 14. Definujte tělo metody initializeitems() tak, aby nejprve vyčistila kolekci items a pak ji naplnila objekty reprezentujícími výchozí h-objekty v daném prostoru. Definice použitá v ukázkové aplikaci (viz výpis 10.4) využívá datovod názvů, jehož prvky předává konstruktoru h-objektů a získané h-objekty pak ukládá do kolekce items. Výpis 10.4: Definice metody initializeitems() ve třídě Room 1 /*************************************************************************** 2 * Vyčistí kolekci {@link #items} a uloží do ní objekty reprezentující 3 * h-objekty vyskytující se v daném prostoru na počátku hry. 4 */ 5 private void initializeitems() Strana 208 z 351

209 Kapitola 10: Dotahujeme svět hry { 7 items.clear(); 8 Arrays.stream(itemNames) 9.map(Item::new) 10.forEach(items::add); 11 } Příprava na nastavení počátečních sousedů S nastavením kolekce výchozích sousedů to bude maličko složitější. Sousedé, tj. příslušné prostory již existují, ale nemáme metodu, které bychom předali název, a ona by nám vrátila odpovídající prostor. Mohli bychom sice požádat objekt světa hry, aby nám poskytl kolekci všech prostorů (viz pasáž Kolekce prostorů hry getallareas() na straně 137), a poté z ní za pomoci statické metody interfejsu INamed (viz pasáž Pomocné statické metody geto(string,?) na straně 141) získat příslušný prostor. Na druhou stranu ale možná cítíte, že metodu, které předáme název prostoru, a ona nám vrátí daný prostor, budeme v budoucnu určitě potřebovat např. při zpracování příkazu pro přechod z prostoru do prostoru. Mohlo by se proto hodit, kdyby svět hry dokázal vrátit nejenom kolekci všech prostorů, ale i konkrétní prostor se zadaným názvem. Postup, jak to udělá, jej popsán na počátku tohoto odstavce. Podíváte-li se ale na výše zmíněné statické metody interfejsu INamed, zjistíte, že vracejí hledaný pojmenovaný objekt zabalený v objektu typu Optional<E>, kde E je typ hledaného pojmenovaného objektu. Budeme-li se držet současného trendu popsaného v podšeděném bloku Datový typ Optional<E> na straně 142, tak by hodnotu tohoto typu měla vracet i naše metoda. Budeme-li se držet i názvových konvencí uvedených tamtéž, pak váš další úkol bude: 15. Ve třídě světa hry definujte metodu getoroom(string), které předáte název požadovaného prostoru, a ona vám vrátí daný prostor zabalený v objektu typu Optional<E>, kde E představuje datový typ vašich prostorů. (Definici této metody v ukázkové aplikaci si můžete prohlédnout ve výpisu 10.5.) Výpis 10.5: Definice metody getoroom(string)ve třídě Apartment 1 /*************************************************************************** 2 * Vrátí prostor se zadaným názvem zabalený v objektu typu {@link Optional}. 3 * 4 name Název požadovaného prostoru 5 Zabalený prostor se zadaným názvem 6 */ 7 public Optional<Room> getoroom(string name) 8 { 9 Optional<Room> result = INamed.getO(name, rooms); 10 return result; Strana 209 z 351

210 Kapitola 10: Dotahujeme svět hry } Nastavení počátečních sousedů Nyní již je vše připraveno k tomu, abyste mohli bez problémů (téměř to svádí říci bez přemýšlení) definovat tělo metody initializeneighbors(). Takže: 16. Definujte tělo metody initializeneighbors(), která vyčistí kolekci neighbors a naplní ji počátečními sousedy daného prostoru. (Definici této metody v ukázkové aplikaci si můžete prohlédnout ve výpisu 10.6.) Výpis 10.6: Definice metody initializeneighbors() ve třídě Room 1 /*************************************************************************** 2 * Vyčistí kolekci {@link #neighbors} a uloží do ní objekty reprezentující 3 * prostory sousedící s daným prostorem na počátku hry. 4 */ 5 private void initializeneighbors() 6 { 7 Apartment apartment = Apartment.getInstance(); 8 neighbors.clear(); 9 Arrays.stream(neighborNames) 10.map(apartment::getORoom) 11.map(Optional<Room>::getO) 12.forEach(neighbors::add); 13 } 10.4 Test Test spuštěný po všech výše uvedených úpravách sice opět skončí chybovým hlášením (jeho část najdete ve výpisu 10.7) a vyhozením výjimky, ale bude vidět, že jsme se dostali zase o kus dál. Tentokrát se výpisu se dozvídáme, že ve třídě hry ještě není plnohodnotně definována metoda getbag(?). Jenomže prozatím nemůžeme metodu opravit, třídu batohu jsme ještě nedefinovali jdeme to napravit. Výpis 10.7: Klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy Room oznamující nedotažení definice metody getbag() ve třídě RUPApartmentGame 1 Ukončení bylo způsobeno vyhozením výjimky: 2 eu.pedu.adv15p_fw.test_util.common.testexception: 3 4 ============================================================================= 5 6 Po zadání příkazu hra odpověděla: Vítáme vás ve služebním bytě. Jistě máte hlad. Strana 210 z 351

211 Kapitola 10: Dotahujeme svět hry Najděte v bytě ledničku - tam vás čeká svačina Nacházíte se v místnosti: Předsíň 12 Sousedé: [Ložnice, Obývák, Koupelna] 13 H-objekty: [Botník, Deštník] 14 Batoh: [] Při vyhodnocování odpovědi se objevil problém: 17 Metoda ještě není hotova. 18 at eu.pedu.adv15p_fw.test_util.tgametests.tgamesteptester.describeerror( TGameStepTester.java:458) 19 at eu.pedu.adv15p_fw.test_util.tgametests.tgamesteptester.checkstate(tgamesteptester.java:200) 20 at eu.pedu.adv15p_fw.test_util.tgametests.tgamesteptester.verify(tgamesteptester.java:155) 21 at eu.pedu.adv15p_fw.test_util.tgametests.tgameruntester.verifyscenariostep( TGameRunTester.java:284) 22 at eu.pedu.adv15p_fw.test_util.tgametests.tgameruntester.executescenario( TGameRunTester.java:141) 23 at eu.pedu.adv15p_fw.test_util.tgametests.tgametester.testgamebyscenario(tgametester.java:176) 24 at eu.pedu.adv15p_fw.test_util.tgametests.tgametester.testgamebyscenarios(tgametester.java:158) 25 at eu.pedu.adv15p_fw.test_util.tgametests.tgametester.testgame(tgametester.java:141) 26 at eu.pedu.adv15p_fw.scenario.ascenariomanager.testgame(ascenariomanager.java:411) 27 at eu.pedu.adventure15p.rupscenariomanagercon.main(rupscenariomanagercon.java:642) 28 Caused by: java.lang.unsupportedoperationexception: 29 Metoda ještě není hotova. 30 at eu.pedu.adventure15p.rupapartmentgame.getbag(rupapartmentgame.java:125) 31 at eu.pedu.adventure15p.rupapartmentgame.getbag(rupapartmentgame.java:29) 32 at eu.pedu.adv15p_fw.test_util.tgametests.tgamesteptester.describeerror( TGameStepTester.java:441) more 10.5 Oprava definice metody getbag() ve třídě hry První sada odkazů do zásobníku (ve výpisu 10.7 řádky 18 až 27) nám sice moc neřekne, ale podíváme-li se na skutečnou příčinu chyby (řádky 28 až 30), dozvíme se, že prvotní příčinou bylo vyhození výjimky UnsupportedOperationException metodou getbag() ve třídě hry. Tato metoda má vrátit objekt batohu. Možná si vzpomenete, že třídu batohu jsme předběžně vytvářeli v pasáži Předběžné vytvoření třídy batohu na straně 191. O batohu víme, že je v celé hře jediný, takže je už šablonová třída definována podle vzoru Jedináček (Singleton). Nedotaženou metodu tedy můžeme definovat standardním, již několikrát opakovaným postupem: 17. Změňte tělo metody getbag() ve třídě hry tak, aby vracela instanci batohu. Strana 211 z 351

212 Kapitola 10: Dotahujeme svět hry Změňte typ návratové hodnoty metody na typ vašeho batohu. (Definici metody v ukázkové hře najdete ve výpisu 10.8.) Výpis 10.8: Definice metody getbag() ve třídě RUPApartmentGame 1 /*************************************************************************** 2 * Vrátí odkaz na batoh, do nějž bude hráč ukládat sebrané h-objekty. 3 * 4 Batoh, do nějž hráč ukládá sebrané h-objekty 5 */ 7 public Hands getbag() 8 { 9 return Hands.getInstance(); 10 } Test Test dopadne obdobně jako minule (tentokrát jeho výpis neuvádím, je velmi podobný tomu minulému). Na konci výpisu zásobníku návratových adres vypsaných po vyhození výjimky UnsupportedOperationException se dozvíme, že metoda getitems() ve třídě batohu ještě není hotova Dotažení definice třídy batohu Test nám připomněl, že jsme třídu batohu definovali opravdu jenom prozatímně, takže na ní bude určitě hodně co dotahovat. Začneme s metodou, na kterou nás upozornilo chybové hlášení. Metoda getitems() Metoda getitems() má vrátit kolekci h-objektů v batohu. Aby bylo co vracet, musí kolekce nejprve existovat. 19. Definujte konstantní atribut, který bude obsahovat kolekci h-objektů nacházejících se v batohu. V doprovodném projektu má tato deklarace tvar: Collection<Item> items; 20. Doplňte do těla konstruktoru příkaz inicializující tuto kolekci prázdným seznamem typu ArrayList. Nyní bychom měli definovat tělo metody getitems(), která bude obsah této kolekce vracet. Objeví se ale stejný problém, s nímž jsme se setkali již ve třídě prostorů Strana 212 z 351

213 Kapitola 10: Dotahujeme svět hry 213 a který jsem rozebíral v podšeděném bloku Zabezpečení nezměnitelnosti kontejneru na straně 185. Zopakujeme proto tehdejší řešení: 21. Doplňte atribut reprezentující kolekci h-objektů v daném prostoru zabalenou využitím metody unmodifiablecollection(collection) (podrobnosti o tomto tématu najdete v podšeděném bloku Zabezpečení nezměnitelnosti kontejneru na straně 185). V ukázkové aplikace v doprovodném programu se tento atribut jmenuje exporteditems. 22. Doplňte tělo konstruktoru příkazem pro inicializaci tohoto atributu. 23. Upravte definici těla metody getitems() tak, že bude vracet tento před chvílí definovaný atribut. 24. Upravte hlavičku metody getitems() z původního public Collection<? extends IItem> getitems() na tvar nepoužívající v typovém parametru žolík. V doprovodném projektu má hlavička této metody tvar: public Collection< Item > getitems () Metoda initialize() Hned bychom si měli vzpomenout na to, jak jsme před nedávnem inicializovali prostory. I batoh bude mít často na počátku hry jiný obsah než na konci hry předešlé, a proto je třeba jej inicializovat. Proto: 25. Definujte metodu initialize(). 26. V těle metody nejprve vyčistěte kolekci items. V ukázkové hře nezačínáme s batohem, v němž už něco je, takže nemusíme v rámci inicializace vkládat potřebné h-objekty do batohu. Kdo z vás takou hru vytváří, může se inspirovat inicializací stejnojmenné kolekce ve třídě Rooms. Takže: 27. Pokud jsou ve vaší hře na počátku v batohu nějaké h-objekty, doplňte do těla metody initialize() příkazy, které tyto h-objekty do kolekce vloží. Začlenění inicializace batohu mezi ostatní To, že jsme inicializační metodu definovali, ještě neznamená, že se batoh bude na počátku každé hry inicializovat. Musíme zařídit, aby ji někdo ve vhodnou chvíli zavolal. Jestli si vzpomínáte, veškeré inicializace má na starosti metoda initialize() definovaná ve třídě AAction (viz podkapitola 10.1 Úvahy o inicializaci na straně 204). Tam je to správné místo. Strana 213 z 351

214 Kapitola 10: Dotahujeme svět hry Do těla metody initialize() ve třídě AAction přidejte příkaz aktivující inicializaci batohu. V případě ukázkové hry by to byl příkaz Hands.getInstance().initialize(); Metoda getcapacity() Batoh už jsme téměř připravili. Zbývá už jen metoda getcapacity(), která má vrátit kapacitu batohu. Definice jejího těla je opět prostoduchá: 29. Upravte definici těla metody getcapacity() tak, aby metoda vracela hodnotu konstanty CAPACITY Test Po následném spuštění testu hry se testovací program trošku rozpovídá a v okně pro standardní výstup objeví základní test správce scénářů, po něm zpráva o provedení startovacího kroku a za ní zpráva, kterou si můžete přečíst ve výpisu Z něj se dočteme, že naše hra nezpracovala až druhý příkaz. (Který to byl, závisí na scénáři, jenž se pokoušíte realizovat.) Vypadá to, že definici částí programu potřebných pro spuštění hry a vyhodnocení jejího stavu po provedení startovacího kroku jsme zvládli, protože chyba se objevila až při vyhodnocování následujícího příkazu. Můžeme se tedy pustit do definic jednotlivých akcí. Těm se budeme věnovat v následující kapitole. Výpis 10.9: Klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy batohu; zpráva oznamuje neschopnost hry reagovat na příkaz Jdi Koupelna 1 ============================================================================= 2 Při testu následujícího, tj. 1. kroku: Jdi Koupelna 3 se objevila CHYBA 4 ============================================================================= 5 Začátky očekávané a obdržené zprávy nesouhlasí. 6 Čekám: «Přesunul(a) jste se do místnosti: Koupelna» 7 Přišlo: «Tento příkaz neznám.chcete-li poradit, zadejte příkaz?» 8 Ověřený počátek zprávy: 9 10 Odchylka na pozici 0 11 očekávám kód 80 znak «P» 12 obdržel jsem kód 84 znak «T» 13 ============================================================================= ===== Očekáváno ===== krok: «Jdi Koupelna» Strana 214 z 351

215 Kapitola 10: Dotahujeme svět hry Očekávaný stav po provedení akce: krok 21 Typ kroku: tsmove 22 Příkaz: Jdi Koupelna 23 Prostor: Koupelna 24 Východy: («Předsíň») 25 H-objekty: («Brýle», «Umyvadlo», «Časopis») 26 Batoh: () Zpráva: 29 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 30 Přesunul(a) jste se do místnosti: Koupelna ===== Obdrženo ===== krok: «Jdi Koupelna» Očekávaný stav po provedení akce: krok 39 Typ kroku: tsnot_set 40 Příkaz: Jdi Koupelna 41 Prostor: Předsíň 42 Východy: («Koupelna», «Ložnice», «Obývák») 43 H-objekty: («Botník», «Deštník») 44 Batoh: () Zpráva: 47 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 48 Tento příkaz neznám.chcete-li poradit, zadejte příkaz? ============================================================================= 10.8 Co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Zopakujme si, co jsme v této kapitole dozvěděli a co jsme naprogramovali: Ke stavu vývoje projektu, který jsme shrnuli v podkapitole 9.10 Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali na straně 201, jsme přidali následující: Upravili jsme metodu startgame(string) ve třídě hry tak, že po odstartování hry nastaví atribut isalive na true. Ve třídě správce akcí jsme definovali metodu initialize(), do níž se umisťují volání inicializačních metod, které se budou spouštět po každém startu hry. Strana 215 z 351

216 Kapitola 10: Dotahujeme svět hry 216 V této metodě jsme pro začátek zavolali metodu initialize() objektu představujícího svět hry. Ve třídě světa hry jsme tuto metodu definovali tak, že zavolá inicializační metodu všech spravovaných prostorů, a poté nastaví výchozí aktuální prostor. Pro aktuální prostor jsme definovali zvláštní konstantní atribut, který se nastavuje v konstruktoru světa hry. Definovali jsme metodu initialize() objektů představujících prostory. V této metodě jsme inicializovali výchozí h-objekty a výchozí sousedy daného prostoru. Pro zpřehlednění programu jsme pro každou z těchto inicializací definovali zvláštní metodu. Ve třídě světa hry jsme definovali metodu, která převede název prostoru na odkaz na daný prostor. Program, jehož zdrojové kódy odpovídají výše popsanému stavu vývoje aplikace, najdete v projektu A110z_GameStarted. Jeho diagram tříd si můžete prohlédnout na obrázku Obrázek 10.1 Diagram tříd projektu A110z_GameStarted zachycujícího stav poté, co byla v testovacím režimu rozběhnuta hra Strana 216 z 351

217 Kapitola 11: Vytvoření prvních definic povinných akcí Vytvoření prvních definic povinných akcí Kapitola 11 Vytvoření prvních definic povinných akcí Co se v kapitole naučíte V této kapitole se zaměříme na vytvoření definic povinných akcí, tj. akcí pro vyvolání nápovědy, přesun z prostoru do prostoru, zvednutí a položení h-objektu a předčasné ukončení. V této kapitole se budeme věnovat prvním třem, zbylé dvě odložíme do kapitoly příští. V této kapitole budeme pokračovat v práci s projektem obsahujícím závěrečnou podobu projektu, k níž jsme se dopracovali v předchozí kapitole, tj. s projektem A110z_GameStarted Jak sjednotit postup pro většinu her Všechny hry musejí mít definovány sadu povinných akcí. Bude-li proto chtít co nejdéle ukazovat řešení, kterým se budete moci maximálně inspirovat při vývoji vlastní aplikace, bylo by vhodné spolu projít definici tříd, jejichž instance budou mít na starosti zpracování příkazů vyvolávajících tyto povinné akce. Chceme-li nejprve definovat mateřské třídy základních akcí, nemusí být rozumné postupovat nadále dosavadním způsobem, protože mnohé scénáře jsou koncipovány tak, že některá z povinných akcí se použije až poté, co se použije některá z těch nestandardních. Abychom mohli začít s definicí povinných akcí naším oblíbeným systémem test oprava test oprava, potřebovali bychom mít zaručeno, že ve scénáři budou nejprve kroky s příkazy vyvolávajícími povinné akce, a až poté kroky s pří- Strana 217 z 351

218 Kapitola 11: Vytvoření prvních definic povinných akcí 218 kazy pro vyvolání akcí nestandardních. V některých případech by ale takovýto požadavek boural celou koncepci navržené hry. Existuje ale způsob, jak toho dosáhnout, aniž bychom museli koncepci hry upravovat můžeme definovat nový scénář, který bude těmto požadavkům vyhovovat. Po tomto scénáři budeme požadovat, aby obsahoval následující kroky v uvedeném pořadí (to abychom sjednotili postup definic tříd): 1. Startovací krok 2. Vypsání nápovědy 3. Přesun do sousedního prostoru 4. Zvednutí h-objektu v tomto prostoru 5. Položení zvednutého h-objektu 6. Předčasné ukončení hry Pokud jste splnili okrajové podmínky popsané v pasáži Dodatečný požadavek k usnadnění testů na straně 108, tak by pro vás neměl být problém takovýto scénář definovat. Jediným případem, kdy by mohl požadavek na vytvoření takovéhoto scénáře kolidovat s koncepcí hry, by byly hry, v nichž hráč začíná s plným batohem. Pak by bylo potřeba buď zvětšit kapacitu batohu, anebo prohodit kroky 4 a 5. Moje zkušenost ale říká, že takového návrhy scénářů vznikají zcela výjimečně z těch několika set scénářů, které mí studenti doposud vytvořili, tak začínal pouze jediný. Pokud tedy někdo z vás takovýto scénář navrhl a nechce se mu zvětšovat kapacitu batohu či jinak měnit koncepci své hry, musí v následujícím výkladu brát část o vytváření akcí pro manipulaci s h-objekty pouze jako inspirativní, protože jeho program se s ním bude bavit trochu jinak Definice nového scénáře Teoretický rozbor požadavků na dodatečný scénář máme za sebou, takže pojďme jej vytvořit. Chceme-li zavést nový scénář, musíme nejprve definovat jeho kroky. Budeme-li chtít, aby byly kroky opět číslovány od jedničky 25, potřebujeme nejprve reinicializovat číslování kroků. Proto: 1. Vložte do třídy správce scénářů za definici pole MISTAKE_SCENARIO_STEPS, a před definici atributu MANAGER statický inicializační blok (součást konstruktoru tří- 25 Počáteční, tj. startovací krok má index 0. Definice pro tento krok se ale do všech scénářů kopíruje, takže pro něj není třeba cokoliv nastavovat. Číslování se nastavuje se až pro kroky následující. Strana 218 z 351

219 Kapitola 11: Vytvoření prvních definic povinných akcí 219 dy) s příkazem pro reinicializaci číslování kroků dalšího scénáře. Jinými slovy: vložte na zadané místo kód static { ScenarioStep.setIndex(1); } který nastaví index příštího vytvářeného kroku scénáře na jedničku. Další doporučené kroky povedou k vytvoření pomocného scénáře přesně podle číslovaných požadavků z počátku předchozí podkapitoly. 2. Za právě vložený příkaz vložte definici pole kroků scénáře. V ukázkové aplikaci je pojmenováno REQUIRED_STEPS. Použijete-li stejný název, vložíte tedy definici private static final ScenarioStep[] REQUIRED_STEPS = { }; 3. Do definice tohoto pole vložte odkaz na startovací krok obdobně, jako jste to udělali např. v základním úspěšném scénáři. 4. Za tento krok vložte krok s příkazem pro vypsání nápovědy, tj. krok typu tshelp. 5. Za něj vložte krok s příkazem pro přesun do některého ze sousedních prostorů, tj. krok typu tsmove. Bylo by vhodné, kdybyste se přesunuli do prostoru, v němž existuje h-objekt, který je možné zvednout. Jak už jsem řekl, kdyby ve vaší hře mezi sousedními prostory výchozího prostoru žádný prostor se přenositelným h-objektem nebyl, bylo by vhodné do některého sousedního prostoru nějaký symbolický h-objekt umístit. 6. Za něj vložte krok s příkazem pro zvednutí h-objektu, tj. krok typu tspick_up. (Možná řešení situace, kdy máte hned na počátku plný batoh, jsme probrali v teoretickém úvodu v předchozí podkapitole.) 7. Za něj vložte krok s příkazem pro položení zvednutého h-objektu, tj. krok typu tsput_down. 8. Za něj vložte krok s příkazem pro předčasné ukončení hry, tj. krok typu tsend. V naší demonstrační hře by výše popsaná definice požadovaného pole kroků vypadala podle výpisu Výpis 11.1: Definice pole REQUIRED_STEPS ve třídě RUPScenarioManagerCon 1 static { ScenarioStep.setIndex(1); } /*************************************************************************** 5 * Kroky scénáře určeného pro prověření povinných akcí, Strana 219 z 351

220 Kapitola 11: Vytvoření prvních definic povinných akcí * přesněji akcí pro přechod do prostoru, zvednutí a položení h-objektu, 7 * vypsání nápovědy a pro předčasné ukončení hry. 8 */ 9 private static final ScenarioStep[] REQUIRED_STEPS = 10 { 11 START_STEP 12, 13 new ScenarioStep(tsHELP, phelp, 14 znápověda 15, 16 PŘEDSÍŇ, 17 new String[] { LOŽNICE, OBÝVÁK, KOUPELNA }, 18 new String[] { BOTNÍK, DEŠTNÍK }, 19 new String[] {} 20 ) 21, 22 new ScenarioStep(tsMOVE, pjdi + " " + KOUPELNA, 23 zpřesun + KOUPELNA 24, 25 KOUPELNA, 26 new String[] { PŘEDSÍŇ }, 27 new String[] { BRÝLE, UMYVADLO, ČASOPIS }, 28 new String[] {} 29 ) 30, 31 new ScenarioStep(tsPICK_UP, pvezmi + " " + BRÝLE, 32 zzvednuto + BRÝLE 33, 34 KOUPELNA, 35 new String[] { PŘEDSÍŇ }, 36 new String[] { UMYVADLO, ČASOPIS }, 37 new String[] { BRÝLE } 38 ) 39, 40 new ScenarioStep(tsPUT_DOWN, ppolož + " " + BRÝLE, 41 zpoloženo + BRÝLE 42, 43 KOUPELNA, 44 new String[] { PŘEDSÍŇ }, 45 new String[] { BRÝLE, UMYVADLO, ČASOPIS }, 46 new String[] {} 47 ) 48, 49 new ScenarioStep(tsEND, pkonec, 50 zkonec 51, 52 KOUPELNA, 53 new String[] { PŘEDSÍŇ }, 54 new String[] { BRÝLE, UMYVADLO, ČASOPIS }, 55 new String[] {} 56 ) 57, Strana 220 z 351

221 Kapitola 11: Vytvoření prvních definic povinných akcí }; 11.3 Úprava definice konstruktoru Kroky máme definovány. Teď ještě musíme zařídit, aby se vytvořil scénář obsahující tyto kroky. Musíme proto do konstruktoru přidat příkaz, který vytvoření takovéhoto scénář zabezpečí. Tomuto scénáři musíme přiřadit jméno. Dohodněme se, že se scénář bude jmenovat REQUIRED. Protože víme, že magické hodnoty jsou v běžných programech zakázané, bylo by vhodné definovat tuto hodnotu jako konstantu, na kterou se budeme moci v dalším programu odvolávat. Proto: 9. Definujte statickou konstantu s hodnotou "REQUIRED". V ukázkové aplikaci je definována prostřednictvím příkazu private static final String REQUIRED_STEPS_SCENARIO_NAME = "REQUIRED"; Nyní již zbývá pouze přidat do definice konstruktoru požadavek na přidání dalšího scénáře. 10. Doplňte do těla konstruktoru vašeho správce scénářů příkaz, který přidá scénář s výše specifikovanými kroky. V ukázkové aplikaci má upravený konstruktor tvar z výpisu 11.2, v němž je přidaný příkaz na řádcích 12 a 13. Výpis 11.2: Upravená definice konstruktoru třídy RUPApartmentManager 1 /*************************************************************************** 2 * Vytvoří instanci představující správce scénářů hry. 3 */ 4 private RUPScenarioManagerCon() 5 { 6 super(author_name, AUTHOR_ID, CLASS); 7 8 addscenario(happy_scenario_name, 9 TypeOfScenario.scHAPPY, HAPPY_SCENARIO_STEPS); 10 addscenario(mistake_scenario_name, 11 TypeOfScenario.scMISTAKES, MISTAKE_SCENARIO_STEPS); 12 addscenario(required_steps_scenario_name, 13 TypeOfScenario.scGENERAL, REQUIRED_STEPS); 14 seal(); 15 } Strana 221 z 351

222 Kapitola 11: Vytvoření prvních definic povinných akcí Úprava definice metody main(string[]) Konstruktor jsme upravili. Nyní musíme upravit ještě metodu main(string[]), která spouští testy. 11. Zakomentujte příkaz pro test hry, který prověřuje chod hry podle úspěšného a chybového scénáře. 12. Odkomentujeme následující příkaz pro testování hry podle scénářů se zadanými názvy. 13. Jako parametr zadejte před chvílí definovanou konstantu s názvem scénáře s povinnými kroky, který jste před chvílí definovali. Výslednou podobu metody main(string[]) v ukázkové aplikaci si můžete prohlédnout ve výpisu Výpis 11.3: Upravená definice metody main(string[]) ve třídě RUPScenarioManagerCon 1 public static void main(string[] args) 2 { 3 //Otestuje, zda správce scénářů a jeho scénáře vyhovují požadavkům 4 // MANAGER.autoTest(); 5 6 //Vypíše na standardní výstup simulovaný průběh hry 7 //odehrané podle základního úspěšného scénáře 8 // MANAGER.getHappyScenario().simulate(); 9 10 //Testování hry prováděné postupně podle obou povinných scénářů, 11 //přičemž základní úspěšný scénář se prochází dvakrát za sebou 12 // MANAGER.testGame(); //Testování hry dle scénářů se zadanými názvy 15 MANAGER.testGameByScenarios(REQUIRED_STEPS_SCENARIO_NAME); //Odehrání hry dle scénáře se zadaným názvem 18 // MANAGER.playGameByScenario("???"); System.exit(0); 21 } 11.5 Definice akce pro nápovědu Při kontrolním spuštění testu bychom měli obdržet obdobnou zprávu, jakou jsme obdrželi na konci minulé kapitoly. Testovací program by tentokrát zadal hře příkaz pro vypsání nápovědy a hra by oznámila, že takový příkaz nezná. Ale to je správně, protože jsme příslušnou akci ještě nedefinovali. Pojďme to tedy napravit. Strana 222 z 351

223 Kapitola 11: Vytvoření prvních definic povinných akcí 223 Zkopírování kostry třídy Jak si jistě domyslíte, začátek definice požadované akce bude stejný jako začátek definice všech předchozích tříd: 14. Z balíčku empty_classes zkopírujte do svého balíčku třídu EmptyAction. (Pozor! Nespleťte si ji s třídou EmptyAAction tu jsme již zkopírovali v podkapitole 8.9 Definice správce akcí třídy AAction na straně 157.) 15. Zkopírovanou třídu přejmenujte tak, aby název naznačoval, že instance třídy realizují akci poskytnutí nápovědy. V ukázkové aplikaci jsem jí přiřadil název ActionHelp. Projděme nyní zdrojový kód nově vytvořené třídy a podívejme se, co bychom mohli hned, bez přemýšlení upravit a definovat. Rodičovská třída První, na co narazíme, je chyba v hlavičce třídy. Naše třída je totiž definována jako potomek třídy EmptyAAction, což, jak víme, není to, co bychom chtěli. 16. Smažte slovíčko Empty v názvu rodičovské třídy. Při dalším procházení zdrojového kódu narazíme na definici konstruktoru, která obsahuje volání konstruktoru rodičovského podobjektu, jemuž předává v parametrech název a popis dané akce. 17. Podívejte se do správce scénářů na přesný název této akce (v ukázkové aplikace je uložen v konstantě Texts.pHELP) a upravte hodnoty parametrů volání konstruktoru rodičovského podobjektu. Podobu konstruktoru v ukázkové aplikaci si můžete prohlédnout ve výpisu Výpis 11.4: Definice konstruktoru instancí třídy ActionHelp 1 /*************************************************************************** 2 * Vytvoří instanci akce pro 3 * vypsání nápovědy s názvy a stručnými popisy dostupných akcí. 4 */ 5 ActionHelp() 6 { 7 super (Texts.pHELP, 8 "Vypíše nápovědu s názvy a stručnými popisy dostupných příkazů."); 9 } Strana 223 z 351

224 Kapitola 11: Vytvoření prvních definic povinných akcí 224 Metoda execute(string...) Budeme-li pokračovat v analýze zkopírovaného kódu, narazíme na definici metody execute(string...), která přebíjí abstraktní metodu rodičovské třídy. Tato metoda bude mít na starosti provedení příkazu žádajícího nápovědu. Pojďme se podívat, jak ji definovat. Zpracování příkazu žádajícího nápovědu by mělo být poměrně jednoduché: zeptáme se hry na všechny její akce a pak postupně vypíšeme názvy jednotlivých akcí spolu s jejich popisem. Začneme tedy získáním kolekce. Vzpomenete-li si na sadu metod požadovaných po objektu hry (viz pasáž IGame na straně 137), tak víte, že se mezi nimi nachází metoda getallactions(), která má umět takovouto kolekci vrátit. Doplnění těla metody getallactions() ve třídě hry Ti pozornější si možná vzpomenou, že jsme v pasáži Tři skupiny objektů na straně 154 hovořili o tom, že objekt hry patří mezi organizátory, a ty bychom měli definovat tak, aby se soustředily na delegování činností na specializované objekty, které pak danou činnost provedou. Na práci s akcemi je specializovaný jejich správce, tj. objekt třídy AAction. Z toho tedy vyplývá další krok: 18. Definujte ve třídě hry tělo metody getallactions() tak, aby hra delegovala zodpovědnost za získání kolekce na objekt třídy AAction, přesněji na jeho stejnojmennou metodu. 19. Upravte v hlavičce metody typ návratové hodnoty tak, že metoda bude vracet instance vaší třídy AAction. Definice proto bude mít tvar z výpisu Výpis 11.5: Definice metody getallactions() ve třídě RUPApartmentGame 1 /*************************************************************************** 2 * Vrátí kolekci všech akcí použitelných ve hře. 3 * 4 Kolekce všech akcí použitelných ve hře 5 */ 7 public Collection<AAction> getallactions() 8 { 9 return AAction.getAllActions(); 10 } Strana 224 z 351

225 Kapitola 11: Vytvoření prvních definic povinných akcí 225 Doplnění těla metody getallactions() ve třídě správce akcí Ve třídě hry jsme se sice problému s nedefinovanou metodou zbavily, ale pouze tak, že jsme nutnost definovat tuto metodu delegovali na správce akcí. Pojďme se tedy podívat, jak bychom ji mohli vyřešit tam. Začněme modifikátory. Správce akcí je objektem třídy, takže metoda by měla být definována jako statická. Metoda má navíc sloužit pouze pro vnitřní potřeby aplikace, takže není důvod ji zbytečně zveřejňovat. Její viditelnost by proto měla být nastavena na package private. Přejděme nyní k její definici. Správce akcí (objekt třídy AAction) nemá definovanou kolekci či jiný kontejner, v němž by si pamatoval definované akce. Má však mapu mapující název akce na objekt zodpovědný za zpracování příkazu odpovídajícího dané akci. Součástí dvojic uložených v mapě jsou proto i objekty akcí. Můžeme tedy o ně mapu požádat. Mapa je na takovéto žádosti připravena a definuje pro ně metodu values(), která vrátí kolekci všech hodnot, kterými jsou v tomto případě naše příkazy. Tato kolekce je ale pouze zvláštním způsobem pohledu na mapu. Jakmile změníme něco v mapě, změní se i obsah této kolekce a naopak, jakmile z kolekce nějakou hodnotu odebereme, odebere se i příslušná dvojice v mapě. (Přidávat do kolekce nemůžeme, protože kolekce neumožňuje zadat k přidávané hodnotě také její klíč.) Chceme-li zabránit tomu, aby nějaký záludný program naši mapu změnil, musíme tuto kolekci opět zabalit a vrátit jejího neměnitelného náhradníka. Takže teď už byste měli vědět dost na to, abyste dokázali takovouto metodu sami definovat. Takže: 20. Definujte ve třídě AAction statickou metodu getallactions() navrženou podle předchozího popisu. Definici této metody ve vzorové aplikaci si můžete prohlédnout ve výpisu Výpis 11.6: Definice metody getallactions() ve třídě AAction 1 /*************************************************************************** 2 * Vrátí kolekci všech akcí použitelných ve hře. 3 * 4 Kolekce všech akcí použitelných ve hře 5 */ 6 static Collection<AAction> getallactions() 7 { 8 Collection<AAction> collection, result; 9 collection = NAME_2_ACTION.values(); 10 result = Collections.unmodifiableCollection(collection); 11 return result; 12 } Strana 225 z 351

226 Kapitola 11: Vytvoření prvních definic povinných akcí 226 Dokončení definice metody execute(string...) Kolekci objektů akcí tedy umíme získat, takže zbývá definovat druhou část těla metody, kterou je vypsání názvů a popisů všech těchto akcí. Tohoto postupného vypsání lze dosáhnout buď použitím cyklu, nebo použitím datovodu. Protože se při tvorbě této aplikace máte naučit moderní techniky, ukážeme si použití datovodu. Postupujte podle návodu: 21. Definujte v metodě lokální proměnnou, do níž vložíte kolekci získanou zavoláním metody getallactions(). 22. Zavoláním metody stream() získejte od této kolekce datovod. Datovod tedy máme. Nyní potřebujeme převést jeho jednotlivé prvky (akce) na textové řetězce složené z názvu dané akce následovaného odřádkováním a jejím popisem. K tomu se používá metoda deklarovaná <R> Stream<R> map(function<? super T,? extends R> mapper) Parametrem této metody je funkce realizující potřebné transformace. Metoda přebírá data ze svého datovodu, na každý přijatý objekt aplikuje transformaci zadanou v parametru a objekty obdržené jako výsledky této transformace posílá výstupním datovodem, který si před tím k tomuto účelu vytvořila. Zbývá tedy vymyslet danou transformaci. Pro tu bude nejlepší použít lambda-výraz, který svůj parametr (objekt akce) požádá o název a popis, a ty pak vhodně sloučí do výstupního řetězce. Takže teď už byste měli být dostatečně informovaní, abyste dokázali přidat do těla definované metody další akci: 23. Zavolejte metodu map(?) datovodu získaného v předchozím kroku a zařiďte, aby výsledný datovod osahoval řetězce s názvem akce následovaný znakem odřádkování a popisem dané akce. Takže nyní máme datovod řetězců s názvy a popisy jednotlivých akcí. Ty bychom nyní potřebovali složit do výsledného textového řetězce. Pro takováto skládání objektů přicházejících datovodem do nějakého výsledného objektu definují datovody metodu collect(collector). Ta obdrží ve svém parametru objekt, který ví, jak objekty skládat, a postupně mu datovodem přicházející objekty předává, aby z nich složil požadovaný výsledek. Potřebujeme tedy ještě nějaký šikovný kolektor. Jako sbírka metod na výrobu kolektorů slouží knihovní třída java.util.stream.collectors. V její nabídce najdeme i metodu joining(charsequence, CharSequence, CharSequence), která vytvoří kolektor, jenž umí z přicházejících textových řetězců složit výsledný řetězec. První parametr této metody specifikuje oddělovač jednotlivých řetězců přicházejících vstupním datovodem. Tento oddělovač vkládá mezi jednotlivé položky Strana 226 z 351

227 Kapitola 11: Vytvoření prvních definic povinných akcí 227 ve výsledném řetězci. Druhý parametr zadává text uváděný před výslednou složeninou a třetí pak text vkládaný za tuto složeninu. Po tomto výkladu můžete pokračovat v dokončení těla naší rozpracované metody: 24. Zavolejte metodu collect(collector) datovodu získaného v předchozím kroku. V parametru ji předejte kolektor získaný zavoláním výše popsané metody Collectors.joining(CharSequence, CharSequence, CharSequence). Této metodě předejte v prvním parametru (oddělovač) řetězec s dvojitým odřádkováním, aby mezi popisy jednotlivých příkazů zůstal volný řádek. Jako druhý parametr (úvodní text) jí předejte text, který si najdete ve svém chybovém scénáři a jako třetí parametr (závěrečný text) zadejte prázdný řetězec. Definici metody execute(string...) navrženou podle předchozího popisu v ukázkové aplikaci si můžete prohlédnout ve výpisu Výpis 11.7: Definice metody execute(string...) ve třídě ActionHelp 2 public String execute(string... arguments) 3 { 4 RUPApartmentGame game = RUPApartmentGame.getInstance(); 5 Collection<AAction> commands = game.getallactions(); 6 String result = commands.stream() 7.map(cmd -> cmd.getname() + "\n" + 8 cmd.getdescription()) 9.collect(Collectors.joining("\n\n", znápověda, "")); 10 return result; 11 } Proč to nechodí Když po této úpravě znovu spustíme test, zjistíme, že je zcela stejný, jako minule. Hra stále na příkaz žádající nápovědu neumí reagovat. To je ale nějak divné, že? Vždyť jsme příslušnou akci právě definovali. Podíváme se tedy, proč se k ní nehlásí. Zamysleme se nejprve nad tím, jak hra zadané příkazy provádí. 1. Testovací program zadává příkazy voláním metody executecommand(string). 2. Ta (viz výpis 8.8 na straně 163) předá požadavek správci akcí, tj. objektu třídy AAction. 3. Ten se ve stejnojmenné metodě (viz výpis 8.10 na straně 169, řádky 67 až 78) rozhoduje, zda jej již hra odstartována. Protože víme, že startovací příkaz již byl úspěšně vykonán, je zřejmé, že zpracování dostane na starost metoda executecommoncomand(command). Strana 227 z 351

228 Kapitola 11: Vytvoření prvních definic povinných akcí Ta (tamtéž, řádky 91 až 104) rozdělí příkaz na slova, a pak požádá mapu NAME_2_ACTION, aby jí dodala objekt akce, jejíž název odpovídá tomuto slovu. Když byste si na tento příkaz dali zarážku a spustili program v ladícím režimu, tak zjistíte, že mapa je stále prázdná. Jinými slovy: akci, jejíž třídu jsme před chvílí definovali, do mapy nikdo nedal. Podíváme-li se, kde se vkládají do mapy příslušné dvojice, zjistíme, že to má na starosti konstruktor instancí (ve výpisu 8.10 řádky 150 až 159, konkrétně řádek 154). Aby se tento příkaz provedl, musel by ale tento konstruktor někdo zavolat, tj. někdo by musel požádat o vytvoření instance před chvílí definované třídy a její konstruktor by svoji činnost zahájil zavoláním konstruktoru svého rodičovského podobjektu, který by příslušný záznam do mapy vložil. Doufám, že vám už začíná svítat. V naší aplikaci nikdo nepožádal o vytvoření instance akce nápovědy, takže se zákonitě nemohla dostat do mapy. Vytvoření objektu definované akce Za vše kolem akcí je zodpovědný jejich správce, kterým je objekt třídy AAction. Když jsme rozebírali jeho funkci, tak jsme v pasáži Konstruktor správce a vytvoření jednotlivých akcí na straně 161 hovořili o tom, že správce by měl jednotlivé akce vytvářet při svém vzniku, tj. ve svém konstruktoru. V něm je však prozatím pouze příkaz vytvářející mapu. Takže: 25. Do statického konstruktoru třídy AAction přidejte za inicializaci mapy příkaz pro vytvoření instance třídy ActionHelp. Ve vzorové aplikace tak konstruktor získá podobu z výpisu Výpis 11.8: Upravená definice konstruktoru třídy AAction, která nyní zahrnuje i vytvoření první akce 1 static { 2 isalive = false; 3 NAME_2_ACTION = new HashMap<>(); 4 new ActionHelp(); 5 } O nic dalšího se již nemusíme starat. Jakmile požádáme o vytvoření instance, její konstruktor zavolá rodičovský konstruktor, a ten vloží příslušnou položku do mapy. Test Po spuštění testu se již tentokrát provedou první dva kroky, tj. startovací příkaz a příkaz nápovědy, a test se zastaví až při vykonávání dalšího kroku, kterým je Strana 228 z 351

229 Kapitola 11: Vytvoření prvních definic povinných akcí 229 přesun do sousedního prostoru (viz výpis 11.9). Jdeme tedy definovat akci pro tento přesun. Výpis 11.9: Klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy ActionHelp; zpráva oznamuje neschopnost hry reagovat na příkaz Jdi Koupelna 1 Ukončení bylo způsobeno vyhozením výjimky: 2 eu.pedu.adv15p_fw.test_util.common.testexception: 3 4 ============================================================================= 5 Při testu následujícího, tj. 2. kroku: Jdi Koupelna 6 se objevila CHYBA 7 ============================================================================= 8 Začátky očekávané a obdržené zprávy nesouhlasí. 9 Čekám: «Přesunul(a) jste se do místnosti: Koupelna» 10 Přišlo: «Tento příkaz neznám.chcete-li poradit, zadejte příkaz?» 11 Ověřený počátek zprávy: Odchylka na pozici 0 14 očekávám kód 80 znak «P» 15 obdržel jsem kód 84 znak «T» 16 ============================================================================= 11.6 Definice akce pro přesun z aktuálního prostoru do zadaného sousedního prostoru Poslední test nám prozradil, že naše hra ještě neumí vykonat akci Jdi. Tak to napravíme. Tentokrát nebudeme kopírovat kostru třídy z balíčku empty_classes, protože si z pasáže Rodičovská třída na straně 223 jistě pamatujete, že jsme pak museli zanášet několik oprav. Využijeme toho, že jsme již definovali třídu ActionHelp a zkopírujeme ji takže: 26. Vytvořte kopii třídy ActionHelp. 27. Výslednou třídu přejmenujte tak, aby název naznačoval, že instance třídy realizují akci přesunu. (V ukázkové aplikaci jsem této třídě zadal název ActionMove.) 28. Upravte její konstruktor instancí tak, aby rodičovskému konstruktoru předal správný název a popis dané akce. (Podobu konstruktoru v ukázkové aplikaci si můžete prohlédnout ve výpisu ) Výpis 11.10: Upravená definice konstruktoru ve třídě ActionMove 1 /*************************************************************************** 2 * Vytvoří instanci akce pro Strana 229 z 351

230 Kapitola 11: Vytvoření prvních definic povinných akcí * přesunutí hráče z aktuálního prostoru do zadaného sousedního prostoru. 4 */ 5 ActionMove() 6 { 7 super (pjdi, 8 "Metoda přesune hráče do sousední místnosti zadané v parametru." + 9 "\nvyžaduje však, aby tato místnost byla sousedem aktuální místnosti," + 10 "\nprotože jinak přesun neprovede a bude příkaz považovat za chybný"); 11 } Nesmíme zapomenout na vytvoření instance připravované akce, které jsme jako příčinu divného chování odhalovali v pasáži Proč to nechodí na straně 227. Takže: 29. Na konec statického konstruktoru třídy AAction (viz výpis 11.8 na straně 228) přidejte příkaz pro vytvoření instance této třídy. Metoda execute(string...) Zavedení třídy pro definici přesunové akce jsme snad zvládli, takže jediné, co je nyní třeba ještě upravit, je výkonná metoda execute(string...), která má na starosti provedení zadaného příkazu. Než ji ale začneme definovat, tak: 30. Smažte tělo zkopírované metody (to reaguje na příkaz nápovědy). Existence parametru Nejprve je třeba zjistit, jestli uživatel vůbec nějaký cílový prostor zadal. To se pozná podle počtu položek pole v parametru, které musí mít alespoň dva prvky (vždy musí být alespoň jeden, protože nultým prvkem je název zadané akce). 31. Do těla metody vložte příkaz, který v případě, že pole ve vstupním parametru nemá alespoň dva prvky, vrátí odpověď naplánovanou ve scénáři pro typ kroku tsmove_wa. Je-li parametr, tj. název cílového prostoru, zadán (je to první prvek pole), je vhodné si jej zapamatovat, protože s ním budeme pracovat a je zbytečné jej pořád získávat z pole. 32. Definujte stringovou proměnnou reprezentující název cílového prostoru (v ukázkové aplikaci se jmenuje destinationname) a uložte do ní hodnotu prvního prvku zadaného pole, tj. hodnotu s indexem 1. Strana 230 z 351

231 Kapitola 11: Vytvoření prvních definic povinných akcí 231 Korektnost parametru Takže nyní víme, že cílový prostor je zadán. Potřebujeme ale zjistit, jestli je zadán korektně, tj. jestli je to jeden ze sousedních prostorů aktuálního prostoru, protože nikam jinam tento příkaz hráče přesunout nesmí. Zeptáme se proto aktuálního prostoru, zda je zadaný prostor mezi jeho sousedy. K tomu ale potřebujeme zjistit, který prostor je právě aktuální. Jak si ale možná pamatujete, to by měl vědět svět hry, v našem případě instance třídy Aparmtent. Když získáme aktuální prostor, zjistíme, že nemá definovanou metodu, jejímž zavoláním mohli jeho souseda zjistit. Umí ale vrátit kolekci svých aktuálních sousedů a interfejs INamed umí v kolekci pojmenovaných objektů najít objekt se zadaným jménem. Musíme jenom mít na paměti, že metoda vrací objekt typu Optional, který jsme rozebírali v podšeděném bloku Datový typ Optional<E> na straně 142. Zjistíme-li, že mezi sousedy aktuálního prostoru není prostor se zadaným názvem, vrátíme text naplánovaný pro tuto situaci ve scénáři. Takže už byste měli vědět, co dělat: 33. V rozpracované metodě definujte proměnnou, do které uložíte odkaz na aktuální prostor, jenž získáte od objektu reprezentujícího svět hry (v ukázkové aplikaci se jmenuje currentroom). 34. Aktuální prostor požádejte o kolekci jeho sousedů. 35. Zavolejte metodu INamed.getO(String, Collection<E>), jíž předáte název uložený v proměnné reprezentující název cílového prostoru (v ukázkové aplikaci se jmenuje destinationname) a čerstvě získanou kolekci sousedů aktuálního prostoru. 36. Definujte proměnnou, do níž vložíte objekt získaný voláním metody popsané v předchozím kroku (v ukázkové aplikaci se tato proměnná jmenuje odestination). 37. Vrátila-li metoda prázdný Optional (to zjistíte zavoláním metody ispresent()), vraťte odpověď naplánovanou ve scénáři pro krok typu tsbad_neighbor. Realizace přesunu do cílového prostoru Teď už by snad mělo být vše v pořádku a můžeme realizovat vlastní přesun a podat o něm zprávu. Jinými slovy, potřebujeme změnit aktuální prostor. Informaci o tom, který prostor je aktuální, udržuje svět hry. Měli bychom mu tedy změnu nahlásit. Chybou je, že není jak. Řešení je jednoduché: 38. Ve třídě světa hry definujte přístupovou metodu setcurrentarea(iarea), kde místo typu IArea zadáte typ svých prostorů. Strana 231 z 351

232 Kapitola 11: Vytvoření prvních definic povinných akcí V těle metody nastavte hodnotu atributu currentarea na hodnotu obdrženou v parametru. Předpokládám, že definici této metody nemusím ukazovat. Jenom bych připomněl, že metoda by neměla být public, ale pouze package private, aby ji mohly volat pouze ostatní objekty vašeho balíčku a nebylo možno aktuální prostor změnit zvenku. 40. Ve třídě akce pro přesun vložte do těla rozpracované metody příkaz, který nastaví zadaný cílový prostor jako aktuální. Připomínám, že jsme tento prostor dostali zabalený do objektu Optional, z nějž jej dostaneme pomocí metody get(). 41. Ve scénáři zjistěte požadovanou podobu odpovědi v kroku typu tsmove. 42. Na konec metody vložte příkaz, který požadovanou odpověď vrátí. Tím je definice hotova. Její podobu ve vzorové aplikaci si můžete prohlédnout ve výpisu Výpis 11.11: Definice metody execute(string...) ve třídě ActionMove 1 /*************************************************************************** 2 * Přesune hráče prostoru (místnosti) zadaného v parametru. 3 * Vyžaduje však, aby tento prostoru byl sousedem aktuálního prostoru, 4 * protože jinak přesun neprovede a bude příkaz považovat za chybný. 5 * 6 arguments Parametry příkazu 7 Text zprávy vypsané po provedeni příkazu 8 */ 10 public String execute(string... arguments) 11 { 12 if (arguments.length < 2) { 13 return Texts. znevím_kam_jít; 14 } 15 String destinationname = arguments[1]; 16 Room currentroom = Apartment.getInstance().getCurrentArea(); 17 Optional<Room> odestination = INamed.getO(destinationName, 18 currentroom.getneighbors()); 19 if (! destination.ispresent()) { 20 return Texts.zNENÍ_SOUSEDEM + destinationname; 21 } 22 Room destinationroom = odestination.get(); 23 Apartment.getInstance().setCurrentArea(destinationRoom); 24 return Texts.zPŘESUN + destinationroom.getname(); 25 } Strana 232 z 351

233 Kapitola 11: Vytvoření prvních definic povinných akcí 233 Test Když po těchto úpravách spustíme test, dozvíme se, že (viz výpis 11.12) příkaz pro přesun do sousedního prostoru prošel bez připomínek a testovací program se zasekl až při vyhodnocování následujícího kroku, kterým je zvednutí h-objektu. Výpis 11.12: Klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy ActionMove; zpráva oznamuje neschopnost hry reagovat na příkaz Vezmi Brýle 1 ============================================================================= 2 Při testu následujícího, tj. 3. kroku: Vezmi Brýle 3 se objevila CHYBA 4 ============================================================================= 5 Začátky očekávané a obdržené zprávy nesouhlasí. 6 Čekám: «Vzal(a) jste h-objekt: Brýle» 7 Přišlo: «Tento příkaz neznám.chcete-li poradit, zadejte příkaz?» 8 Ověřený počátek zprávy: 9 10 Odchylka na pozici 0 11 očekávám kód 86 znak «V» 12 obdržel jsem kód 84 znak «T» 13 ============================================================================= 11.7 Definice akce pro zvednutí h-objektu v aktuálním prostoru a jeho přemístění do batohu Předpokládám, že úvodní kroky k definici akce byste již dokázali psát za mne. Opět začneme tím, že zkopírujeme nějakou existující třídu. Jenom je by bylo dobré si nejprve rozmyslet, která z existujících tříd je pro náš účel nejvhodnější. Doporučuji zvolit tu poslední, protože bychom z ní mohli převzít více, než z těch ostatních. Takže: 43. Vytvořte kopii třídy ActionMove. 44. Výslednou třídu přejmenujte tak, aby název naznačoval, že instance třídy realizují akci zvedání h-objektu. (V ukázkové aplikaci jsem této třídě zadal název ActionPickUp.) 45. Upravte její konstruktor instancí tak, aby rodičovskému konstruktoru předal správný název a popis dané akce. (Podobu konstruktoru v ukázkové aplikaci si můžete prohlédnout ve výpisu ) 46. Na konec statického konstruktoru třídy AAction (viz výpis 11.8 na straně 228) přidejte příkaz pro vytvoření instance této třídy. Strana 233 z 351

234 Kapitola 11: Vytvoření prvních definic povinných akcí 234 Výpis 11.13: Definice konstruktoru instancí třídy ActionPickUp 1 /*************************************************************************** 2 * Vytvoří instanci akce pro 3 * odebrání h-objektu z aktuálního prostoru a jeho vložení do batohu. 4 */ 5 ActionPickUp() 6 { 7 super (Texts.pVEZMI, 8 "Přesune zadaný h-objekt z aktuálního prostoru do batohu.\\n" 9 + "Přesouvá přitom pouze přenositelné h-objekty."); 10 } Metoda execute(string...) Opět nám zbývá definovat tělo metody execute(string...). Při definici třídy, jejíž instance realizují přesun, jsme začali tím, že jsme smazali zkopírované tělo této metody. Tentokrát by se nám z něj ale mohlo něco hodit, takže budeme odmazávat jenom nepotřebné části. (Připomínám, že definice této metody v ukázkové aplikaci je ve výpisu na straně 232.) Existence parametru Nejdříve bychom se měli (stejně jako minule) podívat, jestli uživatel nezapomněl zadat parametry příkazu, tj. název h-objektu, který mám přemístit. To už jsme dělali minule, takže stačí akceptovat první příkaz. (Ve výpisu je na řádcích 23 až 25). Nicméně na jednu změnu nesmíme zapomenout, a to: 47. Změňte odpověď vracenou v případě opomenutého zadání parametru za tu, která je v chybovém scénáři definována v kroku typu tspick_up_wa. Název zvedaného h-objektu Ve zkopírované metodě se pak ukládá název cílového prostoru do proměnné (ve výpisu je tento příkaz na řádku 26). To se nám tu hodí, jenom se bude jednat o název zvedaného h-objektu. Abychom se v budoucnu neuváděli do zmatků, tak: 48. Přejmenujte proměnnou, do níž se ukládá název zvedaného h-objektu, tak, aby její identifikátor vhodně reprezentoval její obsah. (V ukázkové aplikaci je pojmenována itemname.) Korektnost parametru S testem korektnosti zadaného parametru to bude tentokrát poněkud těžší, protože musíme zjistit nejenom to, že se daný h-objekt nachází v aktuálním prostoru, ale také to, že je přenositelný. Přitom příčin jeho nepřenositelnosti může být více: Může se jednat o principiálně nepřenositelný h-objekt (např. budovu). Strana 234 z 351

235 Kapitola 11: Vytvoření prvních definic povinných akcí 235 Může se jednat o h-objekt, který je sice teoreticky přenositelný, ale do batohu se nám již nevejde. Budeme tedy muset postupně otestovat obě dvě. Abychom ale mohli testovat, musíme nejprve získat odkaz na aktuální prostor a od něj pak kolekci jeho h-objektů. Z té se pak pokusíme získat požadovaný h-objekt, a pokud se nám to podaří, můžeme začít přemýšlet o tom, jak batoh požádat, aby h-objekt převzal. Přítomnost h-objektu v aktuálním prostoru Získání aktuálního prostoru (ve výpisu je tento příkaz na řádku 27)můžeme převzít. Následný příkaz pokoušející se získat objekt cílového prostoru však budeme muset upravit, aby se pokusil získat objekt požadovaného h-objektu. Proto v tomto příkazu: 49. zaměňte typový parametr typu Optional za typ vašich h-objektů (v ukázkové aplikaci se zaměňuje typ Room za Item), 50. přejmenujte cílovou proměnnou tak, aby její název představoval zvedaný h-objekt (v ukázkové aplikaci je přejmenována z odestination na oitem), 51. v druhém parametru předávaném metodě INamed.getO(?) zaměňte metodu getneighbors() určenou k získání sousedů, za metodu getitems() určenou k získání h-objektů v prostoru. Změnili jsme test, musíme ještě změnit zprávu vracenou v případě, že h-objekt se zadaným názvem nebude v obdržené kolekci: 52. Text vracený příkazem reagujícím na nepřítomnost h-objektu se zadaným názvem (ve výpisu je tento příkaz na řádcích 30 až 32) upravte tak, aby odpovídal požadavkům testovacího kroku typu tsbad_item z chybového scénáře vašeho správce. 53. Příkaz, který v původní verzi ukládal získaného souseda (ve výpisu je tento příkaz na řádku 33), změňte tak, aby ukládal získaný h-objekt, tj. vlevo od rovnítka vhodně změňte typ a název cílové proměnné. Zbylé příkazy ze zkopírované metody (ve výpisu jsou na řádcích 34 a 35) jsou pro naše účely neupravitelné takže: 54. Zbylé dva příkazy původní metody smažte. Přenositelnost požadovaného h-objektu Nyní tedy máme objekt reprezentující h-objekt, který je potřeba přemístit z prostoru do batohu. Podle požadavků zadání víme, že některé h-objekty musejí být nepřenositelné. Než se tedy začneme pokoušet o jejich přemístění, musíme nejpr- Strana 235 z 351

236 Kapitola 11: Vytvoření prvních definic povinných akcí 236 ve zjistit, zda se dá tokové h-objekt zvednout. Jak už jsme si říkali při konstrukci třídy h-objektů v podkapitole 9.5 Třída h-objektů v prostorech (Item) na straně 186, tak nepřenositelné h-objekty se poznají podle toho, že jejich váha je větší než kapacita batohu. Proto se do něj nevejdou, ani když je prázdný. Abychom se mohli prát, jestli je h-objekt přenositelný, potřebujeme nejenom dotyčný h-objekt (ten už máme), ale také batoh, abychom se ho mohli zeptat na jeho kapacitu. Takže 55. Definujte proměnnou reprezentující batoh a inicializujte ji. 56. Přidejte podmíněný příkaz testující přenositelnost zadaného h-objektu, tj. příkaz, v jehož podmínce se budete ptát, zda je váha h-objektu větší než kapacita batohu. 57. Nebude-li zadaný h-objekt přenositelný, vraťte text odpovědi požadovaný ve scénáři krokem typu tsunmovable. Změna obsahu kolekce h-objektů v prostoru Pokud program prošel všechny předchozí příkazy, tak víme, že máme k dispozici přenositelný h-objekt. Teď se jej musíme pokusit přemístit z prostoru do batohu. To se může, ale také nemusí podařit. Je-li batoh plný, tak by měl převzetí h-objektu odmítnout. To ale budeme řešit až za chvíli. Teď se pojďme soustředit na realizace úspěšného přesunu. Protože si prostor i batoh uchovávají své h-objekty v kolekcích, budeme muset změnit obsah těchto kolekcí. Z výkladu základů OOP ale víme, že není vhodné, abychom se hrabali v cizích kolekcích (dáváme si přece práci s tím, aby se to nenechavcům žádajícím naše kolekce nemohlo podařit), budeme muset změny obsahu těchto kolekcí definovat tak, že do obou tříd přidáme potřebné metody. Do třídy prostoru přidáme metodu, která h-objekt z kolekce odebere, a do třídy batohu metodu, která se jej pokusí přidat (pouze pokusí, protože do plného batohu se už nic přidat nedá). Začneme s třídou prostoru, protož definice její bude prostá tato metoda pouze deleguje požadavek na svoji kolekci. Protože však má, na rozdíl od žadatele, ke své kolekci plný přístup, tak jí nic nebrání obsah kolekce změnit. Proto: 58. Definujte ve třídě prostorů instanční metodu se signaturou void removeitem(iitem) kde za typ IItem dosadíte typ svých h-objektů. Tato metoda odebere zadaný h-objekt z kolekce h-objektů. Bude předpokládat, že volající program vše potřebné zkontroloval, tak již nemusí sama nic kontrolovat. Strana 236 z 351

237 Kapitola 11: Vytvoření prvních definic povinných akcí 237 Definice metody je sice trapně jednoduchá, ale protože znám své pappenheimské, tak si ve výpisu můžete prohlédnout její definici v ukázkové aplikaci. Výpis 11.14: Definice metody removeitem(item) ve třídě Room 1 /*************************************************************************** 2 * Odebere zadaný h-objekt ze své kolekce h-objektů. 3 * 4 item Odebíraný h-objekt 5 */ 6 void removeitem(item item) 7 { 8 items.remove(item); 9 } Úpravy definice třídy batohu Přesuneme se nyní do třídy batohu, v níž bychom měli definovat metodu pro přidání h-objektu. Ta ale bude trochu složitější, protože víme, že se vložení h-objektu do batohu nemusí podařit, a metoda se musí rozhodnout, zda nabízený h-objekt akceptuje či nikoliv. Jinými slovy metoda musí umět přidat h-objekt do kolekce své instance, ale před tím ještě musí prověřit, zda se tam vůbec vejde. Název definované metody by měl odrážet možnost nepřijetí h-objektu a metoda by měla navíc vracet informaci o tom, jestli nabídnutý h-objekt akceptovala. Proto: 59. Definujte metodu se signaturou boolean tryadditem(iitem) v níž místo typu IItem dosadíte typ vašich h-objektů. 60. Definujte atribut, v němž si budete pamatovat, kolik se toho ještě do batohu vejde. (V ukázkové aplikaci se jmenuje remains.) 61. V inicializační metodě batohu tento atribut inicializujte. Startuje-li hra s prázdným batohem, bude mít na počátku hodnotu rovnu kapacitě batohu, startuje-li hra s neprázdným batohem, musíte od kapacity odečíst váhy h-objektů v batohu. 62. V těle metody prověřte, zda se h-objekt zadaný v parametru do batohu ještě vejde (tj. je-li jeho váha menší nebo rovna zbývajícímu prostoru v batohu). Nevejde-li se, vraťte false, vejde-li se, přidejte jej do kolekce, zmenšete příslušně velikost zbývající kapacity a vraťte true. Definici metody použitou v ukázkové aplikaci si můžete pro inspiraci prohlédnout ve výpisu Strana 237 z 351

238 Kapitola 11: Vytvoření prvních definic povinných akcí 238 Výpis 11.15: Definice metody tryadditem(item) ve třídě Hands 1 /*************************************************************************** 2 * Vejde-li se zadaný h-objekt do batohu, přidá jej; 3 * poté vrátí zprávu o výsledku. 4 * 5 item H-objekt, který se má přidat do batohu 6 Zpráva o výsledku: {@code true} = byl přidán, 7 * {@code false} = nebyl přidán 8 */ 9 boolean tryadditem(item item) 10 { 11 if (item.getweight() > remains) { 12 return false; 13 } 14 items.add(item); 15 remains -= item.getweight(); 16 return true; 17 } Dokončení definice metody execute(string...) Vše potřebné již máme připraveno, takže můžeme dokončit definici těla metody execute(string...). Musíme jenom myslet na to, že přidávání nemusí vyjít, takže bychom se měli chovat jinak než v normálním světě: měli bychom se nejprve pokusit přidat h-objekt do batohu, a teprve když to vyjde, tak jej odebrat z aktuálního prostoru. Takže se přesuňte do třídy akce pro zvedání h-objektu a: 63. Přidejte na konec dosavadního těla příkaz deklarující logickou proměnnou, kterou inicializujete hodnotou vracenou voláním metody tryadditem(iitem) instance batohu. 64. Přidejte podmíněný příkaz, který v případě, že se h-objekt již do batohu nevejde, vrátí zprávu odpovídající ve scénáři kroku typu tsbag_full. 65. Do druhé větve podmíněného příkazu pak přidejte příkaz k odebrání zadaného h-objektu z kolekce a vrácení odpovědi odpovídající ve scénářích požadavkům kroků typu tspick_up. Definici této metody v ukázkové aplikaci si můžete prohlédnout ve výpisu Výpis 11.16: Definice metody execute(string...) ve třídě ActionPickUp 1 /*************************************************************************** 2 * Odebere h-objekt zadaný v parametru z aktuálního prostoru (místnosti) 3 * a vloží jej do batohu. 4 * Vyžaduje však, aby se h-objekt se zadaným názvem<br> 5 * a) nacházel v aktuálním prostoru,<br> Strana 238 z 351

239 Kapitola 11: Vytvoření prvních definic povinných akcí * b) byl zvednutelný a<br> 7 * c) vešel se do batohu.<br> 8 * Jinak přesun neprovede a bude příkaz považovat za chybný. 9 * 10 arguments Parametry příkazu 11 Text zprávy vypsané po provedeni příkazu 12 */ 14 public String execute(string... arguments) 15 { 16 if (arguments.length < 2) { 17 return Texts.zNEVÍM_CO_VZÍT; 18 } 19 String itemname = arguments[1]; 20 Room currentroom = Apartment.getInstance().getCurrentArea(); 21 Optional<Item> oitem = INamed.getO(itemName, currentroom.getitems()); 22 if (! oitem.ispresent()) { 23 return Texts.zNENÍ_PŘÍTOMEN + itemname; 24 } 25 Item item = oitem.get(); 26 Hands bag = Hands.getInstance(); 27 if (item.getweight() >= bag.getcapacity()) { 28 return Texts.zTĚŽKÝ_H-OBJEKT + itemname; 29 } 30 boolean added = bag.tryadditem(item); 31 if (added) { 32 currentroom.removeitem(item); 33 return Texts.zZVEDNUTO + itemname; 34 } 35 else { 36 return Texts.zBATOH_PLNÝ; 37 } 38 } Test Po spuštění testu zjistíme, že se právě definovaná akce úspěšně zhostila svého úkolu a test se zarazil až na příkazu pro položení h-objektu viz výpis Při jeho definici vám ale budu chtít ukázat další úpravy architektury, takže mu věnujeme samostatnou kapitolu. Výpis 11.17: Klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy ActionPickUp; zpráva oznamuje neschopnost hry reagovat na příkaz Polož Brýle 1 ============================================================================= 2 Při testu následujícího, tj. 4. kroku: Polož Brýle 3 se objevila CHYBA 4 ============================================================================= 5 Začátky očekávané a obdržené zprávy nesouhlasí. Strana 239 z 351

240 Kapitola 11: Vytvoření prvních definic povinných akcí Čekám: «Položil(a) jste h-objekt: Brýle» 7 Přišlo: «Tento příkaz neznám.chcete-li poradit, zadejte příkaz?» 8 Ověřený počátek zprávy: 9 10 Odchylka na pozici 0 11 očekávám kód 80 znak «P» 12 obdržel jsem kód 84 znak «T» 13 ============================================================================= 11.8 Co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Zopakujme si, co jsme v této kapitole dozvěděli a co jsme naprogramovali: Ke stavu vývoje projektu, který jsme shrnuli v podkapitole 10.8 Co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali na straně 215, jsme přidali následující: Definovali jsme třídu, jejíž instance je zodpovědná za správnou reakci na příkaz k vypsání nápovědy (v ukázkové aplikaci se jmenuje ActionHelp). Definovali jsme třídu ActionMove, jejíž instance je zodpovědná za správnou reakci na příkaz přesunu do sousedního prostoru (v ukázkové aplikaci se jmenuje ActionMove). Definovali jsme třídu ActionPickUp, jejíž instance je zodpovědná za správnou reakci na příkaz zvednutí h-objektu v aktuálním prostoru a jeho přesun do batohu (v ukázkové aplikaci se jmenuje ActionPickUp). Program, jehož zdrojové kódy odpovídají výše popsanému stavu vývoje aplikace, najdete v projektu A111z_FirstRequiredActions. Jeho diagram tříd si můžete prohlédnout na obrázku Strana 240 z 351

241 Kapitola 11: Vytvoření prvních definic povinných akcí 241 Obrázek 11.1 Diagram tříd projektu A111z_FirstRequiredActions zachycujícího stav po definici tříd reprezentujících akce nápovědy, přechodu do sousedního prostoru a zvednutí h-objektu Strana 241 z 351

242 Kapitola 12: Definice společného rodiče kontejnerů h-objektů Definice společného rodiče kontejnerů h-objektů Kapitola 12 Definice společného rodiče kontejnerů h-objektů Co se v kapitole naučíte V této kapitole začneme definovat akci pro zvednutí h-objektu a při její definici si ukážeme, že by se nám mohlo hodit zavedení společného rodiče pro kontejnery h-objektů. V závěru kapitoly doplníme akci pro předčasné ukončení hry a tím úspěšně završíme test doplněného scénáře pro prověření implementace povinných akcí. V této kapitole budeme pokračovat v práci s projektem obsahujícím závěrečnou podobu projektu, k níž jsme se dopracovali v předchozí kapitole, tj. s projektem A111z_FirstRequiredActions Definice akce pro položení h-objektu Definice akce pro položení h-objektu bude velmi podobná definici akce pro jeho zvednutí. 1. Zkopírujte třídu akce (nejlépe třídu pro akce pro zvedání h-objektů) a vytvořenou kopii vhodně pojmenujte (v ukázkové aplikaci jsem ji pojmenoval ActionPutDown). 2. Upravte její konstruktor tak, aby předával správné hodnoty parametrů konstruktoru rodičovského podobjektu. Zdrojový kód odpovídajícího konstruktoru v ukázkové aplikaci si můžete prohlédnout ve výpisu Strana 242 z 351

243 Kapitola 12: Definice společného rodiče kontejnerů h-objektů Na konec statického konstruktoru třídy AAction (viz výpis 11.8 na straně 228) přidejte příkaz pro vytvoření instance této třídy. Výpis 12.1: Definice konstruktoru instancí třídy ActionPutDown 1 /*************************************************************************** 2 * Vytvoří instanci akce pro 3 * odebrání h-objektu z batohu a jeho vložení do aktuálního prostoru. 4 */ 5 ActionPutDown() 6 { 7 super (Texts.pPOLOŽ, 8 "Přesune zadaný h-objekt z batohu do aktuálního prostoru."); 9 } 12.2 Úvahy o výhodnosti společného předka Jak jistě odhadnete, jedná se vlastně pouze o drobně upravenou podobu předchozí akce, v níž se změní zdroj a cíl přesunu h-objektu. I tentokrát bychom měli definovat potřebné metody ve třídách Room a Hands a poté doplnit definici metody execute(string...). Definice je ale přece jen o něco jednodušší, protože při ukládání h-objektu do prostoru nemusíme testovat, zda se tam h-objekt vejde. Je tedy zřejmé, že bychom měli v každé ze tříd Room a Hands definovat ekvivalenty metod, které jsme při definici předchozího příkazu definovali v jejich protějšku. Když se ale podíváte do diagramu tříd interfejsů, které tyto třídy implementují (viz obrázek 7.1 na straně 144), zjistíte, že tyto interfejsy mají společného rodiče, kterým je interfejs IItemContainer. Mohlo by nás proto napadnout, jestli bychom neměli těmto třídám také definovat společného třídního předka, do nějž bychom vytkli vše, co mají obě třídy společné. Pojďme zjistit, nakolik by to bylo výhodné. Při úvahách o výhodnosti definice společného předka musíme vždy přemýšlet nejen nad tím, kolik implementačních detailů do něj budeme moci vytknout a ušetřit si tak opakování jejich definic, ale také nad tím, jestli při vší té snaze o zefektivnění implementace nenabouráváme některý ze základních principů OOP, mezi nimi pak především LSP (měli byste jej znát ze základního kurzu, ti zapomnětliví najdou stručný popis v podšeděném bloku LSP (Liskov Substitution Principle) Substituční princip Liskové). Strana 243 z 351

244 Kapitola 12: Definice společného rodiče kontejnerů h-objektů 244 LSP (Liskov Substitution Principle) Substituční princip Liskové Mezi nejdůležitější zásady, kterými bychom se měli při návrhu svých programů řídit, patří substituční princip definovaný Barbarou Liskov v roce 1987 na konferenci OOPSLA 26 a pojmenovaný podle své autorky jako Substituční princip Liskové (Liskov Substitution Principle), přičemž v textu se pro něj většinou používá zkratka LSP. Tato zásada požaduje, aby instance potomka byly kdykoliv schopny plnohodnotně vystupovat v roli instance kteréhokoliv ze svých předků. Tuto zásadu bychom mohli formulovat i tak, že dceřiný datový typ zavádíme proto, aby definoval instance rodiče s nějakými specifickými vlastnostmi, které ostatní instance rodičovského typu mít nemusejí. Stále však musí platit, že množina instancí dceřiného datového je pouhou podmnožinou množiny instancí rodičovského typu. Se špatnými návrhy dědění ignorujícími výše popsaná pravidla se setkáte v řadě učebnic a kurzů včetně univerzitních. Ukázkově nevhodný návrh pak předvedl jeden lektor, který vysvětloval výhody dědění následovně: Představte si, že máte cyklistu, který umí jezdit na kole. Přijede k potoku s řadou kamenů, po nichž by jej mohl přeskákat. Cyklista to ale neumí. Víme ale, že to umí žába. Definujeme proto cyklistu jako potomka žáby, čímž zdědíme schopnost přeskákat potok po kamenech. Cyklista jej přeskáče a může pokračovat dál. Uvědomíte-li si, že potomek je vždy pouze speciálním případem předka, je vám jasné, že příslušný lektor nenaprogramoval cyklistu, který umí skákat po kamenech, ale žábu, která umí jezdit na kole. Se schopností skákat po kamenech cyklista automaticky zdědil i schopnost plodit pulce, pořádat večerní žabí koncerty a řadu užitečných schopností. Kdykoliv tedy uvažujeme o zavedení dědění a rozhodujeme si, zda je vhodné definovat pro skupinu tříd společného potomka, neměli bychom své úvahy omezit pouze na to, jaký kód budeme moci sdílet a nakolik zefektivníme implementaci, ale měli bychom mít na paměti i další zásady moderního programování a mezi nimi pak určitě LSP, tj. vždy si ujasnit, že mezi navrhovaným rodičovským a dceřiným typem platí vztah obecnějšího a speciálního. Společného předka skupiny tříd jsme při vývoji naší aplikace již definovali, a to dokonce dvakrát: 26 Liskov, B Keynote address data abstraction and hierarchy. In Addendum To the Proceedings on Object-Oriented Programming Systems, Languages and Applications (Addendum) (Orlando, Florida, United States, October 04 08, 1987). L. Power and Z. Weiss, Eds. OOPSLA '87. ACM, New York, NY, DOI= Strana 244 z 351

245 Kapitola 12: Definice společného rodiče kontejnerů h-objektů 245 Společným předkem všech akcí je třída AAction, které její potomci předávají své jméno a popis, a od té chvíle se o ně nemusejí starat. Toto dědění ale LSP nenarušuje. Instance třídy AAction totiž představuje obecnou akci a instance jejích potomků jsou pak pouze nějaké speciální akce. Společným předkem všech pojmenovaných objektů (a mezi nimi i akcí) je třída ANamed. Její instance vystupují v roli rodičovských podobjektů starajících se o práci s názvem instance potomka. Ani toto dědění LSP nenarušuje, protože potomci této třídy jsou pouze speciální druhy pojmenovaných objektů akce, hra, prostory, h-objekty. Více předků prostoru Vrátil bych se ale znovu k diagramu tříd z obrázku 7.1 na straně 144. Když jsme v pasáži Kontejnery h-objektů interfejs IItemContainer na straně 142 uvažovali o tom, že jak batoh, tak prostory jsou vlastně speciální případy obecnějšího kontejneru h-objektů, nevadilo nám, že interfejs IArea má dva předky. Interfejsy mají více předků povoleno. Problém ale vznikne v situaci, kdy bychom potřebovali totéž zopakovat i s třídami, protože třída nemůže mít (alespoň v Javě a ve většině ostatních rozšířených objektově orientovaných jazyků 27 ) více bezprostředních předků. Tady bych zdůraznil slovíčko bezprostředních. I v Javě může mít třída více třídních předků, ale hierarchie dědění musí být lineární, takže jeden předek je rodič, druhý prarodič, třetí praprarodič atd. Jak uvidíme za chvíli, v některých situacích je takováto linearizace řešením. Existuje několik cest, jak se s tímto problémem vypořádat, každá má své výhody a nevýhody. Nemusí-li předek uchovávat žádná data, vyřešit násobně dědění metod definicí implicitních metod v implementovaném interfejsu. Převést větvené dědění na lineární, tj. udělat z jednoho z bezprostředních předků předka toho druhého. Neřešit společné definice prostřednictvím dědění, ale použít alternativní mechanismy např. využitím návrhového vzoru Služebník (Servant) nebo Dekorátor (Decorator). 27 Z těch opravdu rozšířených OO jazyků podporují násobné dědění pouze jazyky C++ a Python. Strana 245 z 351

246 Kapitola 12: Definice společného rodiče kontejnerů h-objektů 246 V našem případě první z uvedených možností není rozumná, protože by se nám hodilo, kdyby dotyčný předek nejenom nabízel metody pro práci s kontejnerem h-objektů, ale mohl také uchovávat onen kontejner. Druhá možnost vypadá rozumněji. Kdybychom prohlásili, že v naší hře jsou všechny kontejnery h-objektů speciálními případy pojmenovaného objekty, tak by stačilo prohlásit batoh za pojmenovaný objekt (jinými slovy pojmenovat batoh) a definovat jako společného rodiče kontejnerů h-objektů třídu, která by byla potomkem třídy ANamed. Kdybychom se vydali třetí cestou, tak bychom nejspíš definovali třídu ItemContainer, jejíž instance by byly atributy prostorů i batohu. Instance této třídy by byly takové chytré kolekce, které vědí, že uchovávají h-objekty, a nabízejí sadu metod pro práci se svým obsahem. Druhá i třetí možnost se ukazují jako přibližně stejně výhodné. Zvolme tu druhou, protože pak bude architektura naší aplikace podobnější architektuře interfejsů v balíčku game_txt Definice společného předka AItemContainer Na základě předchozích úvah jsme zvolili cestu využívající dědění (dobře, já jsem ji zvolil, ale doufám, že mi to nebudete vyčítat). Přistupme k její realizaci. Kontrolní test Nejprve bych vám ale doporučil spustit test a zkontrolovat, jak daleko je současná podoba programu schopna dojít. Plánované změny se totiž týkají změn architektury, a tam by se mohlo lehce stát, že na tom budete po úpravách hůře než před nimi. Stačí drobné přehlédnutí či opomenutí. Spustíte-li test před úpravami a po nich, můžete ověřit, že se výsledek nezměnil a že všechny provedené úpravy můžete prohlásit za čistou refaktoraci, která sice mění vnitřní uspořádání programu, ale na jeho funkci nemá vliv. Spustíte-li nyní test, měl by ohlásit chybou obdobnou chybě ve výpisu Jak vidíte, hra příkaz polož brýle akceptovala, ale chovala se, jakoby dostala příkaz zvedni brýle. Toto chování jsme ale mohli očekávat, protože akci na položení h-objektu jsme vytvořili jako kopii akce na zvednutí h-objektu, v níž jsme změnili pouze název akce a její popis. Strana 246 z 351

247 Kapitola 12: Definice společného rodiče kontejnerů h-objektů 247 Výpis 12.2: Zopakovaná klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy ActionPickUp; zpráva oznamuje neschopnost hry reagovat na příkaz Polož Brýle 1 ============================================================================= 2 Při testu následujícího, tj. 4. kroku: Polož Brýle 3 se objevila CHYBA 4 ============================================================================= 5 Začátky očekávané a obdržené zprávy nesouhlasí. 6 Čekám: «Položil(a) jste h-objekt: Brýle» 7 Přišlo: «Zadaný h-objekt v místnosti není: brýle» 8 Ověřený počátek zprávy: 9 10 Odchylka na pozici 0 11 očekávám kód 80 znak «P» 12 obdržel jsem kód 90 znak «Z» 13 ============================================================================= Vlastní definice předka Takže známa aktuální chování hry a pojďme upravit její architekturu tak, že třídě prostorů a třídě batohů definujeme společného předka. Společný předek svoji prázdnou verzi v balíčku empty_classes nemá. Nezbyde nám tedy, než jej deklarovat jako novou třídu. Takže: 4. Definujte ve své aplikaci novou třídu, která bude vystupovat jako společný předek všech tříd, jejichž instance mají na starosti správu svěřených h-objektů. V ukázkové aplikaci jsem ji pojmenoval AItemContainer, protože v ní vystupuje jako kontejner h-objektů typu Item a doporučuji vám ji pojmenovat stejně. V dalším textu budu předpokládat, že jste mne poslechli. 5. Třídu deklarujte jako abstraktní, aby od ní nebylo možno vytvořit její vlastní instance a aby jako skutečné kontejnery mohli vystupovat pouze její potomci. (Proto má název této třídy v ukázkové aplikaci na počátku to A.) 6. Protože se jedná o interní třídu vaší aplikace, co níž nikomu cizímu nic není, přidělte jí přístupová práva package private (jinými slovy: třída nebude public). 7. Upravte hlavičku třídy a deklarujte v ní, že třída je potomkem třídy ANamed. V tuto chvíli začne u profesionálních vývojových prostředí editor namítat, že máte špatně definovaný konstruktor (BlueJ začne mít námitky až při překladu). A bude mít pravdu. Právě deklarovaná rodičovská třída ANamed má totiž jediný konstruktor, a ten vyžaduje předání parametru s názvem objektu. Tomu se musíme přizpůsobit: 8. Deklarujte v konstruktoru parametr typu String, který bude určen k převzetí názvu dceřiného objektu. Doporučuji proto pro něj název name. Strana 247 z 351

248 Kapitola 12: Definice společného rodiče kontejnerů h-objektů Přidejte do těla konstruktoru volání konstruktoru rodičovského podobjektu, kterému předáte hodnotu právě deklarovaného parametru, tj. přidejte příkaz: super(name); První úprava jeho budoucích potomků: tříd prostorů a batohu Kostru třídy máme připravenu. Řekli jsme si, že právě definovaná třída bude společným předkem třídy prostorů a třídy batohu. Pojďme proto tyto třídy upravit tak, aby ji korektně deklarovaly za svého předka. 10. Upravte hlavičku třídy prostorů tak, aby za svého předka deklarovala právě definovanou třídu AItemContainer. Třída prostorů již byla deklarována jako potomek třídy ANamed, takže počítá s tím, že konstruktor rodičovského podobjektu vyžaduje název s názvem dceřiného objektu, takže by změna měla proběhnout bez problémů. Přejděme k batohu: 11. Upravte hlavičku třídy prostorů tak, aby za svého předka deklarovala třídu AItemContainer. Tady to bude horší, protože, stejně jako u třídy AItemContainer se po této změně přestane překladači líbit konstruktor. Proto: 12. Přidejte do těla konstruktoru batohu volání rodičovského konstruktoru, kterému předáte nějaký text reprezentující název batohu (připomínám, že musí být jednoslovný). Tím by měla formální přestavba skončit, a můžeme se začít zabývat tím, jak tohoto předka a jeho potomky upravit, aby to pro nás bylo co nejvýhodnější Dotváříme společného předka Nyní bychom se tedy měli začít zamýšlet nad tím, které atributy a metody bychom měli do tohoto předka vytknout. Otevřeme proto jednoho z budoucích potomků a budeme postupně procházet jeho členy a přemýšlet, zda bychom měli daný člen přesunout do třídy předka. Začneme od třídy prostorů. Úpravy odvozené od třídy prostorů 13. Otevřete svoji třídu prostorů a pojďte přemýšlet se mnou. Strana 248 z 351

249 Kapitola 12: Definice společného rodiče kontejnerů h-objektů 249 První členy této třídy se objevují až v sekci instančních konstant. Tyto konstanty bychom mohli rozdělit do dvou skupin, a to na konstanty týkající se sousedů daného prostoru a konstanty týkající se h-objektů v daném prostoru. Vytknutí konstant týkajících se h-objektů Konstanty týkající se sousedů můžeme vynechat, ale konstanty týkající se h-objektů bychom měli do společného předka přestěhovat všechny. Proto: 14. Přestěhujte všechny deklarace konstant týkající se h-objektů v prostoru do sekce instančních konstant společného předka, tj. do třídy AItemContainer. Profesionální prostředí v tu chvílí označí ve zdrojovém kódu řadu chyb v místech, kde se právě odstraněné konstanty používaly. Tím nám ale hned naznačí, na které části programu se zaměřit. V dalším textu budu počítat s tím, že máte ve svých třídách seřazené členy podle sekcí a v rámci sekcí podle abecedy. Úprava konstruktoru Budeme-li procházet zdrojovým kódem shora dolů, objevíme první chybu v konstruktoru, kde editor označil příkazy inicializující právě odstraněné konstanty viz obrázek Z toho vyplývá následující úkol: Obrázek 12.1: Chyby v konstruktoru třídy Room po odstranění konstant týkajících se h-objektů 15. Přesuňte příkazy inicializující přesunuté příkazy do konstruktoru instancí rodičovské třídy. Přidání parametru rodičovskému konstruktoru V potomku se překladač se svými námitkami vůči konstruktoru uklidní, ale v konstruktoru předka neproběhne vložení příkazů zcela bez problémů. Editor bude tvrdit, že program nezná proměnnou itemnames z pravé strany přiřazení prvního z přesunutých příkazů. A bude mít pravdu tato proměnná je parametrem konstruktoru prostoru, kterou rodičovský konstruktor opravdu nezná. Takže: Strana 249 z 351

250 Kapitola 12: Definice společného rodiče kontejnerů h-objektů Zkopírujte deklaraci druhého parametru (včetně oddělující čárky) konstruktoru prostorů do hlavičky rodičovského konstruktoru. 17. V konstruktoru prostorů předejte jeho druhý parametr jako druhý parametr konstruktoru předka. Po těchto úpravách bude mít tělo konstruktoru prostorů v ukázkové aplikaci tvar, který si můžete prohlédnout ve výpisu 12.3 a porovnat s ním svůj vlastní. Výpis 12.3: Upravená definice konstruktoru ve třídě Room 1 Room(String name, String[] neighbornames, String... itemnames) 2 { 3 super(name, itemnames); 4 this.neighbornames = neighbornames; 5 this.neighbors = new ArrayList<>(); 6 this.exportedneighbors= Collections.unmodifiableCollection(neighbors); 7 } Metoda getitems() Další chybu objevíme v metodě getitems(), v níž překladač tvrdí, že nezná objekt, jejž má metoda vracet. To je ale metoda, kterou musejí definovat všechny kontejnery h-objektů, takže: 18. Přesuňte metodu getitems() do rodičovské třídy do sekce přístupových metod instancí. Doplnění implementace interfejsu IItemContainer Po přesunu se editoru (ani překladači) nebude v rodičovské třídě líbit protože bude tvrdit, že žádný rodič tuto metodu nedeklaruje ani nedefinuje. A opět bude mít pravdu: při definici třídy AItemContainer jsme zapomněli (dobře, já jsem zapomněl) zohlednit skutečnost, že nás k její definici inspiroval interfejs IItemContainer, a že by bylo vhodné v hlavičce třídy deklarovat, že třída tento interfejs implementuje. 19. Doplňte do hlavičky třídy AItemContainer deklaraci implementace interfejsu IItemContainer. Metoda removeitem(item) Editor/překladač se opět uklidní, takže se můžeme vrátit do třídy prostorů a hledat další chyby. Zjistíme, že v sekci ostatních nesoukromých metod se editoru nelíbí tělo metody removeitem(item). Předpokládám, že již víte, co máte dělat: 20. Přesuňte metodu removeitem(item) do třídy AItemContainer. Strana 250 z 351

251 Kapitola 12: Definice společného rodiče kontejnerů h-objektů 251 Metoda initializeitems() Vrátíme se znovu do třídy prostorů a pokračujme v hledání kódu obsahujícího chyby. Další metodou, která se editoru/překladači nelíbí je soukromá metoda initializeitems(). Jakmile však tentokrát metodu smažete s tím, že ji vložíte do rodičovské třídy, objeví se chyba v předchozí metodě, tj. v metodě initialize(), která smazanou metodu volá. Příčinou jsou přístupová práva: metoda initializeitems() je soukromá, takže na ni metoda initialize() po přesunu neuvidí. Oprava je ale jednoduchá: přesunutou metodu pro potomky zviditelníte. Takže pojďme na to: 21. Přesuňte metodu initializeitems() do třídy AItemContainer, ale ne do sekce pro soukromé metody, ale do sekce pro ostatní neveřejné metody. 22. Změňte u přesunuté metody modifikátor přístupu private za protected. Po všech těchto operacích se nám třída prostorů citelně zjednodušila v ukázkové aplikaci o celých 50 řádků. To jsou mimo jiné řádky, o něž se zjednoduší i druhý potomek. Buď v něm už jsou, takže je můžete smazat, nebo v něm ještě nejsou, ale už je do něj nebudete muset přidávat. Úpravy odvozené od třídy batohu Pojďme tedy otevřít třídu batohu a podívejme se, co bychom v ní měli změnit a co bychom z ní měli převzít. První, na co při procházení zdrojového kódu třídy batohu narazíme, je statická konstanta CAPACITY. Prostory omezenou kapacitu obecně nemají 28, takže jim ji nabudeme vnucovat. Stejně tak jim nebudeme vnucovat konstantu s odkazem na jedináčka. Atributy items a exporteditems Další neprázdnou sekcí je sekce konstantních atributů instance. Zde najdeme atributy items a exporteditems, které jsme již v předchozí pasáži vytkli do předka, takže je můžeme smazat, protože je třída dědí. A proto: 23. Smažte ve třídě batohu atributy items a exporteditems. 28 Tu a tam student přijde s hrou, v níž má omezenou kapacitu i nějaký prostor, ale to je spíše výjimka. Strana 251 z 351

252 Kapitola 12: Definice společného rodiče kontejnerů h-objektů 252 Někoho by mohlo zarazit, jak může třída zdědit soukromé atributy, k nimž pak nemá přístup. Je třeba si však uvědomit, že instance potomka přebírá celý rodičovský podobjekt včetně jeho soukromých členů. To, že je nemůže přímo používat, na tom nic nemění. I tak je zdědí. Následuje atribut remains, v němž si instance ukládá zbylou volnou kapacitu. To je specifika batohu, takže její deklaraci ponecháme beze změny. Konstruktor Vstupujeme do sekce továrních metod a konstruktorů. Tovární metodu mineme bez povšimnutí, ale v konstruktoru nás editor/překladač upozorní na přiřazení hodnot atributům, jejichž deklarace jsme před chvílí smazali. My ale víme, že tyto atributy jsou součástí rodičovského podobjektu, který si je inicializuje, takže: 24. V těle konstruktoru smažte příkazy inicializující atributy items a exporteditems. Tady by mohlo někoho zarazit, jak to, že překladač neprotestuje, když rodičovský konstruktor deklaruje dva parametry a my mu předáváme pouze jeden. Tato otázka je rozebrána v podšeděném bloku Proměnný počet parametrů a jeho specifika. Strana 252 z 351

253 Kapitola 12: Definice společného rodiče kontejnerů h-objektů 253 Proměnný počet parametrů a jeho specifika Ze základního kurzu programování byste si měli pamatovat, že deklarujeme-li metodu s proměnným počtem parametry, předávají se jí tyto parametry v poli. Tuto znalost jsme již využili v konstruktoru prostorů viz výpis 9.1 na straně 182. Při volání metod s proměnným počtem parametrů si můžeme vybrat, zda v příslušném parametru předáte přímo pole, anebo na daném místě vyjmenujete prvky tohoto pole a vlastní vytvoření příslušného pole a jeho naplnění zadanými prvky necháte na překladači. V tomto okamžiku se můžete spolehnout na to, že překladač dané pole vytvoří vždy, tedy i tehdy, když žádný z oněch parametrů neuvedete. V takovém případě předá volané metodě prázdné pole. Bude prázdné, ale bude to pole, takže nehrozí žádné NullPointerException nebo něco podobného. Tato situace nastala v případě batohu v ukázkové aplikaci. Batoh je v této hře na počátku prázdný, takže při volání rodičovského konstruktoru za svým názvem žádný další parametr neuvedl. Jak jsem ale vysvětlil výše, překladač vytvořil prázdné pole, a to předal rodičovskému konstruktoru jako jeho druhý parametr. Pokud tedy někdo z vás začíná hru s batohem, v němž jsou již některé h-objekty, stačí zde při volání rodičovského konstruktoru tyto h-objekty vyjmenovat, přesněji zapsat seznam jejich názvů. Metoda getitems() Při dalším průchodem zdrojovým kódem třídy batohu přeskočíme metodu getcapacity() a zarazíme se u metody getitems(). Editor/překladač v ní sice hlásí chybu, ale ta nás nemusí vzrušovat, protože víme, že jsme tuto metodu již vložili do rodičovské třídy, takže ji zde můžeme smazat. 25. Smažte ve třídě batohu metodu getitems(). Metoda initialize() Následuje metoda initialize(), v níž je také ohlášena chyba obdobně jako u prostorů zde překladač hlásí, že nezná atribut item. Ten je ale přesunut do rodičovského objektu, kde je také inicializován. A proto: 26. V metodě initialize() nahraďte příkazy inicializující atribut item příkazem volajícím rodičovskou metodu initializeitems(). Následující příkaz, který inicializuje atribut remains samozřejmě zůstane. Strana 253 z 351

254 Kapitola 12: Definice společného rodiče kontejnerů h-objektů 254 Metoda tryadditem(item) Poslední chyba hlášená ve třídě batohu je v metodě tryadditem(item). Tato metoda je specifická pro batoh, protože před přidáním objektu nejprve kontroluje, zda se do batohu vejde. Mohli bychom ale její definici upravit tak, že bychom kontrolu váhy ponechali na batohu a do rodičovské třídy bychom delegovali pouze vlastní přidání h-objektu do kontejneru. Taková metoda se bude ve společném rodiči hodit, až budeme přidávat h-objekt do prostoru. Takže: 27. Definujte ve třídě AItemContainer chráněnou (protected) metodu additem(item), která do svého kontejneru h-objektů přidá zadaný h-objekt. 28. Upravte definici metody tryadditem(item) tak, že příkaz pro přidání h-objektu do kontejneru items nahradíte voláním zděděné metody additem(item). Přidání dalších metod Teď vám ještě poradím přidání další metody, které sice bude na první pohled vypadat zbytečně, ale mohlo by zpřehlednit jiné třídy. V definici metody execute(string...) ve třídě ActionPickUp jsme se ptali, jestli je v prostoru (= kontejneru h-objektů) h-objekt se zadaným názvem (viz výpis na straně 238, řádek 21). Jak jistě odhadnete, něco podobného nás za chvíli čeká i při definici metody pro položení h-objektu. Jenom se tentokrát budeme ptát batohu. Dopředu prozradím, že tuto operaci (zjištění, zda kontejner h-objektů obsahuje h-objekt se zadaným názvem) budeme provádět ještě několikrát. Proč si musíme pamatovat, že o h-objekt se zadaným názvem musíme žádat prostřednictvím volání metody interfejsu INamed? To by měly vědět třídy, které tento interfejs implementují. Pro nás by bylo výhodnější, kdybychom se na to mohli zeptat přímo daného kontejneru. Proto 29. Definujte ve třídě AItemContainer chráněnou (protected) metodu getoitem(string), která zjistí, zda je v kontejneru h-objekt se zadaným názvem, a pokud ano, vrátí jej zabalený v objektu typu Optional<Item>. 30. Upravte v definici metody execute(string...) ve třídě ActionPickUp příkaz, který žádá o h-objekt z aktuálního prostoru, tak, abyste v tomto příkazu použili právě definovanou metodu. Strana 254 z 351

255 Kapitola 12: Definice společného rodiče kontejnerů h-objektů 255 Shrnutí Protože jsme těch změn dělali hodně, uvádím pro jistotu výpisy nové podoby zdrojového kódu ekvivalentů v ukázkové aplikaci od všech tří tříd, které jsme upravovali. Zdrojový kód společné rodičovské třídy AItemContainer najdete ve výpisu Zdrojový kód třídy prostorů (v ukázkové aplikaci se jmenuje Room) najdete ve výpisu 12.5 na straně 258. Zdrojový kód třídy batohu (v ukázkové aplikaci se jmenuje Hands) najdete ve výpisu 12.6 na straně 260. Zdrojový kód metody execute(string...) ve třídě ActionPickUp ukazovat nebudu, protože jste v ní měnili jediný příkaz. Výpis 12.4: Definice třídy AItemContainer 1 /******************************************************************************* 2 * Instance třídy {@code AItemContainer} představují objekty 3 * vystupující jako kontejnery h-objektů. 4 * 5 Rudolf PECINOVSKÝ yy-mm-dd 7 */ 8 abstract class AItemContainer extends ANamed implements IItemContainer 9 { 10 //== CONSTANT CLASS ATTRIBUTES ================================================= 11 //== VARIABLE CLASS ATTRIBUTES ================================================= //############################################################################## 16 //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== 17 //== CLASS GETTERS AND SETTERS ================================================= 18 //== OTHER NON-PRIVATE CLASS METHODS =========================================== 19 //== PRIVATE AND AUXILIARY CLASS METHODS ======================================= //############################################################################## 24 //== CONSTANT INSTANCE ATTRIBUTES ============================================== /** Názvy h-objektů v daném prostoru na počátku hry. */ 27 private final String[] itemnames; /** Názvy h-objektů aktuálně přítomných v daném prostoru. */ 30 private final Collection<Item> items; /** Nezměnitelná kolekce h-objektů aktuálně přítomných v daném prostoru, 33 * která však průběžně mapuje obsah kolekce {@link #items}. */ Strana 255 z 351

256 Kapitola 12: Definice společného rodiče kontejnerů h-objektů private final Collection<Item> exporteditems; //== VARIABLE INSTANCE ATTRIBUTES ============================================== //############################################################################## 43 //== CONSTUCTORS AND FACTORY METHODS =========================================== /*************************************************************************** 46 * Vytvoří rodičovský podobjekt kontejneru h-objektů. 47 * 48 name Název dceřiného objektu 49 itemnames Názvy h-objektů v prostoru na počátku hry 50 */ 51 AItemContainer(String name, String... itemnames) 52 { 53 super(name); 54 this.itemnames = itemnames; 55 this.items = new ArrayList<>(); 56 this.exporteditems = Collections.unmodifiableCollection(items); 57 } //== ABSTRACT METHODS ========================================================== 62 //== INSTANCE GETTERS AND SETTERS ============================================== /*************************************************************************** 65 * Vrátí h-objekt se zadaným názvem zabalený do objektu typu 66 * {@link Optional}{@code <}{@link Item}{@code >}. 67 * 68 H-objekt se zadaným názvem zabalený do objektu typu 69 * {@link Optional}{@code <}{@link Item}{@code >} 70 */ 71 public Optional<Item> getoitem(string name) 72 { 73 return INamed.getO(name, items); 74 } /*************************************************************************** 78 * Vrátí kolekci h-objektů nacházejících se v daném prostoru. 79 * 80 Kolekce h-objektů nacházejících se v daném prostoru 81 */ 83 public Collection<Item> getitems() 84 { 85 return exporteditems; Strana 256 z 351

257 Kapitola 12: Definice společného rodiče kontejnerů h-objektů } //== OTHER NON-PRIVATE INSTANCE METHODS ======================================== /*************************************************************************** 93 * Přidá zadaný h-objekt do kontejneru. 94 * 95 item H-objekt, který se má přidat do kontejneru 96 */ 97 protected void additem(item item) 98 { 99 items.add(item); 100 } /*************************************************************************** 104 * Vyčistí kontejner a uloží do něj objekty reprezentující 105 * h-objekty vyskytující se v daném kontejneru na počátku hry. 106 */ 107 protected void initializeitems() 108 { 109 items.clear(); 110 Arrays.stream(itemNames) 111.map(Item::new) 112.forEach(items::add); 113 } /*************************************************************************** 117 * Odebere zadaný h-objekt ze své kolekce h-objektů. 118 * 119 item Odebíraný h-objekt 120 */ 121 void removeitem(item item) 122 { 123 items.remove(item); 124 } //== PRIVATE AND AUXILIARY INSTANCE METHODS ==================================== //############################################################################## 133 //== NESTED DATA TYPES ========================================================= 134 } Strana 257 z 351

258 Kapitola 12: Definice společného rodiče kontejnerů h-objektů 258 Výpis 12.5: Upravená definice třídy Room 1 /******************************************************************************* 2 * Instance třídy {@code Room} představují prostory ve hře. 3 * <p> 4 * V tomto programu jsou prostory místnosti v bytě (a lednička). 5 * 6 Rudolf PECINOVSKÝ Podzim 8 */ 9 class Room extends AItemContainer implements IArea 10 { 11 //== CONSTANT CLASS ATTRIBUTES ================================================= 12 //== VARIABLE CLASS ATTRIBUTES ================================================= //############################################################################## 17 //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== 18 //== CLASS GETTERS AND SETTERS ================================================= 19 //== OTHER NON-PRIVATE CLASS METHODS =========================================== 20 //== PRIVATE AND AUXILIARY CLASS METHODS ======================================= //############################################################################## 25 //== CONSTANT INSTANCE ATTRIBUTES ============================================== /** Názvy sousedů daného prostoru na počátku hry. */ 28 private final String[] neighbornames; /** Aktuální sousedé daného prostoru. */ 31 private final Collection<Room> neighbors; /** Nezměnitelná kolekce aktuálních sousedů daného prostoru, 34 * která však průběžně mapuje obsah kolekce {@link #neighbors}. */ 35 private final Collection<Room> exportedneighbors; //== VARIABLE INSTANCE ATTRIBUTES ============================================== //############################################################################## 44 //== CONSTUCTORS AND FACTORY METHODS =========================================== /*************************************************************************** 47 * Vytvoří nový prostor se zadaným názvem a 48 * zadanými názvy jeho počátečních sousedů a h-objektů. 49 * 50 name Název daného prostoru Strana 258 z 351

259 Kapitola 12: Definice společného rodiče kontejnerů h-objektů neighbornames Názvy sousedů prostoru na počátku hry 52 itemnames Názvy h-objektů v prostoru na počátku hry 53 */ 54 Room(String name, String[] neighbornames, String... itemnames) 55 { 56 super(name, itemnames); 57 this.neighbornames = neighbornames; 58 this.neighbors = new ArrayList<>(); 59 this.exportedneighbors= Collections.unmodifiableCollection(neighbors); 60 } //== ABSTRACT METHODS ========================================================== 65 //== INSTANCE GETTERS AND SETTERS ============================================== /*************************************************************************** 68 * Vrátí kolekci sousedů daného prostoru, tj. kolekci prostorů, 69 * do nichž je možno se z tohoto prostoru přesunout příkazem typu 70 * {@link eu.pedu.adv15p_fw.scenario.typeofstep#tsmove 71 * TypeOfStep.tsMOVE}. 72 * 73 Kolekce sousedů 74 */ 76 public Collection<Room> getneighbors() 77 { 78 return exportedneighbors; 79 } //== OTHER NON-PRIVATE INSTANCE METHODS ======================================== /*************************************************************************** 86 * Metoda inicializující daný prostor. 87 * Na základě konstruktorem zapamatovaných jmen 88 * inicializuje sousedy daného prostoru a přítomné h-objekty. 89 */ 90 void initialize() 91 { 92 initializeitems(); 93 initializeneighbors(); 94 } //== PRIVATE AND AUXILIARY INSTANCE METHODS ==================================== /*************************************************************************** 101 * Vyčistí kolekci {@link #neighbors} a uloží do ní h-objekty reprezentující 102 * prostory sousedící s daným prostorem na počátku hry. Strana 259 z 351

260 Kapitola 12: Definice společného rodiče kontejnerů h-objektů */ 104 private void initializeneighbors() 105 { 106 Apartment apartment = Apartment.getInstance(); 107 neighbors.clear(); 108 Arrays.stream(neighborNames) 109.map(apartment::getORoom) 110.map(Optional<Room>::get) 111.forEach(neighbors::add); 112 } //############################################################################## 117 //== NESTED DATA TYPES ========================================================= 118 } Výpis 12.6: Upravená definice Hands 1 /******************************************************************************* 2 * Instance třídy {@code EmptyBag} představuje úložiště, 3 * do nějž hráči ukládají h-objekty sebrané v jednotlivých prostorech, 4 * aby je mohli přenést do jiných prostorů a/nebo použít. 5 * Úložiště má konečnou kapacitu definující maximální povolený 6 * součet vah h-objektů vyskytujících se v úložišti. 7 * * <p> 8 * V této hře jsou tímto úložištěm ruce hráče s kapacitou 2. 9 * Váha h-objektu představuje počet rukou, které jsou potřeba k jeho zvednutí. 10 * 11 Rudolf PECINOVSKÝ Podzim 13 */ 14 class Hands extends AItemContainer implements IBag 15 { 16 //== CONSTANT CLASS ATTRIBUTES ================================================= /** Kapacita batohu. */ 19 static final int CAPACITY = 2; /** Jediná instance batohu ve hře. */ 22 private static final Hands SINGLETON = new Hands(); //== VARIABLE CLASS ATTRIBUTES ================================================= //############################################################################## 31 //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== 32 //== CLASS GETTERS AND SETTERS ================================================= 33 //== OTHER NON-PRIVATE CLASS METHODS =========================================== Strana 260 z 351

261 Kapitola 12: Definice společného rodiče kontejnerů h-objektů //== PRIVATE AND AUXILIARY CLASS METHODS ======================================= //############################################################################## 39 //== CONSTANT INSTANCE ATTRIBUTES ============================================== 40 //== VARIABLE INSTANCE ATTRIBUTES ============================================== /** Volná kapacit batohu. */ 43 private int remains; //############################################################################## 48 //== CONSTUCTORS AND FACTORY METHODS =========================================== /*************************************************************************** 51 * Tovární metoda vracející odkaz na jedninou existující instanci dané hry. 52 * 53 Instance dané hry 54 */ 55 static Hands getinstance() 56 { 57 return SINGLETON; 58 } /*************************************************************************** 62 */ 63 Hands() 64 { 65 super("batoh-ruce"); 66 } //== ABSTRACT METHODS ========================================================== 71 //== INSTANCE GETTERS AND SETTERS ============================================== /*************************************************************************** 74 * Vrátí kapacitu batohu, tj. maximální povolený součet vah h-objektů, 75 * které je možno současně uložit do batohu. 76 * 77 Kapacita batohu 78 */ 80 public int getcapacity() 81 { 82 return CAPACITY; 83 } Strana 261 z 351

262 Kapitola 12: Definice společného rodiče kontejnerů h-objektů //== OTHER NON-PRIVATE INSTANCE METHODS ======================================== /*************************************************************************** 90 * Metoda inicializující batoh. 91 * Protože v této hře má hráč na počátku hry prázdný batoh, 92 * stačí pouze vyčistit kolekci {@link #items}. 93 */ 94 void initialize() 95 { 96 initializeitems(); 97 remains = CAPACITY; 98 } /*************************************************************************** 102 * Vejde-li se zadaný h-objekt do batohu, přidá jej; 103 * poté vrátí zprávu o výsledku. 104 * 105 item H-objekt, který se má přidat do batohu 106 Zpráva o výsledku: {@code true} = byl přidán, 107 * {@code false} = nebyl přidán 108 */ 109 boolean tryadditem(item item) 110 { 111 if (item.getweight() > remains) { 112 return false; 113 } 114 additem(item); 115 remains -= item.getweight(); 116 return true; 117 } //== PRIVATE AND AUXILIARY INSTANCE METHODS ==================================== //############################################################################## 126 //== NESTED DATA TYPES ========================================================= 127 } Kontrolní test Když po všech výše uvedených úpravách pustíme znovu test, dostaneme stejnou odpověď, jako jsme dostali před tím, než jsme třídě prostorů a batohu začali budovat společného rodiče. Ta naznačuje, že jsme snad neudělali žádnou zásadní chybu a můžeme pokračovat dál. Strana 262 z 351

263 Kapitola 12: Definice společného rodiče kontejnerů h-objektů Pokračujeme v definici akce pro pokládání h-objektů ActionPutDown Po definici třídy kontejnerů h-objektů a úpravě definic tříd prostorů a batohu se vracíme k původnímu námětu této kapitoly, kterým je především definice akce pro položení h-objektu. V této třídě nám už zbývá pouze dotáhnout definici metody execute(string...), která je prozatím v tom stavu, v jakém jsme ji převzali od třídy ActionPickUp. Definice této akce je ale velice podobná definici akce ActionPickUp určené ke zvedání h-objektů. Jenom se prohodí zdrojový a cílový kontejner a bude tu také trochu méně kontrol. O tom jsem ale již hovořil. Přepokládám proto, že byste dokázali naprogramovat tělo této metody sami bez vodění za ručičku. Takže: 31. Definujte tělo metody execute(string...) pro akci pokládání h-objektů batohu. Zdrojový kód této metody v ukázkové aplikaci najdete ve výpisu Výpis 12.7: Definice metody execute(string...) ve třídě ActionPutDown 1 /*************************************************************************** 2 * Odebere h-objekt zadaný v parametru z batohu 3 * a vloží jej do aktuálního prostoru (místnosti). 4 * Vyžaduje však, aby se h-objekt se zadaným názvem nacházel v batohu. 5 * Jinak přesun neprovede a bude příkaz považovat za chybný. 6 * 7 arguments Parametry příkazu 8 Text zprávy vypsané po provedeni příkazu 9 */ 11 public String execute(string... arguments) 12 { 13 if (arguments.length < 2) { 14 return Texts.zNEVÍM_CO_POLOŽ; 15 } 16 String itemname = arguments[1]; 17 Hands bag = Hands.getInstance(); 18 Optional<Item> oitem = bag.getoitem(itemname); 19 if (! oitem.ispresent()) { 20 return Texts.zNENÍ_V_BATOHU + itemname; 21 } 22 Item item = oitem.get(); 23 bag.removeitem(item); 24 Room currentroom = Apartment.getInstance().getCurrentArea(); 25 currentroom.additem(item); 26 return Texts.zPOLOŽENO + item.getname(); 27 } Strana 263 z 351

264 Kapitola 12: Definice společného rodiče kontejnerů h-objektů 264 Test Jako již pravidelně, spustíme test, abychom ověřili, jestli se nám podařilo opět o kousek posunout. Test nám ohlásí: Výpis 12.8: Klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy ActionPutDown; zpráva oznamuje neschopnost hry reagovat na příkaz Konec 1 ============================================================================= 2 Při testu následujícího, tj. 5. kroku: Konec 3 se objevila CHYBA 4 ============================================================================= 5 Začátky očekávané a obdržené zprávy nesouhlasí. 6 Čekám: «Konec hry.» 7 Přišlo: «Tento příkaz neznám.chcete-li poradit, zadejte příkaz?» 8 Ověřený počátek zprávy: 9 10 Odchylka na pozici 0 11 očekávám kód 75 znak «K» 12 obdržel jsem kód 84 znak «T» 13 ============================================================================= Takže to vypadá, že se vše podařilo a test se zastavil až při pokusu o zadání dalšího příkazu, kterým je příkaz k předčasnému ukončení hry. Pojďme tedy definovat příslušnou akci Příkaz k předčasnému ukončení hry ActionExit Začátek již jistě znáte: 32. Vytvořte kopii třídy ActionHelp (tu jsem vybral proto, že je nejjednodušší, takže bude nejspíše vyžadovat nejméně úprav). 33. Výslednou třídu přejmenujte tak, aby název naznačoval, že instance třídy realizují akci předčasného ukončení hry (v ukázkové aplikaci jsem této třídě zadal název ActionExit). 34. Upravte její konstruktor instancí tak, aby rodičovskému konstruktoru předal správný název a popis dané akce. (Podobu konstruktoru v ukázkové aplikaci si můžete prohlédnout ve výpisu 12.9.) Výpis 12.9: Definice konstruktoru instancí třídy ActionExit 1 /*************************************************************************** 2 * Vytvoří instanci akce pro 3 * předčasného ukončení hry. 4 */ 5 ActionExit() Strana 264 z 351

265 Kapitola 12: Definice společného rodiče kontejnerů h-objektů { 7 super (Texts.pKONEC, 8 "Poděkuje, rozloučí se a ukončí hru."); 9 } 35. Na konec statického konstruktoru třídy AAction (viz výpis 11.8 na straně 228) přidejte příkaz pro vytvoření instance této třídy. Metoda execute(string...) Zbývá definovat metodu execute(string...). Ta by ale měla být jednoduchá. Jak naznačuje popisek, příkaz by měl ukončit hru a rozloučit se s hráčem. Stačí tedy pouze vypsat závěrečnou zprávu. Test Po spuštění pravidelného testu ověřujícího správnost doposud navržené části programu se dozvíme, že testovacímu programu se naše řešení nelíbí. Zpráva o chybě (viz výpis 12.10) nám vysvětluje, že hra i po ukončení tvrdí, že běží. Výpis 12.10: Klíčová část hlášení o průběhu testu po dokončení předběžné definice metody execute(string...) ve třídě ActionPutDown oznamující nekorektní informaci o živosti hry 1 Ukončení bylo způsobeno vyhozením výjimky: 2 java.lang.illegalstateexception: 3 Hra tvrdí, že běží, přestože byla ukončena 4 at eu.pedu.adv15p_fw.test_util.tgametests.tgamesteptester.checkaliveness( TGameStepTester.java:250) 5 at eu.pedu.adv15p_fw.test_util.tgametests.tgamesteptester.verify(zzhypertextpgm) 6 atd. Aktualizace informace o živosti hry Pokud hned netušíte, kde by mohla být chyba, tak vám připomenu, že v podkapitole 8.15 Aktualizace informace o živosti hry na straně 174 jsme do startovací metody přidávali příkaz ukládající do proměnné isalive informaci o tom, že hra běží. Když hra skončí, měli bychom tuto informaci aktualizovat a zapamatovat si, že hra už neběží. Problém je v tom, že atribut isalive, v němž je informace uložena, je soukromým atributem objektu třídy AAction, tj. objektu vystupujícího v roli správce akcí, takže k němu nemáme přístup. Možná některé z vás napadlo definovat pro něj nastavovací přístupovou metodu (setr). To ale není v této situaci optimální řešení, protože poskytuje zbytečně Strana 265 z 351

266 Kapitola 12: Definice společného rodiče kontejnerů h-objektů 266 moc svobody. Změna jeho hodnoty má smysl pouze ve dvou situacích: při startu hry a při jejím ukončení. Pojďme tedy ve třídě AAction definovat vedle existující metody startgame(string) také její protiváhu stopgame(). Její signatura se bude (vedle názvu) lišit ve dvou rysech: Protože nemusíme kontrolovat přesnou podobu zadaného příkazu, tak nebude potřebovat žádné parametry. Protože musí být volatelná z jiných tříd aplikace, tak nebude soukromá, ale nastavíme je implicitní přístup package private. Její definice je tak prostá, že se ji tu skoro stydím zveřejňovat, ale pro jistotu na vás čeká ve výpisu Výpis 12.11: Definice metody stopgame() ve třídě AAction 1 /*************************************************************************** 2 * Zabezpečí korektní ukončení hry. 3 * Zapamatuje si, že hra je již ukončena. 4 */ 5 static void stopgame() 6 { 7 isalive = false; 8 } Dokončení definice metody execute(string...) Vrátíme se tedy k metodě execute(string...) ve třídě ActionExit a abychom do jejího těla doplnili příkaz volající správcovu metodu ukončující hru (připomínám, že správcem akcí je objekt třídy AAction). Třída ActionExit je sice potomkem třídy AAction, takže bychom mohli správcovu metodu zavolat bez kvalifikace, ale obecně není považováno za slušné chovat se ke statickým metodám jako by to byly metody instanční, takže doporučuji onu nepovinnou kvalifikaci doplnit. Proto: 36. Doplňte do těla metody execute(string...) příkaz volající metodu stopgame() třídy AAction. Výsledná definice metody execute(string...) je opět trapně jednoduchá, nicméně pro úplnost uvádím ve výpisu její podobu v ukázkové aplikaci. Výpis 12.12: Definice metody execute(string...) ve třídě ActionExit 1 /*************************************************************************** 2 * Předčasně ukončí hru. 3 * 4 arguments Parametry příkazu - zde se nepoužívají 5 Text zprávy vypsané po provedeni příkazu 6 */ Strana 266 z 351

267 Kapitola 12: Definice společného rodiče kontejnerů h-objektů public String execute(string... arguments) 9 { 10 AAction.stopGame(); 11 return Texts.zKONEC; 12 } Test Po spuštění pravidelného testu ověřujícího správnost doposud navržené části programu se dozvíme, že testovací program je nyní spokojen a ukončí proto výpis zprávou o úspěšném ukončení testu (viz výpis 12.13). Výpis 12.13: Klíčová část hlášení o průběhu testu po dokončení výsledné podoby definice metody execute(string...) ve třídě ActionPutDown oznamující úspěšný průchod testem scénáře pojmenovaného REQUIRED 1 ===== Konec testu prováděných operací ===== 2 ############################################################################# 3 ##### Hra: Ukázková hra: Byt s inteligentní ledničkou 4 ##### Autor: RUP15P PECINOVSKÝ Rudolf 5 ##### Scénář: REQUIRED 6 ##### Třída hry: eu.pedu.adventure15p.rupapartmentgame 7 ##### Čas: Tue Aug 11 09:55:20 CEST ############################################################################# Podle výsledků testu program pravděpodobně neobsahuje žádné závažné chyby 12 ############################################################################# 13 ############################################################################# 12.7 Prověření programu chybovým scénářem Pokud ve scénáři testujícím chybná zadání uživatele nevyužíváte nestandardní akce, měl by nyní projít i ten. Tento scénář navíc prověří, že jste při návrhu mysleli na všechny krizové eventuality zmiňované v tomto scénáři a vyřešili je. Pojďme to tedy vyzkoušet: 37. Otevřete zdrojový kód třídy svého správce scénářů a v metodě main(string[]) upravte příkaz MANAGER.testGameByScenario(REQUIRED_STEPS_SCENARIO_NAME); na MANAGER.testGameByScenario(MISTAKE_SCENARIO_NAME); (Druhou možností je, že první podobu zakomentujete a druhou přidáte.) Strana 267 z 351

268 Kapitola 12: Definice společného rodiče kontejnerů h-objektů Upravený program spusťte. Pokud jste nekorektní příkazy zadávali ve stejném pořadí jako já v ukázkové aplikaci, a pokud jste vše programovali přibližně tak, jsem zde předváděl, bude vaše zpráva o testu podobná té z výpisu Výpis 12.14: Klíčová část hlášení o průběhu testu po přepnutí na prověření chodu aplikace prostřednictvím chybového scénáře 1 ============================================================================= 2 Při testu následujícího, tj. 2. kroku: 3 se objevila CHYBA 4 ============================================================================= 5 Začátky očekávané a obdržené zprávy nesouhlasí. 6 Čekám: «Zadal(a) jste prázdný příkaz. 7 Chcete-li poradit, zadejte příkaz?» 8 Přišlo: «Tento příkaz neznám. 9 Chcete-li poradit, zadejte příkaz?» 10 Očekávaná zpráva je delší než obdržená 11 Ověřený počátek zprávy: Odchylka na pozici 0 14 očekávám kód 90 znak «Z» 15 obdržel jsem kód 84 znak «T» 16 ============================================================================= Doplnění reakce na prázdný příkaz Zpráva nám oznamuje, že jsme špatně ošetřili zadání prázdného příkazu při běhu hry. Otevřeme proto ve třídě AAction (třída správce akcí) metodu execute(string), která rozhoduje o tom, který objekt bude pověřen provedením zadaného příkazu (viz výpis 8.10 na straně 169, řádky 67 až 78). Z její definice se dozvíme, že u hry, která neběží, řeší reakci na zadaný příkaz metoda startgame(string), a u hry, která běží, ji řeší metoda executecommoncomand(string). Ta už pouze zjistí, která akce bude reakci na zadaný příkaz řešit, a předá jí řízení. Problém je v tom, že akci reagující za běhu hry na prázdný příkaz jsme ještě nedefinovali, a proto ji správce akcí nezná. Situaci můžeme řešit dvěma způsoby: Definujeme akci reagující na prázdný příkaz. Rozhodneme se, že reakce na prázdný příkaz je tak jednoduchá, že kvůli ní není třeba definovat zvláštní akci, a že bude výhodnější ji rovnou zapracovat do této metody. Mně je sympatičtější druhá varianta, a proto se vydáme touto cestou: 39. Doplňte do metody executecommoncomand(string) ve třídě AAction reakci na situaci, kdy byl zadán prázdný příkaz. Strana 268 z 351

269 Kapitola 12: Definice společného rodiče kontejnerů h-objektů 269 Výslednou podobu této metodu v ukázkové aplikaci se můžete prohlédnout ve výpisu Výpis 12.15: Upravená definice metody executecommoncomand(string) ve třídě AAction 1 /*************************************************************************** 2 * Zjistí, jaká akce má být zadaným příkazem aktivována, 3 * a jedná-li se o známou akci, provede ji. 4 * 5 command Zadávaný příkaz 6 Odpověď hry na zadaný příkaz 7 */ 8 private static String executecommoncomand(string command) 9 { 10 if (command.isempty()) { 11 return Texts.zPRÁZDNÝ_PŘÍKAZ; //==========> 12 } 13 String[] words = command.tolowercase().split("\\s+"); 14 String acttionname = words[0]; 15 AAction action = NAME_2_ACTION.get(acttionName); 16 String answer; 17 if (action == null) { 18 answer = Texts.zNEZNÁMÝ_PŘÍKAZ; 19 } 20 else { 21 answer = action.execute(words); 22 } 23 return answer; 24 } Test Spustíme obligátní test a obdržíme zprávu, jejíž klíčovou část si můžete prohlédnout ve výpisu Výpis 12.16: Upravená definice metody executecommoncomand(string) ve třídě AAction 1 ===== Konec testu prováděných operací ===== 2 ############################################################################# 3 ##### Hra: Ukázková hra: Byt s inteligentní ledničkou 4 ##### Autor: RUP15P PECINOVSKÝ Rudolf 5 ##### Scénář: _MISTAKE_ 6 ##### Třída hry: eu.pedu.adventure15p.rupapartmentgame 7 ##### Čas: Tue Aug 11 12:36:02 CEST ############################################################################# Podle výsledků testu program pravděpodobně neobsahuje žádné závažné chyby 12 ############################################################################# 13 ############################################################################# Strana 269 z 351

270 Kapitola 12: Definice společného rodiče kontejnerů h-objektů 270 Se standardními příkazy jsme tedy hotovi a můžeme se pustit do těch nestandardních. To ale odložíme do příští kapitoly Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Zopakujme si, co jsme v této kapitole dozvěděli a co jsme naprogramovali: Ke stavu vývoje projektu, který jsme shrnuli v podkapitole 11.8 Co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali na straně 240, jsme přidali následující: Definovali jsme třídu ActionPutDown, jejíž instance je zodpovědná za správnou reakci na příkaz odebrání h-objektu z batohu a jeho přesun do aktuálního prostoru. Při úvahách o opakujících se metodách ve třídě prostorů a třídě batohů jsme rozebrali vhodnost definice společného předka, do nějž bychom společné metody vytknuli. Připomněli jsme si substituční princip Liskové označovaný zkratkou LSP. Rozebrali jsme možná řešení situace, kdy bychom potřebovali, aby třída dědila od více rodičů. Zvolili jsme cestu linearizace hierarchie dědění a definovali jsme třídu AItemContainer jako společného rodiče tříd, jejichž instance obsahují kolekce h-objektů, tj. třídy prostorů (v ukázkovém projektu třídy Room) a třídy batohu (v ukázkovém projektu třídy Hands). Upravili jsme definice tříd prostorů a batohu tak, aby deklarovaly třídu AItemContainer jako svého předka. Do třídy AItemContainer jsme vytkli metody, které by bylo jinak potřeba definovat v obou třídách jejích současných potomků v jedné pro zvedání h-objektů, v druhé pro jejich pokládání. Pouze by se v nich prohodil zdroj a cíl. Vysvětlili jsme si některá specifika používání metod s proměnným počtem parametrů. Rozchodili jsme definici třídy ActionPutDown. Definovali jsme třídu ActionExit, jejíž instance jsou zodpovědné za správnou reakci na příkaz k ukončení hry. Strana 270 z 351

271 Kapitola 12: Definice společného rodiče kontejnerů h-objektů 271 Ve třídě AAction jsme definovali metodu stopgame(), která má na starosti korektní ukončení hry. Upravili jsme třídu správce scénářů, aby prověřovala hru chybovým scénářem. Ve třídě AAction jsme v metodě executecommoncomand(string) doplnili reakci na zadání prázdného příkazu za běhu hry. Program, jehož zdrojové kódy odpovídají výše popsanému stavu vývoje aplikace, najdete v projektu A112z_RequiredActionsCompleted. Jeho diagram tříd si můžete prohlédnout na obrázku Obrázek 12.2 Diagram tříd projektu A112z_RequiredActionsCompleted, zachycujícího stav po definici tříd reprezentujících nepovinné akce Strana 271 z 351

272 Kapitola 13: Definice nestandardních akcí Definice nestandardních akcí Kapitola 13 Definice nestandardních akcí Co se v kapitole naučíte V předchozích kapitolách jsme postupně definovali třídy reprezentující jednotlivé součástí světa hry a po nich třídy, jejichž instance reprezentují jednotlivé povinné příkazy. V této kapitole se pustím do definice těch zbývajících akcí. Od této kapitoly už nebudu krok za krokem radit, co máte udělat, protože zbylé příkazy jsou ve scénářích uváděny jako nestandardní a v každé hře mohou být jiné. Lze proto očekávat, že vaše aplikace bude mít jiné požadavky, než ta moje. Předchozí tvrzení však dvakrát poruším: nejprve spolu na počátku v podkapitole 13.1 Změna testu upravíte definici hlavní metody ve třídě správce scénářů. Další doporučené kroky najdete v podkapitole 13.6 Oprava definice akce pro položení h-objektu na straně 283. Před ní totiž zjistíme, že jsme v minulých kapitolách něco nedotáhli, takže to budeme muset opravit 29. Doporučení, jak postupovat při zprovoznění vaší aplikace, pak budou pokračovat až v kapitole 15 Zprovoznění zbylých testů na straně 316. Jak už jsem naznačil, s výjimkou dvou výše zmíněných pasáží se v textu této kapitoly budu snažit ukázat spíše obecné principy, které byste mohli využít při definici nestandardních akcí ve vaší hře. Budu se proto snažit spíš ukazovat cesty, kudy se můžete vydat, narazíte-li na podobný problém. Pojďte tedy dotáhnout rozpracovanou ukázkovou hru se mnou. 29 Samozřejmě jsem vám mohl již v textu předchozích kapitol poradit, jak tuto chybu neudělat, ale snažím se psát celý text tak, aby se v něm neobjevovaly jasnozřivé myšlenky, na které byste při vývoji obdobné aplikace nepřišli sami. Strana 272 z 351

273 Kapitola 13: Definice nestandardních akcí 273 V této kapitole budeme pokračovat v práci s projektem obsahujícím závěrečnou podobu projektu, k níž jsme se dopracovali v předchozí kapitole, tj. s projektem A112z_RequiredActionsCompleted Změna testu V minulých kapitolách jsme pracovali nejprve s jednoúčelovým scénářem pro prověření funkce povinných akcí, a pak s chybovým scénářem prověřujícím jejich funkce v situacích, kdy příkazy vedoucí ke spuštění těchto akcí, zadá hráč chybně. Ti, jejichž chybové scénáře neobsahují aktivaci nestandardních akcí, by již měli mít definice všech standardních akcí kompletně prověřené. Máme-li definovat nestandardní akce a prověřit jejich funkci, musíme použít základní úspěšný scénář, protože v něm jsou definovány požadované reakce na vyvolání oněch nestandardních akcí. Vrátíme se proto k původnímu testu hry, v němž se nejprve prověřuje funkčnost hry podle úspěšného scénáře a následně pak podle chybového scénáře. 1. V metodě main(string[]) svého správce akcí zakomentujte příkaz spouštějící test podle scénáře realizujícího pouze povinné akce a odkomentujte příkaz realizující standardní test hry. Změníme-li v projektu A112z_RequiredActionsCompleted (projekt definující stav ukázkové aplikace na konci minulé kapitoly) výše uvedeným způsobem definici hlavní metody ve správci scénářů, zobrazí testovací program zprávu, jejíž klíčovou část vidíte ve výpisu Výpis 13.1: Klíčová část hlášení o průběhu testu po startu celkového testu hry v projektu A112z_RequiredActionsCompleted oznamující, že hra neumí zpracovat příkaz Otevři Lednička 1 ============================================================================= 2 Při testu následujícího, tj. 7. kroku: Otevři Lednička 3 se objevila CHYBA 4 ============================================================================= 5 Začátky očekávané a obdržené zprávy nesouhlasí. 6 Čekám: «Lednička nejde otevřít. Na ledničce leží nějaký popsaný papír.» 7 Přišlo: «Tento příkaz neznám. 8 Chcete-li poradit, zadejte příkaz?» 9 Očekávaná zpráva je delší než obdržená 10 Ověřený počátek zprávy: Odchylka na pozici 0 13 očekávám kód 76 znak «L» Strana 273 z 351

274 Kapitola 13: Definice nestandardních akcí obdržel jsem kód 84 znak «T» 15 ============================================================================= 13.2 Definice akce pro otevření ledničky Akce pro otevření ledničky je poněkud komplikovanější, protože lednička má jít otevřít pouze v případě, kdy bude něčím podložena. Musíme tedy někde uchovávat informaci o tom, zda hráč již ledničku podložil. Vzhledem k tomu, že k podložení ledničky je třeba zadat jiný příkaz, musí být daná informace dostupná oběma akcím: jedna ji nastavuje a druhá zjišťuje. Máme několik možností, jak takovou informaci uchovávat: Požadovanou informaci by mohla uchovávat sama lednička podkládající akce by ji nastavila a otevírací otestovala. Nevýhodou tohoto řešení je to, že bychom pro ledničku museli definovat zvláštní třídu, která by byla potomkem třídy Room, protože lednička vlastně představuje zvláštní druh prostoru, v němž se mohou vyskytovat h-objekty. Jeho zvláštností je, že nemá žádné sousedy a vchází se do něj nestandardním příkazem otevři lednička. Pokud by se nám nechtělo definovat pro ledničku zvláštní třídu, mohli bychom přidat další kategorii h-objektů h-objekty, které je možno otevřít. Definovali bychom pro ně zvláštní příznak např. "O" a z h-objektů v naší hře bychom sem mohli zařadit vedle ledničky třeba také botník a svým způsobem i deštník (v něm bychom ale po otevření žádné uložené h-objekty nejspíš nenašli ). Kdybychom chtěli opravdu vymyslet hru s řadou možností, mohli bychom se touto cestou vydat. Každý h-objekt by pak musel umět prozradit, jestli je možno jej otevřít a ty otevřitelné by si navíc museli pamatovat, jestli aktuálně jsou nebo nejsou otevřeny. Naším primárním cílem však není vymyslet co nejrafinovanější hru, ale naučit se vyvíjet trochu rozsáhlejší aplikace, než jsou ty, které jsme v úvodním kurzu vytvářeli doposud. Proto tuto cestu zamítneme jako zbytečně složitou. Další možností, jak vyřešit náš problém, je ukládat informace o stavu hry např. ve správci akcí. K němu mají všechny akce přístup, protože je jejich společným rodičem. Jedna akce by u něj informaci ukládala a jiná zjišťovala uloženou hodnotu. Nevýhodou tohoto řešení je, že na správce akcí nakládáme další zodpovědnost, čímž narušujeme pravidlo, že každý objekt se má starat pouze o jedinou věc. Nakládání více zodpovědností na jeden objekt může vést v Strana 274 z 351

275 Kapitola 13: Definice nestandardních akcí 275 budoucnu k chybám zapříčiněných právě přehlédnutím důsledků zanesených oprav na alternativní aktivity daného objektu. Další možností, kam ukládat příznaky o stavu hry, je správce světa hry. Ten by mohl v rámci své starosti o svět hry udržovat i některé pomocné informace. V našem příkladu by to byly např. informace o tom, je-li lednička podložena nebo má-li hráč nasazeny brýle. O této možnosti by už mělo smysl uvažovat, protože bychom na správce světa hry nenakládali zcela novou zodpovědnost, ale pouze bychom trochu rozšířili jeho zodpovědnosti stávající. Mohli bychom ale také definovat zvláštní třídu určenou jenom k tomu, aby její objekt uchovával všechny příznaky. Třída by nepotřebovala žádné instance, protože by se o vše mohl postarat objekt třídy. Já bych se přikláněl k poslednímu uvedenému řešení, protože mi připadá pro programátora nejpřehlednější. Definujme proto knihovní třídu State se statickými atributy, v nichž si bude program pamatovat důležité stavy objektů (např. to, zda je již lednička podložena), a s přístupovými metodami zjišťujícími a nastavujícím hodnoty těchto atributů. Třída i její metody by měly mít přístupová práva package private, protože nikdo jiný, kromě tříd v daném balíčku (tj. tříd realizujících chování naší hry) o nich nemusí vědět. Až se příště objeví potřeba uchovávat hodnoty nějakých dalších příznaků, budeme dopředu vědět, kde definovat příslušný atribut a jeho přístupové metody Definice třídy State Třídu State vytvoříme standardním způsobem, tj. bez kopírování předlohy z balíčku empty_classes. Bude totiž obsahovat kód, který je specifický pro danou hru, a nemá proto v balíčku empty_classes svoji předpřipravenou verzi. Definici členů této třídy nám může usnadnit to, že stačí pouze definovat potřebný atribut a o definici příslušných přístupových metod pak můžeme požádat vývojové prostředí, které je doplní za nás. Na nás zbyde už jen doplnění dokumentačních komentářů. Při zavádění nového příznaku by nás mohlo napadnout, že bychom jej měli na počátku každé hry uvést do správného počátečního stavu. Měli bychom proto ve třídě definovat metodu initialize(), která bude mít veškeré inicializace na starosti a jejíž volání přidáme mezi jednotlivé inicializační příkazy ve stejnojmenné metodě správce akcí v našem případě mezi příkazy metody AAction.initialize(). Oproti předchozím inicializačním metodám se bude lišit v tom, že bude statická, protože všechny členy, které budeme definovat, budou členy třídy. Strana 275 z 351

276 Kapitola 13: Definice nestandardních akcí 276 Kód upravené verze inicializační metody AAction.initialize() v ukázkové aplikaci si můžete prohlédnout ve výpisu Kód výchozí verze třídy State pak najdete ve výpisu Výpis 13.2: Upravená definice statické metody initialize() třídy AAction 1 /*************************************************************************** 2 * Inicializuje všechny příznaky, které udržují informace 3 * o aktuálním stavu hry a jejích součástí. 4 */ 5 static void initialize() 6 { 7 Apartment.getInstance().initialize(); 8 Hands.getInstance().initialize(); 9 State.initialize(); 10 } Výpis 13.3: Předběžné podoba definice třídy State 1 /******************************************************************************* 2 * Knihovní třída {@code State} je schránkou na nejrůznější příznaky, 3 * které si je potřeba v průběhu hry zapamatovat. 4 5 * 6 Rudolf PECINOVSKÝ Podzim 8 */ 9 public class State 10 { 11 //== CONSTANT CLASS ATTRIBUTES ================================================= 12 //== VARIABLE CLASS ATTRIBUTES ================================================= /** Příznak registrující, zda již byla lednička podložena. */ 15 private static boolean iceboxsupported; //############################################################################## 20 //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== 21 //== CLASS GETTERS AND SETTERS ================================================= /*************************************************************************** 24 * Vrátí informaci o tom, je-li lednička podložena. 25 * 26 Je-li podložena, vrátí {@code true}, jinak vrátí {@code false} 27 */ 28 static boolean isiceboxsupported() 29 { 30 return iceboxsupported; 31 } Strana 276 z 351

277 Kapitola 13: Definice nestandardních akcí /*************************************************************************** 35 * Nastaví informaci o tom, je-li lednička podložena. 36 * 37 iceboxsupported Nastavovaná hodnota 38 */ 39 static void seticeboxsupported(boolean iceboxsupported) 40 { 41 State.iceboxSupported = iceboxsupported; 42 } //== OTHER NON-PRIVATE CLASS METHODS =========================================== /*************************************************************************** 49 * Inicializuje všechny příznaky, které udržují informace 50 * o aktuálním stavu hry a jejích součástí. 51 */ 52 static void initialize() 53 { 54 iceboxsupported = false; 55 } //== PRIVATE AND AUXILIARY CLASS METHODS ======================================= //############################################################################## 64 //== CONSTANT INSTANCE ATTRIBUTES ============================================== 65 //== VARIABLE INSTANCE ATTRIBUTES ============================================== //############################################################################## 70 //== CONSTUCTORS AND FACTORY METHODS =========================================== /*************************************************************************** 73 * Soukromý konstruktor zabraňující vytvoření instance. 74 */ 75 private State() 76 { 77 } //== ABSTRACT METHODS ========================================================== 82 //== INSTANCE GETTERS AND SETTERS ============================================== 83 //== OTHER NON-PRIVATE INSTANCE METHODS ======================================== 84 //== PRIVATE AND AUXILIARY INSTANCE METHODS ==================================== 85 Strana 277 z 351

278 Kapitola 13: Definice nestandardních akcí //############################################################################## 89 //== NESTED DATA TYPES ========================================================= 90 } 13.4 Vytvoření třídy ActionOpen Vše potřebné již máme připraveno, a můžeme proto definovat třídu ActionOpen, jejíž instance bude zodpovědná za správnou reakci na příkaz pro otevření ledničky. Pro jistotu připomenu standardní začátek definice třídy akce: 1. Zkopírovat nějakou třídu, jejíž instance řeší podobnou akci (v tomto případě nejlépe třídu ActionMove, protože otevírání ledničky je zvláštní případ přesunu do jiného prostoru). 2. Přejmenovat na požadovaný název (v tomto případě na ActionOpen). 3. Upravit definici konstruktoru, aby předával rodiči správný název a popis definované akce. (Definici konstruktoru instancí třídy ActionOpen v ukázkové aplikaci si můžete prohlédnout ve výpisu 13.4.) 4. Přidat jeho volání do konstruktoru správce akcí, tj. do statického konstruktoru třídy AAction. Výpis 13.4: Definice konstruktoru instancí třídy ActionOpen 1 /*************************************************************************** 2 * Vytvoří instanci akce pro 3 * přesunutí otevření h-objektu, který je možno otevřít 4 * (po zkontrolování definovaných podmínek) 5 * a po otevření se stane aktuálním prostorem. 6 */ 7 ActionOpen() 8 { 9 super (Texts.pOTEVŘI, 10 "Ověří, že hráč chce opravu otevřít otevřitelný h-objekt,\n" 11 + "zadaný jako parametr příkazu.\n" 12 + "Otevírá-li ledničku, tak zkontroluje, je-li podložena.\n" 13 + "Jsou-li všechny podmínky splněny," 14 + "přesune hráče do prostoru daného h-objektu.\n" 15 + "Nebudou-li podmínky splněny, bude příkaz považovat za chybný"); 16 } Strana 278 z 351

279 Kapitola 13: Definice nestandardních akcí Definice metody execute(string...) Takže formality máme za sebou a můžeme se vrhnout na to hlavní, na úpravu těla metody execute(string...), které má v současnosti podobu zobrazenou ve výpisu na straně 232 (upravovaná třída ActionOpen vznikla jako kopie třídy ActionMove, a metoda má proto zatím její verzi těla). Pojďme si je prohlédnout a zamysleme se nad tím, jak je upravit. Test existence parametru Na počátku (ve výpisu na řádcích 12 až 14) metoda prověřuje, zda bylo vůbec zadáno, kam se má přejít. V naší metodě bychom testovali, co se má otevřít. Tento test bychom měli určitě ponechat. Vracený text se nám ale nehodí, takže bychom jej měli upravit. Protože však pro definici textů používáme konstanty, měli bychom ve třídě Texts definovat novou konstantu vložíme ji na začátek sekce se zprávami definujícími reakce na zadání nestandardních příkazů (ve výpisu 5.5 na straně 100 začíná tato sekce na řádku 112). Po úpravě bude začátek sekce vypadat jako ve výpisu Výpis 13.5: Definice chybových textů ve třídě Texts definujících reakce programu na chybná zadání uživatele pokoušejícího se otevřít ledničku 1 /** Texty vypisované v reakci na příkazy vyvolávající nepovinné akce. */ 2 static final String 3 zotevíraný_objekt_nezadán = "Nebylo zadáno, co se má otevřít", 4 5 znení_otevíratelný = "Zadaný h-objekt není otevíratelný: ", 6 7 zlednice_nejde_otevřít = 8 "Lednička nejde otevřít. Na ledničce leží nějaký popsaný papír.", Nyní můžeme zaměnit původní konstantu za konstantu se zprávou oznamující nezadání h-objektu, který se má otevřít. Další příkaz ukládající název otevíraného h-objektu do lokální proměnné destinationname (ve výpisu příkaz na řádku 15) ponecháme. S tímto názvem ještě budeme potřebovat pracovat. Jenom změníme název proměnné na itemname, protože otevíráme h-objekt, který se stane prostorem až poté, co jej otevřeme. Test přítomnost otevíraného h-objektu Příkaz na řádku 16 zjišťuje aktuální prostor, aby jej pak další příkaz (řádky 17 a 18) mohl požádat o sousední prostor se zadaným názvem. My se sice na sousední prostory prát nemusíme, ale bylo by vhodné, kdybychom ověřili, že se otevíraný Strana 279 z 351

280 Kapitola 13: Definice nestandardních akcí 280 h-objekt v daném prostoru vůbec nachází. Jinak by mohl hráč otevřít ledničku třeba již z předsíně. Upravíme proto příkaz tak, abychom se aktuálního prostoru neptali na sousedy, ale na přítomnost h-objektu, který máme otevřít. Příkaz proto bude mít tvar: Optional<Item> oitem = currentroom.getoitem(itemname); Nyní se můžeme zeptat, zda se h-objekt v daném prostoru nachází, a pokud tomu tak není, vrátíme stejnou zprávu, jakou jsme vraceli, když v prostoru nebyl h-objekt, který jsme měli zvednout. Test otevíratelnosti zadaného h-objektu Dobrá, víme tedy, že se h-objekt v prostoru nachází. Měli bychom ještě ověřit, že daný h-objekt je možno otevřít, aby se nestalo, že by uživatel chtěl otevřít třeba papír. Opět vyvstává otázka, jak rozpoznat h-objekty, které je možno otevřít a opět máme několik možností: Rozšířit definici h-objektů tak, aby každý z nich věděl, jestli je možno jej otevřít. Definovat někde (např. ve třídě State) seznam h-objektů, které je možno otevřít. Najít nějakou charakteristiku, podle níž bychom to poznali hned, aniž bychom museli rozšiřovat definice stávajících tříd. Mně se opět nejvíc líbí poslední možnost. Za prvé proto, že vyžaduje minimální úpravu stávajícího kódu a za druhé proto, že k h-objektu, který je možno otevřít, musí existovat stejně pojmenovaný prostor. To platí i pro naši ledničku, která je současně h-objektem v prostoru a současně prostorem. Můžeme si tedy vyžádat od správce všech prostorů (přesněji od správce světa hry) prostor pojmenovaný stejně jako otevíraný h-objekt. Vrátí-li správce prázdný Optional, oznámíme uživateli, že zadaný prostor není možno otevřít (budeme kvůli tomu muset definovat další statickou konstantu ve třídě Texts). znení_otevíratelný = zanp + "\nzadaný h-objekt není otevíratelný: ", Dokončení definice Vše, co bylo třeba před spuštěním příkazu prověřit, jsme prověřili (alespoň si to myslíme), takže můžeme definici metody execute(string...) dokončit. Požádáme správce světa hry o to, aby nastavil požadovaný cílový prostor, a vrátíme poža- Strana 280 z 351

281 Kapitola 13: Definice nestandardních akcí 281 dovanou výstupní zprávu. Po spuštění testu se objeví zpráva, jejíž klíčovou část najdete ve výpisu Výpis 13.6: Klíčová část hlášení o průběhu testu po dokončení předběžné definice metody execute(string...) ve třídě ActionOpen oznamující špatnou reakci na zadání příkazu Otevři Lednička 1 ============================================================================= 2 Při testu následujícího, tj. 7. kroku: Otevři Lednička 3 se objevila CHYBA 4 ============================================================================= 5 Začátky očekávané a obdržené zprávy nesouhlasí. 6 Čekám: «Zadaná akce nebyla provedena. 7 Lednička nejde otevřít. Na ledničce leží nějaký popsaný papír.» 8 Přišlo: «Úspěšně jste otevřel(a) ledničku.» 9 Očekávaná zpráva je delší než obdržená 10 Ověřený počátek zprávy: Odchylka na pozici 0 13 očekávám kód 90 znak «Z» 14 obdržel jsem kód 218 znak «Ú» 15 ============================================================================= Doplnění testu podloženost Ajta! Zapomněli jsme na to, že ledničku je možno otevřít pouze poté, co ji nějakým vhodným h-objektem podložíme. Před příkaz pro vlastní přesun do požadovaného prostoru proto musíme ještě vložit test podloženosti daného h-objektu. Zavoláme proto metodu State.isIceboxSupported() a upravíme vracení zprávy podle jejího výsledku. Definici metodu upravené podle výše zmíněných zásad si můžete prohlédnout ve výpisu Výpis 13.7: Definice metody execute(string...) ve třídě ActionOpen 1 /*************************************************************************** 2 * Otevře zadaný h-objekt a přesune do něj hráče jako do nového prostoru. 3 * Před tím však zkontroluje, 4 * <ul> 5 * <li>zda je zadán h-objekt, který se má otevřít,</li> 6 * <li>zda je tento h-objekt v aktuálním prostoru,</li> 7 * <li>zda je tento h-objekt otevíratelný, tj. zda je současně prostorem,</li> 8 * <li>zda je lednička již podložena, aby šla otevřít.</li> 9 * </ul> 10 * 11 parametry Jediným povoleným parametrem je zatím lednička 12 Text zprávy vypsané po provedeni příkazu 13 */ Strana 281 z 351

282 Kapitola 13: Definice nestandardních akcí public String execute(string... arguments) 16 { 17 if (arguments.length < 2) { 18 return Texts.zNEVÍM_CO_OTEVŘÍT; 19 } 20 String itemname = arguments[1]; 21 Apartment apartment = Apartment.getInstance(); 22 Room currentroom = apartment.getcurrentarea(); 23 Optional<Item> oitem = currentroom.getoitem(itemname); 24 if (! oitem.ispresent()) { 25 return Texts.zNENÍ_PŘÍTOMEN; 26 } 27 Optional<Room> odestinationroom = apartment.getoroom(itemname); 28 if (! odestinationroom.ispresent()) { 29 return Texts.zNENÍ_OTEVÍRATELNÝ; 30 } 31 if (! State.isIceboxSupported()) { 32 return Texts.zLEDNICE_NEJDE_OTEVŘÍT; 33 } 34 apartment.setcurrentarea(odestinationroom.get()); 35 return Texts.zOTEVŘEL_LEDNIČKU; 36 } Test Krok testující právě definovanou akci po spuštění testu nyní úspěšně projde. Testovací program se zastaví až o pár korků později se zprávou, jejíž klíčovou část si můžete prohlédnout ve výpisu Tentokrát jsem zobrazil větší část výpisu, aby bylo zřejmě, kde nastala chyba. Výpis 13.8: Klíčová část hlášení o průběhu testu po doplnění definice metody execute(string...) ve třídě ActionOpen; zpráva oznamuje špatnou reakci na zadání příkazu Vezmi Papír 1 9. krok: «Polož Časopis» Očekávaný stav po provedení akce: 4 9. krok 5 Typ kroku: tsput_down 6 Příkaz: Polož Časopis 7 Prostor: Kuchyň 8 Východy: («Ložnice», «Obývák») 9 H-objekty: («Lednička», «Papír», «Časopis») 10 Batoh: («Brýle») Zpráva: 13 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 14 Položil(a) jste h-objekt: Časopis Obdržená zpráva: Strana 282 z 351

283 Kapitola 13: Definice nestandardních akcí ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 18 Položil(a) jste h-objekt: Časopis ===== Konec testu prováděných operací ===== 23 ############################################################################# 24 ##### Hra: Ukázková hra: Byt s inteligentní ledničkou 25 ##### Autor: RUP15P PECINOVSKÝ Rudolf 26 ##### Scénář: _HAPPY_ 27 ##### Třída hry: eu.pedu.adventure15p.rupapartmentgame 28 ##### Čas: Mon Aug 17 21:11:53 CEST ############################################################################# Ukončení bylo způsobeno vyhozením výjimky: 32 eu.pedu.adv15p_fw.test_util.common.testexception: ============================================================================= 35 Při testu následujícího, tj. 10. kroku: Vezmi Papír 36 se objevila CHYBA 37 ============================================================================= 38 Začátky očekávané a obdržené zprávy nesouhlasí. 39 Čekám: «Vzal(a) jste h-objekt: Papír» 40 Přišlo: «Zadaná akce nebyla provedena. 41 Zadaný h-objekt nemůžete vzít, máte už obě ruce plné» 42 Ověřený počátek zprávy: Odchylka na pozici 0 45 očekávám kód 86 znak «V» 46 obdržel jsem kód 90 znak «Z» 47 ============================================================================= 13.6 Oprava definice akce pro položení h-objektu Podíváme-li se ve výpisu 13.8 na průběh předchozích příkazů, zjistíme, že v předchozím, tj. 9. kroku jsme položili časopis (viz řádek 1), takže nemůžeme mít obě ruce plné. Prohlédnete-li si obsah batohu po provedení příkazu (viz řádek 10), tak jsou v něm opravdu pouze brýle. Někde je zřejmě chyba. Odhalování chyby krokováním programu Ti zkušenější již možná tuší, ale ukážeme si, jak takovou chybu odhalit. Jako nejlepší cestu vidím následující: Strana 283 z 351

284 Kapitola 13: Definice nestandardních akcí Otevřete zdrojový kód třídy ActionPickUp (resp. jejího ekvivalentu ve vaší aplikaci) a najděte v něm definici metody execute(string...), v níž se projevila chyba. Dosavadní definici této metody v ukázkové aplikaci najdete ve výpisu na straně 238. V ní je na řádku 30 příkaz boolean added = bag.tryadditem(item); který se pokouší vložit h-objekt do batohu. Tento příkaz zřejmě uložil do proměnné added informaci, že se vložení h-objektu nepodařilo. Proto: 3. Vložte na řádek s výše zmíněným příkazem zarážku. 4. Spusťte program v ladícím režimu (v NetBeans zadáte místo příkazu Run příkaz Debug, v BlueJ stačí program spustit). Po spuštění programu se na nastavené zarážce zastaví program poprvé v druhém kroku, když si hráč bere v koupelně brýle. Jistě si pamatujete, že zvednutí brýlí proběhlo úspěšně, takže: 5. Pusťte program dál. Podruhé se na zarážce zastaví, když chce zvednout časopis. I zde víme, že vše proběhlo v pořádku, takže opět: 6. Pusťte program dál. Potřetí se zde program zarazí, když chce hráč v kuchyni zvednout papír. Poznáte to podle toho, že objekt v proměnné item se jmenuje Papír. Tentokrát musíme zbystřit, protože na tomto místě se program zasekl. Proto: 7. Zadejte, že se chcete zanořit dovnitř volané metody tryadditem(item). První příkaz v metodě testuje, je-li váha zvedaného h-objektu větší než zbývající kapacita batohu. Podíváme se proto, kolik v batohu zbývá místa: 8. Podívejte se na hodnotu v atributu remains (v NetBeans proto budete muset v panelu Variables zobrazujícím hodnoty proměnných nejprve rozbalit položku this). Jak jistě sami vidíte, v proměnné je hodnota 0, což znamená, že se batoh domnívá, že je plný. Akce pro zvednutí h-objektu tedy postupovala správně, avšak stav programu předaný dříve volanými akcemi byl chybný. Když jsme pokládali objekt, mělo se v batohu udělat místo. Proto: 9. Zrušte nastavenou zarážku a zavřete zdrojový kód třídy akce pro zvednutí h-objektu. Strana 284 z 351

285 Kapitola 13: Definice nestandardních akcí 285 Podíváme se proto, kde se mohla chyba objevit. Před příkazem pro zvednutí papíru se vykonával příkaz pokládající časopis. Jeho výsledkem mělo být uvolnění místa v batohu. Akce pro pokládání h-objektu se tím automaticky dostane do okruhu podezřelých. Proto: 10. Otevřete zdrojový kód třídy ActionPutDown, resp. jejího ekvivalentu ve vaší aplikaci. 11. Najděte v něm definici metody execute(string...) (její stávající definici v ukázkové aplikaci najdete ve výpisu 12.7 na straně 263). V této definici bude pro nás zřejmě klíčový příkaz odebírající h-objekt z batohu (ve výpisu 12.7 je na řádku 23): bag.removeitem(item); 12. Umístěte na tento příkaz zarážku a znovu spusťte program v ladícím režimu. Hned první zastávka je ta pravá hráč se chystá položit časopis. Proto: 13. Zadejte krokování dovnitř volané metody. Tady bude možná některé z vás čekat překvapení. Nevnořili jsme se do metody batohu, ale do metody jeho rodiče obecného kontejneru objektů. (V ukázkové aplikaci třída AItemContainer její definici najdete ve výpisu 12.4 na straně 255, přičemž definice volané metody je zde na řádcích 116 až 124.) Tato definice odebere zadaný h-objekt z kolekce items. V tom problém není (kdyby byl, tak by si testovací program již dávno stěžoval na to, že mu nesedí očekávané a obdržené h-objekty v batohu). Problém je v tom, že si program stále myslí, že batoh je plný. Tato metoda se totiž vůbec nestará o to, kolik místa v kontejneru po položení objektu zbyde. To si musí vyřešit batoh, který si má svoji zbývající kapacitu hlídat. To zabezpečíme tak, že v batohu definujeme metodu, která rodičovskou metodu přebije a vedle odebrání h-objektu i příslušně upraví volnou kapacitu. Podíváte-li se ve třídě Hands na definici metody tryadditem(item), zjistíte, že tato metoda (ve výpisu 12.6 na straně 260 ji najdete na řádcích 101 až 117) předposledním příkazem remains -= item.getweight(); zmenšuje zbývající objem batohu. Metoda pro odebírání h-objektu z batohu by jej proto měla naopak zvětšit. To ale nedělá. Proto: 14. Zkopírujte do schránky definici metody removeitem(item) i s jejím dokumentačním komentářem. 15. Otevřete zdrojový kód třídy batohu. Strana 285 z 351

286 Kapitola 13: Definice nestandardních akcí Vložte metodu ze schránky do sekce nesoukromých metod instancí nejlépe podle abecedy, tj. mezi metody initialize() a tryadditem(item). 17. Upravte definici vložené metody tak, že 1. Před hlavičku přidáte oznamující překladači, že přebíjíte metodu předka. 2. V těle nejprve zavoláte rodičovskou verzi, která vyjme h-objekt z kontejneru. 3. Přidáte příkaz, který přičte váhu odebraného h-objektu ke zbývající velikosti volné kapacity. Definici této metody v ukázkové aplikaci najdete ve výpisu Výpis 13.9: Definice metody removeitem(item) ve třídě Hands 1 /*************************************************************************** 2 * Odebere zadaný h-objekt z batohu 3 * a příslušně zvětší volnou kapacitu batohu. 4 * 5 item Odebíraný h-objekt 6 */ 8 void removeitem(item item) 9 { 10 super.removeitem(item); 11 remains += item.getweight(); 12 } Test Určitě si také myslíte, že jste chybu opravili, takže můžete spustit test. Klíčovou část výpisu při spuštění ukázkové aplikace si můžete prohlédnout ve výpisu Výpis 13.10: Klíčová část hlášení o průběhu testu po dokončení definice metody removeitem(item) ve třídy Hands; zpráva oznamuje neschopnost hry reagovat na příkaz Přečti Papír 1 ============================================================================= 2 Při testu následujícího, tj. 11. kroku: Přečti Papír 3 se objevila CHYBA 4 ============================================================================= 5 Začátky očekávané a obdržené zprávy nesouhlasí. 6 Čekám: «Rozhodl(a) jste se přečíst Papír. 7 Text je psán příliš malým písmem, které je rozmazané.» 8 Přišlo: «Tento příkaz neznám. 9 Chcete-li poradit, zadejte příkaz?» 10 Očekávaná zpráva je delší než obdržená Strana 286 z 351

287 Kapitola 13: Definice nestandardních akcí Ověřený počátek zprávy: Odchylka na pozici 0 14 očekávám kód 82 znak «R» 15 obdržel jsem kód 84 znak «T» 16 ============================================================================= Oprava se zřejmě zdařila, předchozí příkaz prošel a test se zastavil až na vyhodnocení následujícího příkazu. Jdeme tedy vytvářet třídu, jejíž instance bude mít na starosti reakci na příkaz přečti xxx Definice akce pro přečtení papíru Nejprve si připomeneme, k čemu má tato akce sloužit. Při nezdařeném pokusu o otevření lednice se hráč dozví, že na lednici leží papír. Očekává se, že jej vezme a přečte, aby se dozvěděl, co dál. Má-li papír přečíst, musí jej mít v ruce (tj. v batohu ). Navíc musí mít nasazené brýle, protože jinak pro něj bude text nečitelný. Začneme standardně: 1. zkopírujeme třídu ActionOpen (definovali jsme ji z akcí jako poslední, tak si ji nejlépe pamatujeme), 2. pojmenujeme vytvořenou kopii ActionRead, 3. upravíme definici konstruktoru a 4. přidáme jeho volání do konstruktoru správce akcí, tj. do statického konstruktoru třídy AAction. Poté přejdeme na definici metody execute(string...). Vyjdeme z definice ve výpisu 13.7 na straně 281, který postupně upravíme do požadované podoby. Test existence parametru Začneme testem existence parametru (ve výpisu 13.7 na řádcích 17 až 19). Test měnit nemusíme, pouze musíme změnit vracený text. Přesuneme se proto do třídy Texts a na konec sady zpráv o nezadaných parametrech přidáme definici: znevím_co_číst = zanp + "\nnebylo zadáno, co se má přečíst", Konstantu znevím_co_číst pak zadáme jako návratovou hodnotu naší metody v případě, že nebyl zadán parametr. Strana 287 z 351

288 Kapitola 13: Definice nestandardních akcí 288 Test přítomnosti daného h-objektu v batohu Nyní bychom podle zadání měli otestovat, zda je h-objekt, který se má přečíst, přítomen batohu (tj. má-li jej hráč v ruce). Čtení jiných h-objektů hra nepovoluje. Požádáme proto batoh o odkaz na ten ze svých h-objektů, jehož název odpovídá názvu zadanému v parametru. Vrátí-li prázdný Optional, oznámíme hráči, že h-objekt, který chce číst, není v daném okamžiku v batohu. Vrátí-li zabalený odkaz na h-objekt, můžeme pokračovat. Test, zda je požadováno přečtení h-objektu, který lze opravdu přečíst Dalším požadavkem je, aby uživatel požadoval přečtení papíru. Z h-objektů dostupných v našem světě bychom teoreticky mohli povolit i čtení časopisu. Můžeme si pro zajímavost ukázat, jak zpracovat čtení obou h-objektů. Je zřejmé, že musíme nejprve zjistit, jestli chce hráč přečíst h-objekt, který se přečíst dá. To ale bohužel nestačí, protože k přečtení h-objektu musí mít hráč nasazeny brýle. Nasazení brýlí je však vhodné prověřovat až poté, kdy víme, že h-objekt lze číst, protože ve scénáři odpověď při nenasazených brýlích říká, že písmena jsou příliš malá a rozmazaná. Potřebujeme se proto rozhodnout, jak vyřešit zařazení stejné akce (upozornění na brýle) do dvou rozdílných větví (papír versus časopis). Máme několik možností: Test na nasazení brýlí přidáme do obou větví. Bude se sice opakovat stejný kód, ale nebudeme muset dvakrát testovat h-objekt určený k přečtení. Otestujeme nejprve, zda se jedné o některý z h-objektů, které lze přečíst, pak prověříme nasazení brýlí a poté vrátíme zprávu podle toho, který h-objekt máme číst. Při testu čtitelnosti h-objektu si hned zapamatujeme případnou odpověď, poté otestujeme nasazení brýlí, a pokud budou nasazeny, vrátíme předem zapamatovanou dopověď. Já bych se přikláněl k poslednímu uvedenému řešení. Test nasazených brýlí S nasazením brýlí je to obdobné, jako s podložením ledničky: musíme si zapamatovat aktuální stav, aby nevadilo, že mezi nasazením a použitím bylo vykonáno Strana 288 z 351

289 Kapitola 13: Definice nestandardních akcí 289 několik dalších akcí. Hráč si je mohl nasadit hned poté, co je našel, a pak ještě chvíli bloumat po bytě. Při úvahách o podložení ledničky jsme definovali třídu State. To je to správné místo, kam bychom nyní mohli umístit i příznak toho, zda již má hráč nasazeny brýle. Definujeme v ní proto atribut glassesputon spolu s příslušnými přístupovými metodami. Současně nezapomeneme přidat inicializaci tohoto příznaku do metody initialize(), aby na počátku příštího spuštění hry byl hráč opět bez brýlí. Výsledná podoba metody Výslednou podobu těla metody navržené podle předchozích úvah si můžete prohlédnout ve výpisu Výpis 13.11: Definice metody execute(string...) ve třídě ActionRead 1 /*************************************************************************** 2 * Prozradí hráči text napsaný na daném h-objektu. 3 * Před tím však zkontroluje, 4 * <ul> 5 * <li>zda je zadán h-objekt, který se má číst,</li> 6 * <li>zda je tento h-objekt v batohu (tj. drží-li jej hráč v ruce,</li> 7 * <li>zda je tento h-objekt čitelný (musí to být papír nebo časopis,</li> 8 * <li>zda má hráč nasazeny brýle.</li> 9 * </ul> 10 * 11 parametry H-objekt, který chce hráč číst 12 Text zprávy vypsané po provedeni příkazu 13 */ 15 public String execute(string... arguments) 16 { 17 if (arguments.length < 2) { 18 return Texts.zNEVÍM_CO_ČÍST; //==========> 19 } 20 String itemname = arguments[1]; 21 Hands hands = RUPApartmentGame.getInstance().getBag(); 22 Optional<Item> oitem = hands.getoitem(itemname); 23 if (! oitem.ispresent()) { 24 return Texts.zNENÍ_V_BATOHU; //==========> 25 } 26 String message; 27 if (Texts.PAPÍR.equalsIgnoreCase(itemName)) { 28 message = Texts.zNAPSÁNO_PAPÍR; 29 } 30 else if (Texts.ČASOPIS.equalsIgnoreCase(itemName)) { 31 message = Texts.zNAPSÁNO_ČASOPIS; 32 } 33 else { 34 return Texts.zNEČTITELNÝ_OBJEKT + itemname; //==========> Strana 289 z 351

290 Kapitola 13: Definice nestandardních akcí } 36 return Texts.zCHCE_PŘEČÍST + oitem.get().getname() + 37 (State.isGlassesPutOn()? message : Texts.zNEMÁ_BRÝLE); 38 } Test Po dokončení metody spustíme pravidelný test, který nám prozradí, že příkaz zpracovávaný právě definovanou akcí úspěšně prošel a že se testovací program nyní zarazil na tom, že hra neumí zareagovat na příkaz Nasaď Brýle (viz výpis 13.12). Vrhněmež se tedy na něj. Výpis 13.12: Klíčová část hlášení o průběhu testu po dokončení definice třídy ActionRead; zpráva oznamuje neschopnost hry reagovat na příkaz Polož Brýle 1 ============================================================================= 2 Při testu následujícího, tj. 12. kroku: Nasaď Brýle 3 se objevila CHYBA 4 ============================================================================= 5 Začátky očekávané a obdržené zprávy nesouhlasí. 6 Čekám: «Nasadil(a) jste si brýle.» 7 Přišlo: «Tento příkaz neznám. 8 Chcete-li poradit, zadejte příkaz?» 9 Ověřený počátek zprávy: Odchylka na pozici 0 12 očekávám kód 78 znak «N» 13 obdržel jsem kód 84 znak «T» 14 ============================================================================= 13.8 Definice akce pro nasazení brýlí Definice třídy ActionPutOnGlasses, jejíž instance budou mít na starosti zpracování příkazu pro nasazení brýlí, nepřináší žádný nový problém. Předpokládám, že byste ji byli jistě schopni definovat sami. Výpis s definicí metody execute(string...) proto uvádím pouze pro kontrolu. Pokud se někdo diví tomu, že jsem tentokrát dal do názvu třídy nejenom název akce, ale i požadovanou hodnotu parametru, tak je to proto, že tato akce s jinou hodnotou parametru v zadaném příkazu nepracuje (přesněji ohlásí chybu). Strana 290 z 351

291 Kapitola 13: Definice nestandardních akcí 291 Výpis 13.13: Definice metody execute(string...) ve třídě ActionPutOnGlasses 1 /*************************************************************************** 2 * Simuluje nasazení zadaného objektu, přičemž tímto objektem musí být 3 * brýle, které navíc musí mít hráč v daném okamžiku v batohu. 4 * 5 arguments Parametry příkazu 6 Text zprávy vypsané po provedeni příkazu 7 */ 9 public String execute(string... arguments) 10 { 11 if (arguments.length < 2) { 12 return Texts.zNEVÍM_CO_NASADIT; 13 } 14 String itemname = arguments[1]; 15 Optional<Item> oglasses = Hands.getInstance().getOItem(itemName); 16 if (! oglasses.ispresent()) { 17 return Texts.zNENÍ_V_BATOHU + itemname; 18 } 19 if (Texts.BRÝLE.equalsIgnoreCase(itemName)) { 20 State.setGlassesPutOn(true); 21 Hands.getInstance().removeItem(oGlasses.get()); 22 return Texts.zNASADIL_BRÝLE; 23 } 24 else { 25 return Texts.zNELZE_NASADIT + itemname; 26 } 27 } Test Jak už jste si doufám zvykli, po dokončení metody spustíme pravidelný test. Ten nám prozradí, že nasazení brýlí v základním úspěšném scénáři úspěšně prošlo a že se testovací program nyní zarazil na tom, že hra neumí zareagovat na příkaz Podlož Lednička Časopis (viz výpis 13.14). Takže pokračujeme definicí akce podkládající ledničku. Výpis 13.14: Klíčová část hlášení o průběhu testu po dokončení definice třídy ActionPutOnGlasses; zpráva oznamuje neschopnost hry reagovat na příkaz Podlož Lednička Časopis 1 ============================================================================= 2 Při testu následujícího, tj. 15. kroku: Podlož Lednička Časopis 3 se objevila CHYBA 4 ============================================================================= 5 Začátky očekávané a obdržené zprávy nesouhlasí. 6 Čekám: «Rozhodl(a) jste se podložit h-objekt Lednička h-objektem Časopis. 7 Zadaná akce nebyla provedena.» 8 Přišlo: «Tento příkaz neznám. Strana 291 z 351

292 Kapitola 13: Definice nestandardních akcí Chcete-li poradit, zadejte příkaz?» 10 Očekávaná zpráva je delší než obdržená 11 Ověřený počátek zprávy: Odchylka na pozici 0 14 očekávám kód 82 znak «R» 15 obdržel jsem kód 84 znak «T» 16 ============================================================================= 13.9 Definice akce pro podložení ledničky Před definicí nové třídy, kterou bychom mohli pojmenovat ActionSupportIcebox, se opět zamyslíme, jaké chování scénář pravděpodobně vyžaduje a jaké bychom měli ještě přidat z logiky věci. Naše úvahy by nás měly dovést k následujícím požadavkům: Musí být zadány dva parametry: První definuje, co se má podložit. Druhý definuje, čím se má zmíněný h-objekt podložit. Podkládaný h-objekt se musí vyskytovat v aktuálním prostoru. Podkládací h-objekt se musí vyskytovat v batohu (hráč jej musí držet v ruce). Podložit smíme pouze vybranými h-objekty. Z h-objektů v naší hře je pro tento účel vhodný pouze časopis. Je zřejmé, že podkládat ledničku deštníkem, županem nebo brýlemi asi není dobrý nápad. Aby mohl hráč podkládaný h-objekt nadzdvihnout, musí mít nejméně jednu ruku volnou, tj. batoh nesmí být plný. Test plnosti batohu První tři body jsme již řešili, takže byste si s nimi měli umět poradit. Jediným doposud neřešeným problémem je požadavek na neplnost batohu. Tady si opět můžeme vybrat z několika možných řešení: Zkusíme do batohu vložit fiktivní h-objekt (nazveme jej např. Díra). Nepodaří-li se h-objekt do batohu vložit, víme, že batoh je plný a vrátíme příslušnou chybovou zprávu. Nepodaří-li se h-objekt do batohu vložit, tak jej opět vyjmeme a můžeme se pokusit zadaný h-objekt podložit. Strana 292 z 351

293 Kapitola 13: Definice nestandardních akcí 293 Naučíme batoh prozradit, zda je v něm ještě volno (přidáme mu odpovídající metodu). Druhé řešení působí čistším dojmem. Doplníme tedy třídu Hands o metodu isfull(), která prozradí, zda je kontejner plný. Její definici byste jistě zvládli sami, ale pro úplnost ji uvádím ve výpisu Výpis 13.15: Definice metody isfull() instancí třídy ItemContainer 1 /*************************************************************************** 2 * Vrátí informaci o tom, zda je batoh již plný, 3 * anebo zda se do něj ještě něco vejde. 4 * 5 Je-li plný, vrátí {@code true}, není-li, vrátí {@code false} 6 */ 7 boolean isfull() 8 { 9 return remains == 0; 10 } Teoreticky bychom samozřejmě mohli doplnit univerzálnější metodu nazvanou např. getreamins(), která by prozradila, kolik je v kontejneru ještě volného místa, ale protože takovouto dokonalou informaci zatím nepotřebujeme, zůstaneme u jednodušší metody isfull(), s níž se nám bude i jednodušeji pracovat. Teoreticky bychom mohli doplnit i test, jestli podkládaný h-objekt již náhodou není podložen. Vzhledem k tomu že v naší ukázkové hře je možno podložit jediný h-objekt (ledničku), a to jediným h-objektem (časopisem), tak nám tuto možnost ostatní testy automaticky vyloučí. Kdybychom se ale rozhodli dělat hru obecnější, museli bychom asi tuto možnost vzít do úvahy. O tom si mohou zájemci popřemýšlet. Definice metody execute(string...) Se vším, co pro definici této metody potřebujeme, jsme se již setkali, anebo jsme si to již připravili. Můžeme se tedy směle pustit do definice výkonné metody execute(string...). Protože byste již měli mít všechny potřebné vědomosti, doporučuji vám si zkusit tuto metodu naprogramovat sami a na výpis 13.16, v němž je metoda definovaná, se následně podívat pouze pro kontrolu. Výpis 13.16: Definice metody execute(string...) instancí třídy ActionSupportIcebox 1 /*************************************************************************** 2 * Podloží h-objekt zadaný v prvním parametru Strana 293 z 351

294 Kapitola 13: Definice nestandardních akcí * h-objektem zadaným v druhém parametru. 4 * Před tím však zkontroluje, 5 * <ul> 6 * <li>zda je zadán h-objekt, který se má podkládat,</li> 7 * <li>zda je zadán h-objekt, kterým se má podkládat,</li> 8 * <li>zda je podkládaný h-objekt v aktuálním prostoru,</li> 9 * <li>zda je podkládací h-objekt v batohu (=hráč jej drží v ruce),</li> 10 * <li>zda je podkládaný h-objekt lednička,</li> 11 * <li>zda je podkládací h-objekt časopis (=hráč jej drží v ruce),</li> 12 * </ul> 13 * 14 parametry Jediným povoleným parametrem je zatím lednička 15 Text zprávy vypsané po provedeni příkazu 16 */ 18 public String execute(string... arguments) 19 { 20 if (arguments.length < 2) { 21 return Texts.zNEVÍM_CO_PODLOŽIT; 22 } 23 if (arguments.length < 3) { 24 return Texts.zNEVÍM_ČÍM_PODLOŽIT; 25 } 26 String namesupported = arguments[1]; 27 String namesupporting = arguments[2]; 28 String message1 = Texts.zCHCE_PODLOŽIT + namesupported + 29 Texts.zH-OBJEKTEM + namesupporting + '.'; 30 String messageerr = message1 + '\n'; 31 Room currentroom = Apartment.getInstance().getCurrentArea(); 32 Hands hands = Hands.getInstance(); Optional<Item> osupportedobject = currentroom.getoitem(namesupported); 35 Optional<Item> osupportingobject = hands.getoitem(namesupporting); if (! osupportedobject.ispresent()) { 38 return messageerr + Texts.zPODKLÁDANÝ_NENÍ_V_MÍSTNOSTI + namesupported; 39 } 40 if (! osupportingobject.ispresent()) { 41 return messageerr + Texts.zPODKLÁDACÍ_NENÍ_V_BATOHU + namesupported; 42 } 43 if (! Texts.LEDNIČKA.equalsIgnoreCase(nameSupported)) { 44 return messageerr + Texts.zTOTO_NELZE_PODLOŽIT + namesupported; 45 } 46 else if (Texts.ČASOPIS.equalsIgnoreCase(nameSupported)) { 47 return messageerr + Texts.zTÍMTO_NELZE_PODLOŽIT + namesupporting; 48 } 49 else if (hands.isfull()) { 50 return messageerr + Texts.zNELZE_NADZVEDNOUT; 51 } 52 else { 53 hands.removeitem(osupportingobject.get()); 54 State.setIceboxSupported(true); Strana 294 z 351

295 Kapitola 13: Definice nestandardních akcí return message1 + Texts.zLEDNIČKA_PODLOŽENA; 56 } 57 } Jak vidíte, metoda je nechutně dlouhá. Měli bychom s tím něco udělat. Teď ale v prvním kole nejprve aplikaci rozchodíme, a poté můžeme začít vylepšovat její architekturu. Věnujeme tomu samostatnou kapitolu. Test Test spuštěný po dokončení definice třídy ActionSupportIcebox popojede zase o kousek dále a zastaví se s hlášením, jehož klíčovou část si můžete prohlédnout ve výpisu Výpis 13.17: Klíčová část hlášení o průběhu testu po dokončení definice třídy ActionPutOnGlasses; zpráva oznamuje neschopnost hry reagovat na příkaz Podlož Lednička Časopis 1 ============================================================================= 2 Při testu následujícího, tj. 19. kroku: Vezmi Pivo 3 se objevila CHYBA 4 ============================================================================= 5 Začátky očekávané a obdržené zprávy nesouhlasí. 6 Čekám: «Pokoušíte si vzít z inteligentní ledničky Pivo. 7 Toto je inteligentní lednička, která neumožňuje» 8 Přišlo: «Vzal(a) jste h-objekt: pivo» 9 Očekávaná zpráva je delší než obdržená 10 Ověřený počátek zprávy: Odchylka na pozici 0 13 očekávám kód 80 znak «P» 14 obdržel jsem kód 86 znak «V» 15 ============================================================================= Podařilo se tedy ledničku podložit a následně otevřít tak, aby test byl spokojen. Test se zastavil se až u braní piva, protože lednička se má tvářit, že je inteligentní, a je proto ochotna vydávat alkoholické nápoje pouze zletilým osobám. Musí proto nejprve zjistit, zda je hráč plnoletý. O tom, jak naprogramovat h-objekty, které se s vámi snaží zapřádat rozhovor, si povíme v příští kapitole. Strana 295 z 351

296 Kapitola 13: Definice nestandardních akcí Co jsme prozatím přeskočili Při výkladu definice tříd, jejichž instance budou zodpovědné za zpracování jednotlivých nestandardních akcí, jsem v zájmu stručnosti vynechal jednu důležitou věc, kterou jsou testy. V chybovém scénáři jste měli za povinnost definovat kroky, které otestují všechna možná chybná zadání standardních příkazů. Aby byl váš návrh dokonalý, měli byste nyní doplnit kroky scénáře, který obdobným způsobem otestuje i korektní reakci hry na chybná zadání vašich nestandardních akcí. Někdy je ale uvedení hry do stavu, v němž je možno plně otestovat některou z nestandardních akcí, poměrně zdlouhavé. V takové situaci může být výhodnější opustit scénáře a definovat separátní testovací třídu pro otestování reakcí na všechna chybná zadání příkazů vedoucích ke spuštění dané akce. Tento postup je výhodnější v situaci, kdy vaše testovací třída může uvést hru do potřebného stavu mnohem rychleji a efektivněji, než by bylo možno prostřednictvím emulace průběhu hry. Nepříjemné však u toho je, že takto navržené testy většinou neprovedou kompletní test výsledného stavu hry. Výhodou práce se scénáři je naopak to, že uvedou hru do požadovaného stavu přirozeným způsobem a že v rámci jednoho scénáře lze postupně realizovat celou řadu testů, jak je to předvedeno např. v chybovém scénáři. Doporučoval bych vám proto doplnit hru scénářem pojmenovaným např. NON_STANDARD_MISTAKES, v němž byste vyzkoušeli možná chybná zadání příkazů vyvolávající vaše rozšiřující akce Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Zopakujme si, co jsme v této kapitole dozvěděli a co jsme naprogramovali: Ke stavu vývoje projektu, který jsme shrnuli v podkapitole 12.8 Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali na straně 270, jsme přidali následující: V metodě main(string[]) správce scénářů jsme opět aktivovali standardní test aplikace. Definovali jsme třídu State jako schránku na nejrůznější příznaky, které je třeba si pamatovat v průběhu hry. Každý příznak je definovaný jako statický atribut s příslušnými přístupovými metodami. Strana 296 z 351

297 Kapitola 13: Definice nestandardních akcí 297 Ve třídě State jsme definovali metodu initialize(), která má na starosti uvedení všech atributů do správného počátečního stavu. Její volání jsme proto zařadili do inicializační metody ve třídě AAction. Ve třídě State jsme definovali vlastnost (= atribut + přístupové metody) iceboxsupported uchovávající informaci o tom, je-li již lednička podložena. Definovali jsme třídu ActionOpen, jejíž instance je zodpovědná za správnou reakci na příkaz pro otevření ledničky. Opravili jsme chybu v metodě execute(string...) instancí třídy ActionPutDown. Definovali jsme třídu ActionRead, jejíž instance je zodpovědná za správnou reakci na příkaz k přečtení zadaného h-objektu papíru nebo časopisu. Definovali jsme třídu ActionSupportIcebox, jejíž instance je zodpovědná za správnou reakci na příkaz pro podložení ledničky. Program, jehož zdrojové kódy odpovídají výše popsanému stavu vývoje aplikace, najdete v projektu A113z_NonstandardActions. Jeho diagram tříd si můžete prohlédnout na obrázku Obrázek 13.1 Diagram tříd projektua113z_nonstandardactions zachycujícího stav poté, co byly definovány třídy reprezentující nestandardní příkazy hry bez podpory rozhovoru Strana 297 z 351

298 Kapitola 14: Realizace rozhovoru Realizace rozhovoru Kapitola 14 Realizace rozhovoru Co se v kapitole naučíte V této kapitole se soustředíme na to, jak naprogramovat rozhovor hráče s některým z h-objektů hry. Ukážeme si, jak na dobu rozhovoru odstavit správce akcí a jak jej po skončení rozhovoru opět spustit. V této kapitole budeme pokračovat v práci s projektem obsahujícím závěrečnou podobu projektu, k níž jsme se dopracovali v předchozí kapitole, tj. s projektem A113z_NonstandardActions Specifika rozhovoru Naše hra spouštěná podle úspěšného scénáře doběhla až do okamžiku, kdy má lednička zahájit rozhovor a hráčem, aby si ověřila, že je plnoletý. Problémem rozhovoru je to, že jej není možné řídit dosavadními prostředky pro reakci na zadávané příkazy. Věty, kterými hráč oslovuje nějaký h-objekt či kterými odpovídá na jeho dotazy, není vhodné zpracovávat stejně jako příkazy, tj. definovat h-objekty zodpovědné za tuto reakci tak, že budou jednoznačně určeny prvním proneseným slovem. V takovém rozhovoru totiž často nejde dopředu přesně specifikovat, jaké to první slovo bude. Při programování chování programu v průběhu rozhovoru si musíme uvědomit, že rozhovor je třeba zpracovávat poněkud jinak, a přepnout proto zpracovávání odpovědí hráče do jiného režimu. Tady trochu záleží na tom, kdo vlastně ten rozhovor začíná. Zahajuje-li rozhovor nějaký h-objekt, je to jednodušší, protože změnu režimu můžeme zapracovat do akce zpracovávající reakce osloveného h-objektu, jejímž výsledkem je zahájení onoho rozhovoru. Strana 298 z 351

299 Kapitola 14: Realizace rozhovoru 299 Zahajuje-li rozhovor hráč, musíme to zařídit tak, abychom se ve výsledku dostali do stavu popsaného v předchozím bodu. Nejlepší je definovat nějaký příkaz, kterým rozhovor zahájíme a součástí jeho zpracování bude i ona potřebná změna režimu zpracování reakcí hráče např. oslov kouzelný_strom Situace v naší hře odpovídá prvnímu bodu, protože rozhovor zahajuje lednička v okamžiku, kdy si z ní chce hráč vzít nějaký alkoholický nápoj. Pojďme se tedy podívat, jak bychom mohli takovouto úlohu naprogramovat Přepnutí do konverzačního režimu První, co budeme muset upravit, je metoda execute(string...) ve třídě ActionPickUp. Musíme zabezpečit, aby se hra v situaci, kdy hráč chce odebrat z ledničky alkoholický nápoj a lednička jej nemá ještě prověřeného, přepnula do konverzačního režimu, v němž si ověří, že hráč je již plnoletý. Zavedení příznaku prověřenosti Když se zamyslíme nad předchozím popisem požadované reakce, tak by nás mělo napadnout, že bychom asi měli ještě před úpravou metody execute(string...) definovat příznak, v němž si bude lednička pamatovat, zda již ověřila, že hráč je plnoletý, aby se jej při každém pokusu o odebrání alkoholického nápoje neptala znovu. Jak si jistě pamatujeme, pro chovávání hodnot nejrůznějších příznaků jsme definovali knihovní třídu State. Definujeme v ní proto příznak ismajor spolu s příslušnými přístupovými metodami. Současně nezapomeneme přidat inicializaci tohoto příznaku do metody initialize(), aby na počátku příštího spuštění hry byl hráč opět neprověřený. Úprava metody execute(string...) ve třídě ActionPickUp Příznak prověřenosti máme tedy definován (a inicializován), takže se můžeme soustředit na vlastní přepnutí do konverzačního režimu, které musíme začlenit do reakce na příkaz zvednutí h-objektu, jejíž dosavadní podobu si můžete prohlédnout ve výpisu 12.7 na straně 263. Na metodě toho moc měnit nemusíme. Počáteční prověřování, jestli je odebíraný h-objekt v daném prostoru, je přenositelný a hráč má dostatek místa v batohu, můžeme ponechat. Pouze upravíme finální fázi těsně před vypsáním zprávy Strana 299 z 351

300 Kapitola 14: Realizace rozhovoru 300 uživateli. Když už jsme ověřili splnění výše popsaných parametrů, tak ještě zjistíme, jestli se náhodou nejedná o odebírání alkoholického nápoje z ledničky neprověřeným uživatelem. Pokud ano, zapřede s ním program konverzaci, při níž se bude snažit zjistit, je-li uživatel plnoletý. Upravenou verzi definice této metody naleznete ve výpisu 14.1, přičemž upravená část kódu je zvýrazněna. Pojďme si ji ještě zrychleně projít: Až do řádku 27 se původní kód nemění. V tuto chvíli je vše potřebné ověřeno, takže se můžeme zeptat, jestli se náhodou nejedná o výše popsanou speciální situaci (test na řádcích 28 a 29). Pokud ano (předchozí test i reakce v případě, kdy je splněn, jsou ve výpisu zvýrazněny): Vyjmeme zpět z batohu h-objekt (řádek 31), který jsme do něj zkušebně vložili při ověřování, zda se do něj vejde (řádek 26). Spustíme konverzaci, přičemž musíme metodě, která konverzaci řídí, předat h-objekt, o nějž hráč ledničku požádal, aby mu jej mohla po úspěšně skončené konverzaci předat. Pokud ne, dokončíme metodu standardním způsobem Výpis 14.1: Upravená definice metody execute(string...) ve třídě ActionPickUp 1 /*************************************************************************** 2 * Odebere h-objekt zadaný v parametru z aktuálního prostoru (místnosti) 3 * a vloží jej do batohu. 4 * Vyžaduje však, aby se h-objekt se zadaným názvem<br> 5 * a) nacházel v aktuálním prostoru,<br> 6 * b) byl zvednutelný a<br> 7 * c) vešel se do batohu.<br> 8 * Jinak přesun neprovede a bude příkaz považovat za chybný. 9 * 10 arguments Parametry příkazu 11 Text zprávy vypsané po provedeni příkazu 12 */ 14 public String execute(string... arguments) 15 { 16 if (arguments.length < 2) { 17 return Texts.zNEVÍM_CO_VZÍT; 18 } 19 String itemname = arguments[1]; 20 Room currentroom = Apartment.getInstance().getCurrentArea(); 21 Optional<Item> oitem = currentroom.getoitem(itemname); 22 if (! oitem.ispresent()) { 23 return Texts.zNENÍ_PŘÍTOMEN + itemname; 24 } 25 Item item = oitem.get(); Strana 300 z 351

301 Kapitola 14: Realizace rozhovoru Hands bag = Hands.getInstance(); 27 if (item.getweight() >= bag.getcapacity()) { 28 return Texts.zTĚŽKÝ_H-OBJEKT + itemname; 29 } 30 boolean added = bag.tryadditem(item); 31 if (added) { 32 if (currentroom.equals(apartment.getinstance() 33.getORoom(Texts.LEDNIČKA) 34.get()) && 35 item.isalcoholic() &&! State.isMajor()) 36 { 37 bag.removeitem(item); 38 return Conversation.start(item); 39 } 40 currentroom.removeitem(item); 41 return Texts.zZVEDNUTO + itemname; 42 } 43 else { 44 return Texts.zBATOH_PLNÝ; 45 } 46 } 14.3 Třída Conversation Pro realizaci konverzace definujeme zvláštní třídu, kterou nazveme Conversation. Můžeme se rozhodnout, zda vším pověříme objekt třídy, anebo od ní necháme vytvořit instanci, která pak bude mít vše na starosti. Při rozhodování o tom, zda dát přednost objektu třídy či její instanci je nejdůležitější to, jestli někdy budeme potřebovat daný objekt ukládat do proměnné. Pokud ano, není z čeho vybírat, protože objekt třídy v Javě do proměnné neuložíme. (Pravda, můžeme do ní uložit její class-objekt, ale volání metod prostřednictvím class-objektu je poměrně neefektivní, takže po něm saháme pouze v případech, kdy jsou ostatní cesty ještě horší.) Protože prozatím nevidím žádný důvod pro to, abych objekt, zodpovědný za řízení konverzace, někam ukládal, rozhodl jsem se zvolit jednodušší řešení žádnou instanci nevytvářet a pověřit řízením konverzace objekt třídy. Kdybych později zjistil, že to nebylo dobré rozhodnutí, mohu to změnit. Metoda start(item) Metoda, která odstartuje konverzaci, musí udělat následující: Musí především nastavit příznak specifikující, že hra probíhá v režimu konverzace. Strana 301 z 351

302 Kapitola 14: Realizace rozhovoru 302 Musí si zapamatovat nápoj, který chtěl hráč z ledničky odebrat, aby mu jej objekt řídící konverzaci mohl po skončené konverzaci vydat. Musí připravit zprávu oznamující hráči, proč mu nápoj nebyl vydán, a co musí udělat pro to, aby jej získal. Tato zpráva musí respektovat zadání ve scénáři. Jak vidíte, není to naštěstí nic složitého. Zdrojový možné podoby této metody si můžete prohlédnout ve výpisu (Na konci výpisu je za příkazem return vložen komentář, v němž je naznačena odpověď hry v textové podobě pro případ, kdy by byl použit scénář používající literály.) Výpis 14.2: Počáteční verze definice metody start(item) ve třídě Conversation 1 /*************************************************************************** 1 * Odstartuje rozhovor o nemožnosti jednoduchého odebrání zadaného h-objektu, 2 * který je alkoholickým nápojem. 3 * 4 drink H-objekt, který chce hráč odebrat z ledničky 5 První část rozhovoru pronesená ledničkou 6 */ 7 static String start(item drink) 8 { 9 State.setConversation(true); 10 Conversation.drink = drink; 11 String drinkname = drink.getname(); return zbere_alkohol + drinkname + "." + zkolik_let; 14 // "Pokoušíte si vzít z inteligentní ledničky " + drinkname + 15 // "Tato lednička neumožňuje podávání alkoholických nápojů " + 16 // "mladistvým.\nkolik je vám let?"); 17 } 14.4 Úprava metody executecommand(string) ve třídě AAction Definicí objektu zodpovědného za řízení konverzace však ještě naši úlohu neřeší. Musíme myslet na to, že veškerá komunikace s uživatelem probíhá přes metodu executecommand(string) správce akcí (její dosavadní podobu najdete ve výpisu na straně 269). Ta rozhoduje o tom, kdo bude pověřen vlastním zpracováním reakce na uživatelovo zadání. Musíme proto upravit ještě tuto metodu, aby do svého rozhodování zakomponovala i informaci o tom, neprobíhá-li právě nějaká konverzace. Strana 302 z 351

303 Kapitola 14: Realizace rozhovoru 303 Metoda se nejprve rozhoduje podle toho, jestli hra znova běží. Toto počáteční rozhodování bychom ponechali. Pokud hra běží, zjišťuje, nezadal-li uživatel prázdný příkaz. Tady je již na naší úvaze, jestli na prázdnou odpověď v průběhu rozhovoru zareagujeme stejně, jako bychom na ni zareagovali v režimu zadávání příkazů. Já bych se přimlouval za to, aby objekt zodpovědný za vedení rozhovoru, řešil všechny situace včetně oné prázdné odpovědi. Rozhodneme-li se pro tuto variantu, mohla by upravená verze dispečerské metody executecommand(string) vypadat podle výpisu 14.3 (změněný kód na řádcích 30 a 31 je ve výpisu zvýrazněn). Výpis 14.3: Upravená definice metody executecommand ve třídě AAction 1 /*************************************************************************** 2 * Zpracuje zadaný příkaz a vrátí text zprávy pro uživatele. 3 * 4 command Zadávaný příkaz 5 Textová odpověď hry na zadaný příkaz 6 */ 7 public static String executecommand(string command) 8 { 9 command = command.trim(); 10 String answer; 11 if (isalive) { 12 answer = State.isConversation()? Conversation.answer(command) 13 : executecommoncomand(command); 14 } 15 else { 16 answer = startgame(command); 17 } 18 return answer; 19 } 14.5 Definice metody answer(string) ve třídě Conversation Předchozí definice je však zatím nepřeložitelná, protože jsme ve třídě Conversation ještě nedefinovali metodu answer(string), kterou tato definice volá. Pojďme to tedy napravit. Strana 303 z 351

304 Kapitola 14: Realizace rozhovoru 304 Stavy rozhovoru První věcí, nad níž se musíme zamyslet, je zapracování toho, že konverzace většinou probíhá různými stavy. Jeden účastník něco řekne, druhý na to nějak zareaguje a první účastník reaguje na tuto reakci. U složitějších průběhů je vhodné si nakreslit stavový diagram a pomocí něj si ujasnit, jak přesně budou přechody mezi jednotlivými stavy probíhat. Možné stavy našeho rozhovoru jsou tak jednoduché, že bychom pro ně žádný diagram kreslit nemuseli. Z cvičných důvodů si jej ale nakreslíme. Připomenu jenom možný průběh rozhovoru: 0. Při odstartování rozhovoru se program hráče zeptá na jeho věk a přesune se do stavu, v němž očekává zadání věku. 1. Lze-li odpověď hráče považovat za zadání aktuálního věku, mohou nastat dvě různá pokračování: Nelze-li odpověď hráče považovat za zadání věku, program požádá hráče o opravu. Zůstává nadále v tomto stavu a čeká na zadání věku hráče. Je-li zadán nízký věk, lednička odmítne hráči požadovaný alkoholický nápoj vydat. Je-li zadán akceptovatelný věk, program se zeptá hráče na rok jeho narození a přesune se do stavu, v němž očekává zadání tohoto roku. 2. Ve stavu očekávajícím zadání roku narození je opět několik možností pokračování: Nelze-li odpověď hráče považovat za zadání roku narození, program požádá hráče o opravu. Zůstává nadále v tomto stavu a čeká na zadání roku narození hráče. Odpovídá-li zadaný rok narození dříve zadanému věku, vydá lednička hráči požadovaný nápoj a zapamatuje si, že hráč je prověřený a bude mu proto ochotna vydávat alkoholické nápoje i příště. Neodpovídá-li zadaný rok narození dříve zadanému věku, lednička odmítne hráči požadovaný alkoholický nápoj vydat. To, že hráč v prvním či druhém kole neprošel prověrkou, si lednička nepamatuje a umožní mu se příště znovu pokusit prověrkou projít. Stavový diagram zachycující výše popsaný přechod mezi stavy si můžete prohlédnout na obrázku Strana 304 z 351

305 Kapitola 14: Realizace rozhovoru 305 stm Stavy rozhovoru Špatně Špatně ŘekniVěk Číslo ŘekniRok Odpovídá Prověřený Malé číslo Neodpovídá Obrázek 14.1 Stavový diagram rozhovoru přechody mezi stavy v závislosti na odpovědi hráče Reakce v závislosti na stavu Nyní je třeba ještě vyřešit reakci programu v závislosti na stavu, do nějž se rozhovor dostal. Možností řešení je (jako obyčejně) celá řada: Strukturovaně orientovaný programátor by si zapamatovat aktuální stav v nějaké proměnné a pak se podle hodnoty této proměnné rozhodoval, jaké pokračování zvolíme. K tomuto rozhodování by použil nejspíš přepínač. Objektově orientovaný programátor by pro každý stav definoval objekt (např. jako hodnotu výčtového typu či instanci anonymní třídy). Každý z těchto objektů by měl vlastní verzi metody definující požadovanou reakci. V nějaké proměnné bychom pak uchovávali objekt odpovídající aktuálnímu stavu a pro reakci na odpověď hráče bychom zavolali jeho metodu. Funkcionálně orientovaný programátor by nepotřeboval balit spouštěnou metodu do objektu, ale ukládal by do oné proměnné přímo odkaz na funkci definující požadovanou reakci v daném stavu rozhovoru. Protože už máme v jazyku nástroje, umožňující pracovat jednoduše s metodami jako s objekty, přikloníme se k poslední variantě. Pro onu stavovou proměnnou definujeme atribut (konverzaci řeší objekt třídy, takže atribut musí být statický) private static Function<String, String> statedependentanswer; do nějž uložíme odkaz na funkci, které předáme v parametru text s odpovědí hráče a která vrátí text s odpovědí hry. Každá metoda pak nastaví atribut na metodu odpovídající stavu, do kterého hra přešla. V naší hře máme pouze dva přechodné stavy, takže definujeme dvě metody. Pro zpracování reakce na opověď hráče ve stavu, kdy po něm hra chtěla, aby ji prozradil svůj věk, bychom definovali metodu private static String waitingfortheage(string useranswer) a pro zpracování reakce ve stavu, kdy po hráči chceme, aby nám zadal rok svého narození, metodu Strana 305 z 351

306 Kapitola 14: Realizace rozhovoru 306 private static String waitingfortheyear(string useranswer) Prozatím definujeme obě metody s poloprázdným tělem vyhazujícím výjimku unsupportedoperationexception. Současně musíme upravit metodu start(item) (viz výpis 14.2 na straně 302) tak, abychom před vrácením odpovědi nastavili hodnotu stavové proměnné příkazem statedependentanswer = Conversation::waitingForTheAge; Vlastní definice metody answer(string) Všechny předchozí úvahy vedou k superjednoduché definici obsahující jediný příkaz, který zavolá metodu uloženou ve stavové proměnné statedependentanswer viz výpis Výpis 14.4: Definice metody answer(string) ve třídě Conversation 1 /*************************************************************************** 20 * Metoda řešící reakce hry na odpovědi hráče v průběhu konverzace. 21 * 22 command Odpověď hráče na předchozí otázku 23 Odpověď hry hráči 24 */ 25 static String answer(string command) 26 { 27 return statedependentanswer.apply(command); 28 } Test Takže bychom měli mít vše hotovo pro to, abychom mohli spustit test, jestli jsme se ve scénáři dostali o krok dál. Po spuštění testu se dozvíme (viz výpis 14.5), že 19. krok se nám opravdu podařilo projít a test byl ukončen vyhozením výjimky: Výpis 14.5: Klíčová část hlášení o průběhu testu po dokončení předběžné definice třídy Conversation; zpráva oznamuje nedokončenost definice metody waitingfortheage(string) krok: «Vezmi Pivo» Očekávaný stav po provedení akce: krok 5 Typ kroku: tsunmovable 6 Příkaz: Vezmi Pivo 7 Prostor: Lednička 8 Východy: () 9 H-objekty: («Houska», «Pivo», «Pivo», «Pivo», «Rum», «Salám», «Víno») Strana 306 z 351

307 Kapitola 14: Realizace rozhovoru Batoh: () Zpráva: 13 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 14 Pokoušíte si vzít z inteligentní ledničky Pivo. 15 Toto je inteligentní lednička, která neumožňuje 16 podávání alkoholických nápojů mladistvým. 17 Kolik je vám let? Obdržená zpráva: 20 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 21 Pokoušíte si vzít z inteligentní ledničky Pivo. 22 Toto je inteligentní lednička, která neumožňuje 23 podávání alkoholických nápojů mladistvým. 24 Kolik je vám let? ===== Konec testu prováděných operací ===== 28 ############################################################################# 29 ##### Hra: Ukázková hra: Byt s inteligentní ledničkou 30 ##### Autor: RUP15P PECINOVSKÝ Rudolf 31 ##### Scénář: _HAPPY_ 32 ##### Třída hry: eu.pedu.adventure15p.rupapartmentgame 33 ##### Čas: Wed Aug 19 14:05:24 CEST ############################################################################# Ukončení bylo způsobeno vyhozením výjimky: 37 eu.pedu.adv15p_fw.test_util.common.testexception: 38 Při vykonávání příkazu: «20» 39 vyhodila hra výjimku UnsupportedOperationException 40 at eu.pedu.adv15p_fw.test_util.tgametests.tgameruntester.verifyscenariostep( TGameRunTester.java:267) 41 at eu.pedu.adv15p_fw.test_util.tgametests.tgameruntester.executescenario( TGameRunTester.java:141) 42 at eu.pedu.adv15p_fw.test_util.tgametests.tgametester.testgamebyscenario(tgametester.java:179) 43 at eu.pedu.adv15p_fw.test_util.tgametests.tgametester.testgamebyscenarios(tgametester.java:141) 44 at eu.pedu.adv15p_fw.test_util.tgametests.tgametester.testgame(tgametester.java:124) 45 at eu.pedu.adv15p_fw.scenario.ascenariomanager.testgame(ascenariomanager.java:411) 46 at eu.pedu.adventure15p.rupscenariomanagercon.main(rupscenariomanagercon.java:725) 47 Caused by: java.lang.unsupportedoperationexception: Prozatím neimplementováno 48 at eu.pedu.adventure15p.conversation.waitingfortheage(conversation.java:100) 49 at eu.pedu.adventure15p.conversation$$lambda$54/ apply(unknown Source) 50 at eu.pedu.adventure15p.conversation.answer(conversation.java:63) 51 at eu.pedu.adventure15p.aaction.executecommand(aaction.java:111) 52 at eu.pedu.adventure15p.rupapartmentgame.executecommand(rupapartmentgame.java:184) 53 at eu.pedu.adv15p_fw.test_util.tgametests.tgameruntester.verifyscenariostep( TGameRunTester.java:262) more Strana 307 z 351

308 Kapitola 14: Realizace rozhovoru 308 Zdá se, že se nám prozatím podařilo vše naprogramovat správně (nebo alespoň tak, aby prošly testy) Ze zprávy vyplývá, že test podle očekávání zhavaroval na nedokončené definici metody waitingfortheage. Pojďme tedy na ni Definice metody waitingfortheage(string) Metoda waitingfortheage(string) bude zodpovědná za reakci hry v první fázi konverzace, tj. ve stavu, kde po hráči chceme, aby nám prozradil svůj věk. Metoda musí nejprve ověřit, že hráč zadal svůj věk opravdu jako celé číslo za zadaného rozsahu. Není-li tomu tak, upozorní jej na to. Konverzace přitom zůstává ve stejném stavu, protože se nadále čeká na zadání věku hráče. Zadá-li hráč místo čísla nějaký text, metoda převádějící řetězec na číslo vyhodí výjimku NumberFormatException. V reakci na ni musí metoda upozornit hráče na to, co se od něj očekává. Stejné upozornění bude metoda vracet i tehdy, když hráč zadá nějaký nesmyslný věk např. -3 roky. Je proto rozumné, aby tento test vyhodil stejnou výjimku, aby se oba druhy špatných zadání ošetřovaly na jednom místě. Zadá-li hráč smysluplný věk, bude pokračování záviset na zadané hodnotě. Nebude-li hráč plnoletý, metoda mu zdvořile oznámí, že mu alkoholický nápoj vydat nemůže, požádá jej, aby si vybral něco jiného, a ukončí konverzaci. Bude-li hráč plnoletý, metoda nastaví další stav konverzace a položí hráči kontrolní otázku na rok jeho narození. Odpověď na ni pak bude řešit metoda zodpovědná za reakci ve druhém stavu konverzace. Přepnutí do nového stavu řeší metoda zadáním nové hodnoty do atributu statedependentanswer, do nějž uloží odkaz na metodu waitingfortheyear(string). Aby tato metoda mohla porovnat zadaný rok narození s dříve zadaným věkem, musíme si zadaný věk zapamatovat. Proto musíme zadanou hodnotu věku uložit do pomocného atributu, který definujeme příkazem private static int age; Zdrojový kód metody navržený na základě předchozích úvah si můžete prohlédnout ve výpisu Výpis 14.6: Definice metody waitingfortheage(string) ve třídě Conversation 1 /*************************************************************************** 2 * Metoda řešící reakci hry na odpovědi uživatele ve fázi, 3 * kdy má uživatel zadat svůj věk. 4 * 5 command Odpověď uživatele 6 Odpověď hry hráči 7 */ Strana 308 z 351

309 Kapitola 14: Realizace rozhovoru private static String waitingfortheage(string useranswer) 9 { 10 try { 11 age = Integer.parseInt(userAnswer); 12 if ((age < LOW_AGE) (HIGH_AGE < age)) { 13 throw new NumberFormatException(); 14 } 15 }catch(numberformatexception nfe) { 16 return String.format(fWRONG_INTEGER + fnot_allowed + fonce_more, 17 fage, LOW_AGE, HIGH_AGE); 18 } 19 if (age < 18) { 20 State.setConversation(false); 21 return String.format(fNOT_ALLOWED, drink.getname()); 22 } 23 statedependentanswer = Conversation::waitingForTheYear; 24 return znarozen; 25 } V předchozí definici najdete ještě jednu novinku. Výstupní text se zde nezadává jako textová konstanta či složenina několika textových konstant, ale začleněním zadaných dat prostřednictvím předpřipraveného formátovacího řetězce. Do třídy Texts byly jako konstanty zadány části tohoto formátovacího řetězce zobrazené ve výpisu Výpis 14.7: Definice dalších konstant ve třídě Texts 1 /** Formáty zpráv vypisovaných v reakci na některé příkazy. */ 2 static final String 3 fwrong_integer = 4 "Musíte zadat svůj %s jako celé číslo", 5 6 fage = "věk", 7 8 fyear = "rok narození", 9 10 fwrong_range = 11 " v rozsahu od %d do %d", fonce_more = 14 ".\nzkuste to ještě jednou.", ftoo_low = 17 "Ve vašem věku ještě nesmíte požívat alkoholické nápoje.", fnot_allowed = 20 "\nvhledem k tomu vám bohužel nemohu %s vydat." + 21 "\nvemte si něco jiného nebo zavřete ledničku.", fdoes_not_match = 24 "Vámi udaných %s let neodpovídá roku narození %s"; Strana 309 z 351

310 Kapitola 14: Realizace rozhovoru 310 První formátovací řetězec je přitom složen ze tří částí proto, aby z nich bylo možno poskládat i řetězec pro následující metodu, která již nebude prozrazovat rozsah akceptovatelných čísel. Test Po dokončení definice metody waitingfortheage(string) spustíme pravidelný test. Ten nám prozradí (viz výpis 14.8), že jsme se opravdu dostali o krok dál. Test se zastavil na výjimce oznamující nedokončenost metody waitingfortheyear(string). Pojďme ji tedy dokončit. Výpis 14.8: Klíčová část hlášení o průběhu testu po dokončení definice metody waitingfortheage(string) ve třídě Conversation; zpráva oznamuje nedokončenost definice metody waitingfortheyear(string) krok: «20» Očekávaný stav po provedení akce: krok 5 Typ kroku: tsdialog 6 Příkaz: 20 7 Prostor: Lednička 8 Východy: () 9 H-objekty: («Houska», «Pivo», «Pivo», «Pivo», «Rum», «Salám», «Víno») 10 Batoh: () Zpráva: 13 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 14 V kterém roce jste se narodil(a)? Obdržená zpráva: 17 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 18 V kterém roce jste se narodil(a)? ===== Konec testu prováděných operací ===== 23 ############################################################################# 24 ##### Hra: Ukázková hra: Byt s inteligentní ledničkou 25 ##### Autor: RUP15P PECINOVSKÝ Rudolf 26 ##### Scénář: _HAPPY_ 27 ##### Třída hry: eu.pedu.adventure15p.rupapartmentgame 28 ##### Čas: Wed Aug 19 16:19:36 CEST ############################################################################# Ukončení bylo způsobeno vyhozením výjimky: 32 eu.pedu.adv15p_fw.test_util.common.testexception: 33 Při vykonávání příkazu: «1995» 34 vyhodila hra výjimku UnsupportedOperationException Strana 310 z 351

311 Kapitola 14: Realizace rozhovoru at eu.pedu.adv15p_fw.test_util.tgametests.tgameruntester.verifyscenariostep( TGameRunTester.java:267) 36 at eu.pedu.adv15p_fw.test_util.tgametests.tgameruntester.executescenario( TGameRunTester.java:140) 37 at eu.pedu.adv15p_fw.test_util.tgametests.tgametester.testgamebyscenario(tgametester.java:179) 38 at eu.pedu.adv15p_fw.test_util.tgametests.tgametester.testgamebyscenarios(tgametester.java:141) 39 at eu.pedu.adv15p_fw.test_util.tgametests.tgametester.testgame(tgametester.java:124) 40 at eu.pedu.adv15p_fw.scenario.ascenariomanager.testgame(ascenariomanager.java:411) 41 at eu.pedu.adventure15p.rupscenariomanagercon.main(rupscenariomanagercon.java:725) 42 Caused by: java.lang.unsupportedoperationexception: Prozatím neimplementováno 43 at eu.pedu.adventure15p.conversation.waitingfortheyear(conversation.java:128) 44 at eu.pedu.adventure15p.conversation$$lambda$55/ apply(unknown Source) 45 at eu.pedu.adventure15p.conversation.answer(conversation.java:63) 46 at eu.pedu.adventure15p.aaction.executecommand(aaction.java:111) 47 at eu.pedu.adventure15p.rupapartmentgame.executecommand(rupapartmentgame.java:184) 48 at eu.pedu.adv15p_fw.test_util.tgametests.tgameruntester.verifyscenariostep( TGameRunTester.java:262) more 14.7 Definice metody waitingfortheyear(string) Specifikum definice metody waitingfortheyear(string) tkví v tom, že metoda musí zjistit, jestli zadaný rok narození odpovídá dříve zadanému věku. Metoda proto musí nejprve zjistit aktuální rok, od něj odečíst zadaný věk a tak spočítat odhadovaný rok narození. Ten pak porovná s rokem zadaným hráčem, a nebudou-li se lišit o více než o jedničku, prohlásí, že bude hráči věřit, že je plnoletý. Zdrojový kód metody navržený na základě předchozích úvah si můžete prohlédnout ve výpisu Výpis 14.9: Definice metody waitingfortheyear(string) ve třídě Conversation 1 /*************************************************************************** 2 * Metoda řešící reakci hry na odpovědi uživatele ve fázi, 3 * kdy má uživatel zadat rok svého narození. 4 * 5 command Odpověď uživatele 6 Odpověď hry hráči 7 */ 8 private static String waitingfortheyear(string useranswer) 9 { 10 int year; 11 try { 12 year = Integer.parseInt(userAnswer); 13 }catch(numberformatexception nfe) { 14 return String.format(fWRONG_INTEGER + fonce_more, "rok narození"); Strana 311 z 351

312 Kapitola 14: Realizace rozhovoru } 16 State.setConversation(false); int thisyear = LocalDate.now().getYear(); 19 int countedage = thisyear - year; 20 if (Math.abs(age - countedage) > 1) { 21 return String.format(fDOES_NOT_MATCH + fnot_allowed, 22 age, year, drink.getname()); 23 } 24 State.setMajor(true); 25 Apartment.getInstance().getCurrentArea().removeItem(drink); 26 Hands.getInstance().tryAddItem(drink); 27 return zodebral + drink.getname() + znezapomeň; 28 } Test Opět spustíme verifikační test. Jak vidíme (viz výpis 14.10), další krok prošel a test se tentokrát zarazil na tom, že jsme ještě nedefinovali příkaz pro zavření ledničky. Pojďme to napravit. Výpis 14.10: Klíčová část hlášení o průběhu testu po dokončení definice metody waitingfortheyear(string) ve třídě Conversation; zpráva oznamuje neschopnost hry reagovat na příkaz Zavři Lednička 1 ============================================================================= 2 Při testu následujícího, tj. 22. kroku: Zavři Lednička 3 se objevila CHYBA 4 ============================================================================= 5 Začátky očekávané a obdržené zprávy nesouhlasí. 6 Čekám: «Úspěšně jste zavřel(a) ledničku.» 7 Přišlo: «Tento příkaz neznám. 8 Chcete-li poradit, zadejte příkaz?» 9 Ověřený počátek zprávy: Odchylka na pozici 0 12 očekávám kód 218 znak «Ú» 13 obdržel jsem kód 84 znak «T» 14 ============================================================================= 14.8 Příkaz pro zavření ledničky ActionCLose S příkazem pro zavření ledničky je to obdobné jako s příkazem pro její otevření. Opět je třeba definovat novou třídu a nechat správce akcí vytvořit její instanci. Opět bude potřeba prověřit splnění některých podmínek (je zadáno, co se má zavřít, je to lednička a je v danou chvíli aktuálním prostorem) a opět bude potřeba Strana 312 z 351

313 Kapitola 14: Realizace rozhovoru 313 definovat několik nových textových konstant zastupujících texty neuvedené ve správci scénářů, texty, jimiž program reaguje na špatně zadané příkazy. Předpokládám, že v současné době jste již ve stavu, kdy byste danou třídu dokázali definovat sami. Pro jistotu ale ve výpisu uvádím zdrojový kód její metody execute(string...). Výpis 14.11: Definice metody execute(string...) instancí třídy ActionCLose 1 /*************************************************************************** 2 * Zavře zadaný h-objekt a přesune hráče jako do prostoru, 3 * v němž je zavíraný h-objekt umístěn. 4 * Před tím však zkontroluje, 5 * <ul> 6 * <li>zda je zadán h-objekt, který se má otevřít,</li> 7 * <li>zda je tento h-objekt lednička,</li> 8 * <li>zda je lednička aktuálním prostorem.</li> 9 * </ul> 10 * 11 parametry Jediným povoleným parametrem je zatím lednička 12 Text zprávy vypsané po provedeni příkazu 13 */ 15 public String execute(string... arguments) 16 { 17 if (arguments.length < 2) { 18 return Texts.zNEVÍM_CO_ZAVŘÍT; 19 } 20 String roomname = arguments[1]; 21 Apartment apartment = Apartment.getInstance(); 22 Room currentroom = apartment.getcurrentarea(); 23 if (! currentroom.getname().equalsignorecase(roomname)) { 24 return Texts.zNENÍ_AKTUÁLNÍM_PROSTOREM; 25 } 26 if (! roomname.equalsignorecase(texts.lednička)) { 27 return Texts.zZAVŘÍT_LZE_JEN_LEDNIČKU; 28 } 29 Room newcurrent = apartment.getoroom(texts.kuchyň).get(); 30 Apartment.getInstance().setCurrentArea(newCurrent); 31 return Texts.zZAVŘEL_LEDNIČKU; 32 } Test Po spuštění pravidelného testu se tentokrát dozvíme, že sice všechny scénáře prošly, ale že se testovacímu programu na naší aplikaci stále něco nelíbí. Výpis obsahující konec zprávy a průběhu testu oznamuje, že v naší aplikaci stále ještě existují metody, které nejsou plnohodnotně definovány. Zprovoznění zbylých testů se ale budeme věnovat až v příští kapitole. Strana 313 z 351

314 Kapitola 14: Realizace rozhovoru 314 Výpis 14.12: Klíčová část hlášení o průběhu testu po dokončení definice třídy ActionCLose; zpráva oznamuje nedotaženost metody stop() ve třídě RUPApartmentGame 1 ============================================================================= 2 Ve vlákně Thread[main,5,main] byla vyhozena výjimka: 3 java.lang.unsupportedoperationexception: 4 Metoda ještě není hotova. 5 at eu.pedu.adventure15p.rupapartmentgame.stop(rupapartmentgame.java:195) 6 at eu.pedu.adv15p_fw.test_util.tgametests.tgameremainstester. verifyaliveness(tgameremainstester.java:87) 7 at eu.pedu.adv15p_fw.test_util.tgametests.tgameremainstester.verify( TGameRemainsTester.java:78) 8 at eu.pedu.adv15p_fw.test_util.tgametests.tgametester.testgamebyscenarios( TGameTester.java:145) 9 at eu.pedu.adv15p_fw.test_util.tgametests.tgametester.testgame( TGameTester.java:124) 10 at eu.pedu.adv15p_fw.scenario.ascenariomanager.testgame( AScenarioManager.java:411) 11 at eu.pedu.adventure15p.rupscenariomanagercon.main(rupscenariomanagercon.java:725) 14.9 Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Zopakujme si, co jsme v této kapitole dozvěděli a co jsme naprogramovali: Ke stavu vývoje projektu, který jsme shrnuli v podkapitole 14.9 Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali na straně 314, jsme přidali následující: Probrali jsme specifika zanesení rozhovoru do aplikace a naznačili metody jejich realizace. Do třídy State jsme přidali další příznaky, které sdílí několik akcí současně. Upravili jsme definici metody execute(string...) ve třídě ActionPickUp tak, aby zabezpečila, že se hra v situaci, kdy hráč chce odebrat z ledničky alkoholický nápoj a lednička jej nemá ještě prověřeného, přepne do konverzačního režimu, v němž si ověří, že hráč je již plnoletý. Definovali jsme třídu Conversation jejíž objekt je zodpovědný za řízení konverzace s hráčem. Upravili jsme definici metody executeaction(string) ve třídě AAction tak, aby v režimu konverzace automaticky předávala řízení objektu zodpovědnému za řízení této konverzace. Strana 314 z 351

315 Kapitola 14: Realizace rozhovoru 315 Ujasnili jsme si stavový diagram konverzace a požadavky na metody zabezpečující správnou reakci hry v jednotlivých etapách konverzace. Aplikovali jsme funkcionální přístup k řízení přechodu mezi stavy a zavedli atribut obsahující odkaz na metodu, která má zabezpečující správnou reakci hry v aktuálním stavu s tím, že tato metoda připraví případný přechod do dalšího stavu uložením nového odkazu do tohoto atributu. Definovali jsme třídu ActionCLose, jejíž instance je zodpovědná za správnou reakci na příkaz pro zavření ledničky. Program, jehož zdrojové kódy odpovídají výše popsanému stavu vývoje aplikace, najdete v projektu A114z_ConversationAdded. Jeho diagram tříd si můžete prohlédnout na obrázku Obrázek 14.2 Diagram tříd projektua114z_conversationadded zachycujícího stav poté, co byly definována třídy realizující podporu rozhovoru Strana 315 z 351

316 Kapitola 15: Zprovoznění zbylých testů Zprovoznění zbylých testů Kapitola 15 Zprovoznění zbylých testů Co se v kapitole naučíte Scénáře naší ukázkové hry již prošly, ale testovací program stále není spokojen. Cílem této kapitoly je doprovodit hru do stavu, kdy projdou všechny testy. V této kapitole se proto znovu objeví číslované body s doporučenými úkoly, jejichž provedení by vás mělo dovést k cíli. V této kapitole budeme pokračovat v práci s projektem obsahujícím závěrečnou podobu projektu, k níž jsme se dopracovali v předchozí kapitole, tj. s projektem A114z_ConversationAdded Metoda stop() ve třídě RUPApartmentGame Z výpisu na straně 314 jsme se dozvěděli, že naše aplikace ještě není bezchybná. Test konkrétně odhalili nedokončenou definici metody stop() ve třídě RUPApartmentGame. Jak naznačuje dokumentační komentář zkopírovaný z interfejsu IGame, tato metoda má umožnit ukončit hru z programu. Má tedy zareagovat stejně, jako akce reagující na ukončovací příkaz. Tu jsme definovali v podkapitole 12.6 Příkaz k předčasnému ukončení hry ActionExit na straně 264. Tehdy jsme vše vyřešili tak, že správce akcí, tj. objekt třídy AAction, definoval metodu stopgame(), kterou jsme z výkonné metody naší akce zavolali a která vše zabezpečila. To můžeme nyní zopakovat. Takže: 1. Upravte tělo metody stop() ve vaší třídě hry tak, aby metoda zavolala metodu stopgame() správce akcí, tj. metodu třídy AAction. Strana 316 z 351

317 Kapitola 15: Zprovoznění zbylých testů 317 Test Po spuštění ověřovacího testu se pravděpodobně posuneme dál (pravděpodobně proto, že sice žádný zjevný postup není vidět, ale testovací program si již na nedefinovanost metody nestěžuje), ale testovací program stále není spokojen. Nyní si stěžuje na to (viz výpis 15.1), že třída hry ještě nemá plnohodnotně implementovanou metodu getbasicactions(). Výpis 15.1: Klíčová část hlášení o průběhu testu po úpravě definice metody stop() ve třídě RUPApartmentGame; zpráva oznamuje neplnohodnotnou definici metody getbasicactions() 1 ============================================================================= 2 Ve vlákně Thread[main,5,main] byla vyhozena výjimka: 3 eu.pedu.adv15p_fw.test_util.common.testexception: 4 Třída hry ještě nemá plnohodnotně implementovanou metodu getbasicactions() 5 at eu.pedu.adv15p_fw.test_util.common.atester.error(atester.java:153) 6 at eu.pedu.adv15p_fw.test_util.tgametests.tgameremainstester.verifygetter( TGameRemainsTester.java:149) 7 at eu.pedu.adv15p_fw.test_util.tgametests.tgameremainstester.verifygetters( TGameRemainsTester.java:129) 8 at eu.pedu.adv15p_fw.test_util.tgametests.tgameremainstester.verify(tgameremainstester.java:80) 9 at eu.pedu.adv15p_fw.test_util.tgametests.tgametester.testgamebyscenarios(tgametester.java:145) 10 at eu.pedu.adv15p_fw.test_util.tgametests.tgametester.testgame(tgametester.java:124) 11 at eu.pedu.adv15p_fw.scenario.ascenariomanager.testgame(ascenariomanager.java:411) 12 at eu.pedu.adventure15p.rupscenariomanagercon.main(rupscenariomanagercon.java:725) 15.2 Metoda getbasicactions() ve třídě RUPApartmentGame Podíváme-li se do třídy hry na definici této metody, zjistíme, že testovací program má (bohužel) opět pravdu. Tělo metody nic neřeší a pouze vyhazuje výjimku UnsupportedOperationException. Pojďme to napravit. O této metodě jsme se bavili v pasáži Povinné akce getbasicactions na straně 139, kde jsme si ve výpisu 7.1 ukazovali hlavičku a dokumentační komentář konstruktoru třídy BasicActions, jejíž instanci má metody vracet. Zde jsme se také dozvěděli, že instance třídy BasicActions jsou přepravky uchovávající názvy akcí, které musí být implementovány ve všech hrách, avšak každá hra je může mít pojmenované po svém. Víme tedy vše, co potřebujeme vědět pro to, abychom mohli definovat tělo této metody. Zbývá jediné rozhodnutí: zda dáme přednost tomu, abychom při každém volání této metody znovu vytvářeli požadovanou přepravku, anebo zda tuto pře- Strana 317 z 351

318 Kapitola 15: Zprovoznění zbylých testů 318 pravku vytvoříme na jenom počátku, uložíme ji do nějaké konstanty a metoda bude pokaždé vracet hodnotu této konstanty. Mně osobně se víc líbí druhé řešení, zvlášť když si uvědomíme, že danou konstantu můžeme definovat jako konstantu třídy. Tuto moji preferenci respektuje i definice ve výpisu 15.2 a i následující návod: Výpis 15.2: Definice konstanty basicactions a metody getbasicactions() ve třídě RUPApartmentGame 1 //== CONSTANT CLASS ATTRIBUTES ================================================= 2 3 /** Přepravka uchovávající názvy povinných akcí. */ 4 private static final BasicActions BASIC_ACTIONS = 5 new BasicActions(Texts.pJDI, Texts.pPOLOŽ, Texts.pVEZMI, 6 Texts.pHELP, Texts.pKONEC); //== INSTANCE GETTERS AND SETTERS ============================================== /*************************************************************************** 13 * Vrátí odkaz na přepravku s názvy povinných akcí, tj. akcí pro 14 * <ul> 15 * <li>přesun hráče do jiného prostoru,</li> 16 * <li>zvednutí h-objektu (odebrání z prostoru a vložení do batohu),</li> 17 * <li>položení h-objektu (odebrání z batohu a vložení do prostoru),</li> 18 * <li>vyvolání nápovědy,</li> 19 * <li>okamžité ukončení hry.</li> 20 * </ul> 21 * 22 Přepravka názvy povinných akcí 23 */ 25 public BasicActions getbasicactions() 26 { 27 return BASIC_ACTIONS; 28 } 2. Definujte ve své třídě hry soukromou statickou konstantu typu BasicActions, kterou inicializujete zavoláním konstruktoru, jemuž v parametrech předáte názvy příslušných akcí ve vaší hře. 3. Upravte ve své třídě hry tělo metody getbasicactions() tak, aby metoda vracela hodnotu konstanty definované v minulém kroku. Strana 318 z 351

319 Kapitola 15: Zprovoznění zbylých testů 319 Protože hra je definována podle vzoru Jedináček (Singleton), tak bych rád připomněl, že je-li odkaz na jedináčka definován ve statickém konstantním atributu, bývá dobré dodržet pravidlo, že tento atribut je inicializován jako poslední. Jsou-li ve třídě pouze statické konstanty, stačí jej uvést jako na konci, jsou-li ve třídě i inicializované statické proměnné, je vhodné definovat statický inicializační blok, který končí inicializací atributu jedináčka. V naší ukázkové třídě jsou definovány pouze statické konstanty (a předpokládám, že je tomu tak i u vás), takže mám(e) život jednodušší, protože stačí dohlédnout na to, aby byl atribut deklarován jako poslední a inicializovat jej hned v deklaraci. Test 4. Spusťte pravidelný test. Pokud jste postupovali podle mých doporučení, zvládli definici svých nestandardních akcí a nezanesli do svého programu nějakou dodatečnou chybu, měli byste konečně vidět to, na co jste celou dobu čekali: zprávu o úspěšném provedení všech testů, která končí větou: Podle výsledků testu program pravděpodobně neobsahuje žádné závažné chyby Program hry je tedy prověřen (neříkám, že je bez chyby, ale prošel povinnými testy) a můžete jej začít vybavovat uživatelským rozhraním, aby si vaši hru mohl zahrát nejenom testovací program, ale i řadový uživatel. Právě tvorbě jednoduchého uživatelského rozhraní bude věnována následující kapitola Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Zopakujme si, co jsme v této kapitole dozvěděli a co jsme naprogramovali: Ke stavu vývoje projektu, který jsme shrnuli v podkapitole Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali na straně 296, jsme přidali následující:.dokončili jsme definici metody stop() ve třídě RUPApartmentGame..Dokončili jsme definici metody getbasicactions() ve třídě RUPApartmentGame. Zprovoznili jsme všechny testy. Strana 319 z 351

320 Kapitola 15: Zprovoznění zbylých testů 320 Program, jehož zdrojové kódy odpovídají výše popsanému stavu vývoje aplikace, najdete v projektu A115z_AllTestsRun. Diagram tříd se nezměnil, a proto vám jej zde neukazuji. Strana 320 z 351

321 Kapitola 16: Definice uživatelského rozhraní Definice uživatelského rozhraní Kapitola 16 Definice uživatelského rozhraní Co se v kapitole naučíte V této kapitole přidáme k naší hře uživatelské rozhraní, aby si ji mohl zahrát i běžný uživatel a ne jenom náš testovací program. Vyzkoušíme přitom několik verzí rozhraní. V této kapitole se vaše řešení nebudou lišit. Teoreticky byste je mohli jednoduše opsat. Pak ovšem hrozí, že až budete dělat obhajobu své práce a dostanete za úkol něco drobně modifikovat, budete bloudit. Na druhou stranu jsou ale tak jednoduchá, že by pro vás mělo být výhodnější se je pokusit pochopit, než je jenom tupě opsat. Optimální je přečíst si výklad, zavřít text a pokusit se definovat požadovaný kód podle svého. V této kapitole budeme pokračovat v práci s projektem obsahujícím závěrečnou podobu projektu, k níž jsme se dopracovali v předchozí kapitole, tj. s projektem A115z_AllTestsRun Co je třeba navrhnout Základní program hry jsme rozchodili. Prozatím jej však jsou schopny spouštět pouze testy. K tomu, aby si naši hru mohl zahrát i řadový uživatel, potřebujeme definovat ještě nějaké uživatelské rozhraní, prostřednictvím nějž bude uživatel s hrou komunikovat. V současné době používá většina aplikací různá důmyslná grafická uživatelská rozhraní, která se snaží komunikaci s programem co nejvíce usnadnit. K jejich Strana 321 z 351

322 Kapitola 16: Definice uživatelského rozhraní 322 návrhu jsou však potřeba jisté znalosti a zkušenosti, a proto se touto cestou prozatím nevydáme to až v příštím dílu. Naším cílem bude vytvoření co nejjednoduššího rozhraní, které můžeme následně vylepšovat tak, abychom dosáhli s minimální námahou maximální funkčnost. Při návrhu uživatelského rozhraní půjdeme dokonce ještě dál: navrhneme je tak, aby toto rozhraní bylo schopno zprostředkovat komunikaci s libovolnou hrou vyhovujícím požadavkům rámce, tj. hrou, jejíž mateřská třída implementuje interfejs eu.pedu.adv15p_fw.game_txt.igame Úprava rozdělení do balíčků Doposud jsme měli všechny třídy umístěné v jednom balíčku. Tato koncepce vyhovovala do okamžiku, dokud definované třídy a jejich instance vzájemně spolupracovaly. Třídy uživatelského rozhraní, které se chystáme definovat nyní, ale už komunikaci se všemi třídami hry tak bezpodmínečně nevyžadují. Má-li být uživatelské rozhraní opravdu tak univerzální, jak jsem před chvílí naznačoval, mělo by mu stačit, bude-li mít přístup ke třídě hry, případně alespoň k její tovární třídě. Proto: 1. Ve svém balíčku definujte podbalíček game. 2. Do tohoto balíčku přemístěte všechny doposud definované třídy s výjimkou třídy GSMFactory. Její instance nám v případě potřeby dokáže dodat instanci hry i instanci správce scénářů a v budoucnu i instanci uživatelského rozhraní. 3. Vedle něj vytvořte balíček textui, do nějž budete ukládat třídy definující uživatelské rozhraní. 4. Definujte ve svém balíčku (tj. v rodičovském balíčku podbalíčků game a textui) hlavní třídu aplikace (tj. třídu s veřejnou statickou metodou main(string[])), která bude v budoucnu spouštět vaše preferované uživatelské rozhraní. Název třídy nechám na vás, ale já jsem si zvykl nazývat hlavní třídu aplikace standardně Main. Aspoň vždy rychle poznám, která to je. Protože ale tentokrát vím, že za touto publikací budou následovat další, které budou také chtít svoji třídu Main, doplním do názvu příponu a třídu nazvu MainTXT, protože se jedná o hlavní třídu aplikace ve stádiu, kdy nás zajímá pouze textové uživatelské rozhraní. Strana 322 z 351

323 Kapitola 16: Definice uživatelského rozhraní Uživatelské rozhraní využívající služeb třídy javax.swing.joptionpane Začneme třídou využívající služeb třídy javax.swing.joptionpane, která nabízí několik metod umožňujících velmi jednoduše definovat uživatelské rozhraní využívající jednoduchá dialogová okna. 5. V balíčku textui definujme novou třídu a nazvěte ji UIA_JOptionPane. Písmeno A je v názvu proto, že je to první ze tříd, které zde budeme definovat. Názvy dalších třídy pak budou mít za úvodním UI další písmeny abecedy. Třídy uživatelského rozhraní mají podle požadavků frameworku implementovat interfejs eu.pedu.adv15p_fw.game_txt.iui definující požadavky na takovouto třídu. Proto: 6. Upravte definici třídy tak, aby (prozatím formálně) implementovala výše uvedený interfejs. Interfejs IUI vyžaduje implementaci dvou metod: void startgame() Spouští implicitní hru, což by měla být hra definovaná tvůrcem daného rozhraní. void startgame(igame game) Spouští hru zadanou v parametru. Prostřednictvím implementace druhé z metod dané uživatelské rozhraní dokazuje, že je schopno pracovat s libovolnou hrou vyhovující rámci, tj. implementující interfejs IGame. První metoda pak umožňuje, aby uživatel, jehož hra má některé dodatečné vlastnosti přesahující požadavky rámce, mohl definovat své uživatelské rozhraní tak, aby tyto požadavky dokázalo využít. Navíc zjednodušuje spouštění této hry. Definice metody startgame() Protože ukázková hra nemá žádné speciální vlastnosti, pro jejichž plné využití by bylo potřeba mít nějaké zvláštně upravené uživatelské rozhraní, můžeme bezparametrickou verzi spouštěcí metody definovat jednoduše tak, že zavoláme jednoparametrickou verzi, které v parametru předáme instanci hry. Takováto definice by mohla vypadat např. podle výpisu Pokud jste vaší hru vybavili nějakými nadstandardními vlastnostmi přesahujícími požadavky frameworku, můžete tuto metodu definovat tak, aby spustila váš program v režimu, který umožní ony nestandardní vlastnosti využít. Patří-li vaše Strana 323 z 351

324 Kapitola 16: Definice uživatelského rozhraní 324 hra do takovéto skupiny, lze očekávat, že vám nebude dělat problém definovat tělo metody startgame() s potřebnou rafinovaností. 7. Definujte tělo metody startgame() tak, aby optimálně spouštěla vaši hru. Výpis 16.1: Definice metody startgame() ve třídě UIA_JOptionPane 1 /*************************************************************************** 2 * Spustí komunikaci mezi implicitní hrou 3 * a danou instancí uživatelského rozhraní. 4 */ 6 public void startgame() 7 { 8 startgame(rupapartmentgame.getinstance()); 9 } Metody třídy JOptionPane Definice jednoparametrické metody bude (alespoň pro ty, jejichž hra nevybočuje z požadavků rámce) o maličko složitější, ale ne o moc. Pro komunikaci s uživatelem můžeme použít statickou metodu showinputdialog(component parentcomponent, Object message) Jejím prvním parametrem je komponenta (např. okno), nad jejímž středem otevře dialogové okno (tato komponenta je označována jako rodičovská). Druhým parametrem metody je text, který uživateli většinou vysvětluje, co má zadat. Metoda otevře dialogové okno se vstupním textovým polem (viz obrázek 16.1), do kterého uživatele zadá text, který pak metoda vrátí. Obrázek 16.1 Dialogové okno s úvodní zprávou hry čekající na zadání příkazu Je-li prvním parametrem metody hodnota null, okno se otevře ve středu primární obrazovky. Většině uživatelů to stačí, ale pokud potřebujete otevřít dialogové okno na nějakém specifickém místě, zejména pak na jiném než primárním monitoru, musíte první parametr zadat. Nejjednodušší je vyrobit prázdné okno, umístit je do požadované pozice a zobrazit (nebude-li zobrazené, naše dialogové okno je bude ignorovat). Předáme-li Strana 324 z 351

325 Kapitola 16: Definice uživatelského rozhraní 325 toto okno v prvním parametru metody showinputdialog, zobrazí se požadované okno nad tímto pomocným oknem a zakryje je, takže pomocné okno nebude rušit. (Tedy alespoň do doby, dokud ono dialogové okno někam neodsunete.) Pro vytvoření takovéhoto pomocného okna můžeme použít kód: Component parent = new JFrame(); parent.setlocation(100, 100); parent.setvisible(true); Při používání metod této třídy ale nesmíte zapomenout na to, že jakmile otevřete nějaké okno, spustí se samostatná obsluha grafického uživatelského rozhraní, která se neukončí automaticky po ukončení metody main(string). Program proto musíte explicitně ukončit, např. zavoláním metody System.exit(int), které předáte v parametru chybový kód. Když program končí bez chyby, předáte nulu. Definice metody startgame(igame) Pojďme se nyní podívat, jak naprogramovat vlastní komunikaci. Jak jistě odhadnete, budeme uživateli stále kolem dokola zobrazovat dialogové okno se zprávou hry, a budeme po něm chtít, aby v reakci na tuto zprávu zadal další příkaz. Tento příkaz předáme hře (zavoláme její metodu execute(string)), a opět otevřeme dialogové okno s její odpovědí. Tak to budeme dělat, dokud hra neskončí, což zjistíme zavoláním její metody isalive(). Jednou z možností, jak naprogramovat takovouto opakovanou činnost, je definovat cyklus: do { command = JOptionPane.showInputDialog(parent, answer); answer = game.executecommand(command); } while (game.isalive()); Otázkou zůstává, jak zjistit tu první odpověď hry. Jak si ale možná vybavíte, hra se startuje prázdným příkazem a v odpovědi na tento příkaz vrátí úvodní vítací sekvenci. Druhým problémem je, že takto ukončená hra se s hráčem vůbec nerozloučí a nepogratuluje mu k případnému vítězství. Poslední odpověď hry totiž zůstane nezpracovaná. Tak tomu ale nemusí být. Stačí přidat za cyklus volání statické metody JOptionPane.showMessageDialog(Component parentcomponent, Object message) která pouze zobrazí zadanou zprávu a nebude již po uživateli chtít žádnou reakci kromě stisknutí potvrzovacího tlačítka OK. Strana 325 z 351

326 Kapitola 16: Definice uživatelského rozhraní 326 Zdrojový kód metody startgame(igame) respektující všechny výše uvedené úvahy v podobě, v jaké je najdete v ukázkové aplikaci, si můžete prohlédnout ve výpisu Výpis 16.2: Definice metody startgame(igame) ve třídě UIA_JOptionPane 1 /*************************************************************************** 2 * Spustí komunikaci mezi zadanou hrou a danou instancí 3 * mající na starosti komunikaci s uživatelem. 4 * 5 game Hra, kterou ma dané UI spustit 6 */ 8 public void startgame(igame game) 9 { 10 Component parent = new JFrame(); 11 parent.setlocation(100, 100); 12 parent.setvisible(true); String command; 15 String answer = game.executecommand(""); 16 do { 17 command = JOptionPane.showInputDialog(parent, answer); 18 answer = game.executecommand(command); 19 } while (game.isalive()); 20 JOptionPane.showMessageDialog(parent, answer); 21 System.exit(0); 22 } 8. Inspirujte se předchozími úvahami a výpisem 16.2 a definujte ve své třídě tělo metody startgame(igame) využívající metody třídy JOptionPane. Test 9. Do hlavní metody aplikace vložte deklaraci proměnné ui typu IUI. 10. Za deklaraci vložte příkaz, který inicializuje proměnnou ui nově vytvořenou instancí třídy UIA_JOptionPane. 11. Za tuto inicializaci vložte příkaz, který zavolá metodu ui.startgame(). 12. Ověřte, že pomocí právě definovaného uživatelského rozhraní je možno vaši hru hrát. Strana 326 z 351

327 Kapitola 16: Definice uživatelského rozhraní Uživatelské rozhraní komunikující prostřednictvím standardního vstupu a výstupu Právě naprogramované uživatelské rozhraní má jednu nevýhodu: nezobrazuje historii našich akcí. Tu bychom mohli zobrazit, kdybychom s uživatelem komunikovali prostřednictvím standardního vstupu a výstupu. Nevýhody používání standardního vstupu a výstupu Standardní vstup a výstup má však jednu nevýhodu: nelze jej používat nezávisle na platformě. Tuto nezávislost narušuje operační systém Windows, který používá v různých režimech různá kódování. Řada programů je schopna pracovat s univerzálním kódováním UTF-8, ale standardní vstup a výstup mezi ně nepatří. Budete-li program spouštět pod nějakým vývojovým prostředím, tak to je vám schopno nabídnout vlastní verzi standardního vstupu a výstupu, která vás od těchto problémů odstíní. Budete-li však pracovat v příkazovém řádku systému Windows, budete muset tento problém vyřešit. V dalším textu předpokládám, že můžete hrou spouštět pod nějakým IDE. Třída java.util.scanner Přímé čtení ze standardního vstupu je poměrně náročné, protože je třeba jej číst znak za znakem a definovat vlastní zpracování přečtených znaků (např. zjistit, kdy uživatel ukončil řádek, aby jej bylo možno zpracovat). Aby byly vývojáři od těchto starostí do jisté míry osvobození, zavedla Java 5 třídu Scanner, jejíž instance za nás čtení ze standardního vstupu do jisté míry předzpracují. My budeme ze třídy Scanner využívat pouze jeden z jejích deseti konstruktorů a jednu z jejích 65 metod. Potřebnou instanci budeme vytvářet prostřednictvím konstruktoru Scanner(InputStream source) kterému jako jeho parametr předáme objekt standardního vstupu System.in. Pro čtení uživatelových příkazů pak budeme používat metodu String nextline() která přečte zbytek aktuálního řádku bez znaku ukončení řádku. Protože my nebudeme řádky zpracovávat nijak jinak, přečte nám tato metoda řádek vždy celý. Strana 327 z 351

328 Kapitola 16: Definice uživatelského rozhraní 328 Vytvoření třídy pro konzolový vstup 13. Zkopírujte třídu UIA_JOptionPane. 14. Kopii pojmenujte UIB_Scanner. Definici metody startgame() ponecháme beze změny a soustředíme se na metodu startgame(igame). Kdybychom převzali koncepci ze třídy UIA_JOptionPane, vypadala by definice metody nejspíš podle výpisu 16.3: Výpis 16.3: Definice metody startgame(igame) ve třídě UIB_Scanner 2 public void startgame(igame game) 3 { 4 Scanner scanner = new Scanner(System.in); 5 String command; 6 String answer = game.executecommand(""); 7 System.out.println(answer); 8 do { 9 command = scanner.nextline(); 10 answer = game.executecommand(command); 11 System.out.println(answer); 12 } while (game.isalive()); 13 } Tady by nám ale mohlo vadit, že se na řádcích 6 a 10 vyskytuje stejný příkaz a stejně tak na řádcích 7 a 11. Kdybychom cyklus upravili podle výpisu 16.4, tak by tato duplikace řádků odpadla. Vyberte si, které řešení je vám sympatičtější. Výpis 16.4: Definice metody startgame(igame) ve třídě UIB_Scanner 2 public void startgame(igame game) 3 { 4 Scanner scanner = new Scanner(System.in); 5 String command = ""; 6 String answer; 7 for(;;) { 8 answer = game.executecommand(command); 9 System.out.println(answer); 10 if (! game.isalive()) { break; } // > 11 command = scanner.nextline(); 12 } 13 } 15. Upravte definici metody startgame(igame) podle svých preferencí. Strana 328 z 351

329 Kapitola 16: Definice uživatelského rozhraní 329 Nyní jistě někoho napadlo, že obdobná duplikace příkazů pro získání odpovědi je i ve výpisu To je pravda, ale tam se opakuje pouze jeden příkaz, kdežto tady se opakovaly již dva. Nicméně zájemci samozřejmě mohou příslušně upravit i kód z výpisu Test 16. V hlavní metodě zakomentujte příkaz vytvořený podle bodu 10 na straně Za zakomentovaný příkaz vložte příkaz, který inicializuje proměnnou ui nově vytvořenou instancí třídy vytvoří instanci třídy UIB_Scanner. 18. Ověřte, že pomocí právě definovaného uživatelského rozhraní je opět možno vaši hru hrát Zobecnění uvedených řešení Další podkapitoly jsou již určeny pouze pro zájemce, kteří chtějí poznat některé možnosti využití objektových vlastností při sjednocování různých verzí implementace společné funkcionality. Když porovnáme řešení prostřednictvím metod třídy JOptionPane s řešeními využívajícími instanci třídy Scanner, vidíme, že mají mnoho společného. Jejich podobnost přímo vyzývá k tomu, abychom pro ně definovali společnou kostru, do níž bychom pak dosazovali konkrétní řešení. Uvedená řešení se vlastně liší pouze v inicializaci uživatelského rozhraní, která předchází vlastnímu spuštění hry, a potom ve způsobu, jakým získávají od uživatele další příkaz a způsobu, jak uživateli předávají závěrečnou zprávu hry. Kdybychom proto definovali interfejs deklarující tyto metody, pak bychom mohli definovat pro každé z uvedených řešení třídu, která tento interfejs implementuje, tak bychom mohli definovat univerzální uživatelské rozhraní, do kterého bychom pouze dosadili danou třídu. Pojďme si to ukázat. Interfejs IGamePlayer Před chvílí jsem říkal, že se jednotlivá řešení liší ve třech detailech: inicializaci, získání dalšího příkazu a zobrazení závěrečné zprávy. První z metod ale v chys- Strana 329 z 351

330 Kapitola 16: Definice uživatelského rozhraní 330 taném interfejsu deklarovat nemusíme, protože inicializaci si může vzít na starost konstruktor. Budeme-li chtít vše odstartovat znovu, vytvoříme prostě další instanci. Interfejs deklarující zbylé dvě metody bychom pak mohli definovat podle výpisu Výpis 16.5: Definice interfejsu IGamePlayer 1 /******************************************************************************* 2 * Instance interfejsu {@code IGamePlayer} definují variantní části 3 * univerzálního textového uživatelského rozhraní 4 * pro hraní textových konverzačních her. 5 */ 6 public interface IGamePlayer 7 { 8 //== OTHER ABSTRACT METHODS ==================================================== 9 10 /*************************************************************************** 11 * Pošle uživateli zadanou zprávu a převezme od něj další příkaz. 12 * 13 message Posílaná zpráva 14 Uživatelem zadaný příkaz 15 */ 16 public String askcommand (String message); /*************************************************************************** 20 * Pošle uživateli zadanou zprávu. 21 * 22 message Posílaná zpráva 23 */ 24 public void sendmessage(string message); 25 } Současně bychom mohli definovat třídu UIC_GamePlayer, jejíž konstruktor by jako parametr přebral instanci interfejsu IGamePlayer a v příslušných bodech svého algoritmu by pak použil metody této instance. Definice výkonných tříd implementujících interfejs IGamePlayer budou tak jednoduché, že bychom je mohli definovat jako interní třídy. Jako interní bychom nakonec mohli definovat i interfejs IGamePlayer, jehož jediným účelem je definovat společný rodičovský typ tříd, jejichž instance specifikují druh komunikace s uživatelem. Hlavní metoda umožňující volbu použitého rozhraní Když ale nyní můžeme zvolit způsob realizace uživatelského rozhraní použitého v metodě řídící komunikaci mezi uživatelem a hrou, tak také musíme uživateli umožnit, aby si vybral, jakému způsobu dá přednost. K tomu je nejvýhodnější použít parametry příkazového řádku, kterým se spouští celá aplikace. Strana 330 z 351

331 Kapitola 16: Definice uživatelského rozhraní 331 Definujeme proto hodnoty parametrů, které budou specifikovat zvolený způsob, a současně zvolíme způsob, který se použije v případě, když uživatel žádný preferovaný způsob nezadá. Můžeme se rozhodnout třeba následovně: Bude-li mít první parametr příkazového řádku parametr hodnotu con, použije se komunikace využívající standardní vstup a výstup (konzoli) a služeb třídy Scanner. Bude-li mít první parametr příkazového řádku parametr hodnotu jop, použije se komunikace využívající služeb třídy JOptionPain. Protože je druhá možnost universálnější (nemusíme řešit problémy s kódováním), zvolíme ji současně jako implicitní. Nebude-li proto mít první parametr žádnou z výše uvedených hodnot, použije se služeb třídy JOptionPain. Příslušné rozhodování o tom, který způsob komunikace s uživatele zvolit, pak bude mít na starosti metoda main(string[]). Výsledná definice třídy UIC_GamePlayer Na základě předchozích úvah bychom již měli znát vše potřebně, abychom definovali třídu UIC_GamePlayer s hlavní metodou umožňující výběr použitého uživatelského rozhraní. Možnou definici této třídy si můžete prohlédnout ve výpisu Jak z výpisu vidíte, vzhledem k jednoduchosti tříd definujících jednotlivé způsoby komunikace s uživatelem, jsou tyto třídy společně s jimi implementovaným interfejsem definovány jako interní (přesněji vnořené). Výpis 16.6: Definice třídy UIC_GamePlayer 1 /******************************************************************************* 2 * Instance třídy {@code UIC_GamePlayer} realizují uživatelské rozhraní, 3 * kterému lze zadat objekt typu {@link IGamePlayer}, jehož prostřednictvím 4 * bude program komunikovat s uživatelem. 5 */ 6 public class UIC_GamePlayer implements IUI 7 { 8 //== CONSTANT CLASS ATTRIBUTES ================================================= 9 //== VARIABLE CLASS ATTRIBUTES ================================================= //############################################################################## 14 //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== 15 //== CLASS GETTERS AND SETTERS ================================================= 16 //== OTHER NON-PRIVATE CLASS METHODS =========================================== 17 //== PRIVATE AND AUXILIARY CLASS METHODS ======================================= Strana 331 z 351

332 Kapitola 16: Definice uživatelského rozhraní //############################################################################## 22 //== CONSTANT INSTANCE ATTRIBUTES ============================================== /** Objekt specifikující některé detaily konverzace. */ 25 private final IGamePlayer player; //== VARIABLE INSTANCE ATTRIBUTES ============================================== //############################################################################## 34 //== CONSTUCTORS AND FACTORY METHODS =========================================== /*************************************************************************** 37 * Vytvoří instanci využívající pro řešení některých detailů zadaný objekt. 38 * 39 player Objekt pro řešení některých detailů 40 */ 41 public UIC_GamePlayer(IGamePlayer player) 42 { 43 this.player = player; 44 } //== ABSTRACT METHODS ========================================================== 49 //== INSTANCE GETTERS AND SETTERS ============================================== 50 //== OTHER NON-PRIVATE INSTANCE METHODS ======================================== /*************************************************************************** 53 * Spustí komunikaci mezi implicitní hrou 54 * a danou instancí uživatelského rozhraní. 55 */ 57 public void startgame() 58 { 59 startgame(rupapartmentgame.getinstance()); 60 } /*************************************************************************** 64 * Spustí komunikaci mezi zadanou hrou a danou instancí 65 * mající na starosti komunikaci s uživatelem. 66 * 67 game Hra, kterou ma dané UI spustit 68 */ Strana 332 z 351

333 Kapitola 16: Definice uživatelského rozhraní public void startgame(igame game) 71 { 72 String command = ""; 73 String answer; 74 for(;;) { 75 answer = game.executecommand(command); 76 if (! game.isalive()) { break; } // > 77 command = player.askcommand(answer); 78 } 79 player.sendmessage(answer); 80 } //== PRIVATE AND AUXILIARY INSTANCE METHODS ==================================== //############################################################################## 89 //== NESTED DATA TYPES ========================================================= /*************************************************************************** 92 * Instance interfejsu {@code IGamePlayer} definují variantní části 93 * univerzálního textového uživatelského rozhraní 94 * pro hraní textových konverzačních her. 95 */ 96 public interface IGamePlayer 97 { 98 /*********************************************************************** 99 * Pošle uživateli zadanou zprávu a převezme od něj další příkaz. 100 * 101 message Posílaná zpráva 102 Uživatelem zadaný příkaz 103 */ 104 public String askcommand (String message); /*********************************************************************** 108 * Pošle uživateli zadanou zprávu. 109 * 110 message Posílaná zpráva 111 */ 112 public void sendmessage(string message); 113 } //////////////////////////////////////////////////////////////////////////////// 118 //////////////////////////////////////////////////////////////////////////////// /*************************************************************************** 121 * Instance třídy {@code ByJOptionPane} zprostředkovávají komunikaci Strana 333 z 351

334 Kapitola 16: Definice uživatelského rozhraní * s uživatelem prostřednictvím statických metod třídy {@link JOptionPane}. 123 */ 124 public static class ByJOptionPane implements IGamePlayer 125 { 126 final Component PARENT; /** Vytvoří novou instanci. Při té příležitosti vytvoří rodičovskou 129 * komponentu definující umístění dialogových oken. */ 130 ByJOptionPane() { 131 PARENT = new JFrame(); 132 PARENT.setLocation(100, 100); 133 PARENT.setVisible(true); 134 } /** {@inheritdoc} */ public String askcommand(string message) { 138 return JOptionPane.showInputDialog(PARENT, message); 139 } /** {@inheritdoc} */ public void sendmessage(string message) { 143 JOptionPane.showMessageDialog(PARENT, message); 144 } 145 } //////////////////////////////////////////////////////////////////////////////// 150 //////////////////////////////////////////////////////////////////////////////// /*************************************************************************** 153 * Instance třídy {@code ByScanner} zprostředkovávají komunikaci 154 * s uživatelem prostřednictvím standardního (konzolového) vstupu a výstupu, 155 * přičemž vstup je zabalen do instance třídy {@link Scanner}. 156 */ 157 public static class ByScanner implements IGamePlayer 158 { 159 Scanner scanner = new Scanner(System.in); /** {@inheritdoc} */ public String askcommand(string message) { 163 sendmessage(message); 164 return scanner.nextline(); 165 } /** {@inheritdoc} */ public void sendmessage(string message) { 169 System.out.println(message); 170 } 171 } Strana 334 z 351

335 Kapitola 16: Definice uživatelského rozhraní //############################################################################## 176 //== MAIN METHOD =============================================================== /*************************************************************************** 179 * Metoda spouštějící hru {@link RUPApartmentGame} umožňující zadat 180 * prostřednictvím parametrů příkazového řádku, 181 * zda bude použito uživatelském rozhraním využívající služeb 182 * třídy {@link JOptionPane} nebo standardního výstupu a 183 * standardního vstupu zabaleného do instance třídy {@link Scanner}. 184 * 185 args Parametry příkazového řádku 186 */ 187 public static void main(string[] args) 188 { 189 IGamePlayer gameplayer; 190 gameplayer = ((args.length < 1) (! args[0].equals("-con"))) 191? new ByJOptionPane() 192 : new ByScanner(); 193 new UIC_GamePlayer(gamePlayer).startGame(); 194 System.exit(0); 195 } 196 } 16.6 Další zobecnění uživatelského rozhraní Předchozí řešení bychom mohli dále zobecnit a umožnit hráči, aby si po skončení hry vybral, jestli si ji nebude chtít zahrát ještě jednou. Kdybychom rozšířili definici interfejsu o metodu zprostředkovávající tento výběr. Implementovaný interfejs IUI tuto možnost nezmiňuje, takže by se implementace jeho metod neměla měnit. Přesněji: neměl by se měnit kontrakt definující jejich chování. To nám otevírá možnost, abychom vylepšenou třídu definovali jako potomka té původní a v tomto potomkovi definovali pouze přidané členy. Definujeme proto třídu UID_Multiplayer, která bude potomkem naší předchozí třídy UIC_GamePlayer a bude k ní pouze přidávat členy týkající se rozšíření možností instancí nové třídy. Především v ní definujeme interfejs IGameMultiplayer, který bude potomkem obdobného interfejsu rodičovské třídy a který ke dvěma zděděným metodám přidá deklaraci metody wantcontinue() získávající hráčovu odpověď na otázku, zda si chce zahrát ještě jednou. Současně v ní definujeme interní třídy ByJOptionPane a ByScanner, které budou potomky svých stejnojmenných protějšků a budou implementovat vylepšený interfejs IGameMultiplayer jinými slovy přidají definici třetí metody. Strana 335 z 351

336 Kapitola 16: Definice uživatelského rozhraní 336 Ve třídě UID_Multiplayer pak definujeme instanční metodu multistartgame(), která odstartuje hru, po jejím ukončení se zeptá na ochotu k nové hře a v případě kladné odezvy spustí hru znovu. Předchozí úvahy vedou k definici dceřiné třídy, která od rodičovské třídy dědí nejenom metody, ale i rodiče svých interních datových typ interfejsu a dvou tříd. Takto definovanou třídu se můžete prohlédnout ve výpisu. Výpis 16.7: Definice třídy UID_Multiplayer 1 /******************************************************************************* 2 * Instance třídy {@code UID_Multiplayer} realizují uživatelské rozhraní, 3 * které rozšiřuje možnosti interfejsu typu {@link IGamePlayer} o možnost 4 * zadat po skončení hry, zda si hráč chce zahrát ještě jednou, 5 * a pokud ano, tak si také vybrat typ rozhraní, kterému bude dávat přednost. 6 * / 7 public class UID_Multiplayer extends UIC_GamePlayer 8 { 9 //== CONSTANT CLASS ATTRIBUTES ================================================= 10 //== VARIABLE CLASS ATTRIBUTES ================================================= //############################################################################## 15 //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== 16 //== CLASS GETTERS AND SETTERS ================================================= 17 //== OTHER NON-PRIVATE CLASS METHODS =========================================== 18 //== PRIVATE AND AUXILIARY CLASS METHODS ======================================= //############################################################################## 23 //== CONSTANT INSTANCE ATTRIBUTES ============================================== /** Objekt specifikující některé detaily konverzace. */ 26 private final IGameMultiplayer multiplayer; //== VARIABLE INSTANCE ATTRIBUTES ============================================== //############################################################################## 35 //== CONSTUCTORS AND FACTORY METHODS =========================================== /*************************************************************************** 38 * Vytvoří instanci využívající pro řešení některých detailů zadaný objekt. 39 * 40 multiplayer Objekt definující řešení některých detailů 41 */ 42 public UID_Multiplayer(IGameMultiplayer multiplayer) Strana 336 z 351

337 Kapitola 16: Definice uživatelského rozhraní { 44 super(multiplayer); 45 this.multiplayer = multiplayer; 46 } //== ABSTRACT METHODS ========================================================== 51 //== INSTANCE GETTERS AND SETTERS ============================================== 52 //== OTHER NON-PRIVATE INSTANCE METHODS ======================================== /*************************************************************************** 55 * Komunikuje s uživatelem prostřednictvím zadaného prostředku. 56 * Vždy spustí hru a po jejím ukončení se uživatele zeptá, 57 * chce-li si zahrát ještě jednou, a proku ano, znovu spustí hru. 58 */ 59 public void multistartgame() 60 { 61 do { 62 startgame(); 63 } while(multiplayer.wantcontinue()); 64 } //== PRIVATE AND AUXILIARY INSTANCE METHODS ==================================== //############################################################################## 73 //== NESTED DATA TYPES ========================================================= /*************************************************************************** 76 * Instance interfejsu {@code IGameMultiplayer} definují variantní části 77 * univerzálního textového uživatelského rozhraní 78 * pro hraní textových konverzačních her. 79 */ 80 public interface IGameMultiplayer extends IGamePlayer 81 { 82 /*********************************************************************** 83 * Zjistí, chce-li si uživatel zahrát ještě jednou. 84 * 85 Chce-li si uživatel znovu zahrát, vrátí {@code true}, 86 * jinak vrátí {@code false} 87 */ 88 public boolean wantcontinue(); 89 } //////////////////////////////////////////////////////////////////////////////// 94 //////////////////////////////////////////////////////////////////////////////// Strana 337 z 351

338 Kapitola 16: Definice uživatelského rozhraní /*************************************************************************** 97 * Instance třídy {@code ByJOptionPane} zprostředkovávají komunikaci 98 * s uživatelem prostřednictvím statických metod třídy {@link JOptionPane}. 99 */ 100 public static class ByJOptionPane extends UIC_GamePlayer.ByJOptionPane 101 implements IGameMultiplayer 102 { 103 /** {@inheritdoc} */ public boolean wantcontinue() 105 { 106 int answer = JOptionPane.showConfirmDialog(PARENT, 107 "Chcete si zahrát ještě jednou?"); 108 return (answer == 0); 109 } 110 } //////////////////////////////////////////////////////////////////////////////// 115 //////////////////////////////////////////////////////////////////////////////// /*************************************************************************** 118 * Instance třídy {@code ByJOptionPane} zprostředkovávají komunikaci 119 * s uživatelem prostřednictvím statických metod třídy {@link JOptionPane}. 120 */ 121 public static class ByScanner extends UIC_GamePlayer.ByScanner 122 implements IGameMultiplayer 123 { 124 /** {@inheritdoc} */ public boolean wantcontinue() { 126 String answer = askcommand("chcete si zahrát ještě jednou (A/N)?"); 127 answer = answer.trim().touppercase(); 128 return (answer.charat(0) == 'A'); 129 } 130 } //############################################################################## 135 //== MAIN METHOD =============================================================== /*************************************************************************** 138 * Metoda spouštějící hru {@link RUPApartmentGame} umožňující zadat 139 * prostřednictvím parametrů příkazového řádku, 140 * zda bude použito uživatelské rozhraní využívající služeb 141 * třídy {@link JOptionPane} nebo standardního výstupu a 142 * standardního vstupu zabaleného do instance třídy {@link Scanner}. 143 * 144 args Parametry příkazového řádku 145 */ 146 public static void main(string[] args) Strana 338 z 351

339 Kapitola 16: Definice uživatelského rozhraní { 148 IGameMultiplayer gamemultiplayer; 149 gamemultiplayer = ((args.length < 1) (! args[0].equals("-con"))) 150? new ByJOptionPane() 151 : new ByScanner(); 152 new UID_Multiplayer(gameMultiplayer).multistartGame(); 153 System.exit(0); 154 } 155 } 16.7 Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Zopakujme si, co jsme v této kapitole dozvěděli a co jsme naprogramovali. Ke stavu vývoje projektu, který jsme shrnuli v podkapitole 15.3 Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali na straně 319, jsme přidali následující: Rozdělili jsme řešení do dvou podbalíčků: Do podbalíčku game jsme přemístili všechny datové typy týkající se přímo definované hry, tj. definice všech doposud definovaných tříd s výjimkou tovární třídy GSMFactory. V podbalíčku textui jsme pak definovali všechny postupně se vylepšující třídy, jejichž instance realizují uživatelské rozhraní. Definovali jsme třídu UIA_JOptionPane, jejíž instance komunikuje s uživatelem za pomoci služeb třídy javax.swing.joptionpane, která umožňuje komunikace prostřednictvím velmi jednoduchých dialogových oken. Definovali jsme třídu UIB_Console, jejíž instance řeší komunikaci s uživatelem prostřednictvím standardního vstupu a výstupu, přičemž pro snadnější zpracování vstupu používá instance třídy Scanner. Definovali jsme třídu UIC_GamePlayer, která zobecňuje předchozí dvě řešení a definuje univerzální metodu pro komunikaci s hráčem, přičemž umožňuje specifikovat detaily této komunikace prostřednictvím objektů implementujících interní interfejs IGamePlayer. V této třídě jsme definovali interní třídy ByJOptionPane a ByScanner, jejichž instance představují ony specifikační objekty. Definovali jsme třídu UID_Multiplayer, v níž jsme předchozí řešení dále zobecnili tak, že hráč si nyní může zahrát hru znovu, aniž by musel ukončovat a znovu spouštět program. Třídu i její interní datové typy jsme definovali jako Strana 339 z 351

340 Kapitola 16: Definice uživatelského rozhraní 340 potomky datových typů z prvního zobecňovacího kroku, přičemž tito potomci pouze doplňují přidanou funkcionalitu, aniž by upravovali funkcionalitu svých předků. Diagram tříd balíčku s třídami hry se nezměnil, takže si pouze připomeneme současný stav balíčku s doposud definovanými třídami řešícími textové uživatelské rozhraní najdete jej na obrázku Obrázek 16.2 Aktuální diagram tříd balíčku s doposud definovanými třídami řešícími textové uživatelské rozhraní Program, jehož zdrojové kódy odpovídají výše popsanému stavu vývoje aplikace, najdete v projektu A116z_UserInterface. Strana 340 z 351

341 Příloha A: Používané termíny 341 A Používané termíny Příloha A Používané termíny Tato příloha obsahuje seznam termínů, které jsme si vysvětlovali v základním kurzu a které byste proto měli znát. Je určena pro ty, kteří prošli jiným základním kurzem, a pro ty, kteří již trochu zapomněli. Rozhraní (interface) je soubor charakteristik objektu, které o sobě programová entita zveřejňuje, jinými slovy: co o ní okolní program ví. Každá část programu (balíček, třída, interfejs, metoda, atribut) má svoje rozhraní. Signatura (signature) je ta část rozhraní, kterou může zkontrolovat překladač. Kontrakt (contract) je ta část rozhraní, kterou nemůže překladač zkontrolovat a za jeho dodržení je zodpovědný programátor. Kontrakt každé entity by měl být deklarován v jejím dokumentačním komentáři. Interfejs (interface type) je programová konstrukce, která v Javě původně reprezentovala rozhraní, ale od verze 8 může obsahovat i dohodnutým způsobem definovanou implementaci. Rodičovský podobjekt je ta část instance, která reprezentuje instanci rodičovské třídy. Obracíme se na ni, jako by byla hodnotou atributu nazvaného super. Metoda (method) je část kódu zodpovědná za reakci objektu na zaslanou zprávu. Metody, u nichž není při analýze zdrojového zřejmé, která z nich se při zaslání příslušné zprávy Strana 341 z 351

342 Příloha A: Používané termíny 342 vykoná (a překladač proto musí jejich volání začleňovat do kódu trochu jinak), označujeme jako virtuální (virtual method). Konstruktor třídy (class constructor) je metoda, jejíž kód je zodpovědný za vytvoření objektu třídy (viz pasáž Instance versus objekt na straně 343). Skládá se z deklarací statických atributů a statických inicializačních bloků vyvolaných v tom pořadí, v němž jsou deklarovány ve zdrojovém kódu dané třídy. Interní název této metody je <clinit>, ale ve zdrojovém kódu pod tímto názvem nevystupuje. Konstruktor instancí (instance constructor) je metoda, jejíž kód je zodpovědný za inicializaci instance své třídy, kterou mu předává operátor new v parametru this poté, co alokoval potřebnou paměť na haldě (heap) a provedl několik předinicializačních operací např. nastavil odkaz na objekt mateřské třídy vytvářené instance. Konstruktor instance sestává z deklarací instančních atributů a instančních inicializačních bloků vyvolaných v tom pořadí, v němž jsou deklarovány ve zdrojovém kódu dané třídy následovaných tělem metody označované ve zdrojovém kódu jako konstruktor. Interní název této složené metody je <init>, ale ve zdrojovém kódu vystupuje jako metoda, jejímž názvem je prázdný řetězec. Činnost konstruktoru instance končí provedením skrytého příkazu return this; který je do přeloženého bajtkódu vložen překladačem. Instance typu Xxx (Xxx instance) je objekt, který vznikl za pomoci instančního konstruktoru, případně zkopírováním nějakého objektu vzniklého aplikací instančního konstruktoru. Je-li daným typem interfejs, pak jeho instance jsou současně instancemi nějaké třídy, která jej implementuje. Objekt, který je instancí nějakého typu, je současně instancí všech předků daného typu včetně případných implementovaných interfejsů. Mateřská třída objektu (object s mother class) je třída, jejíž instanční konstruktor vrátil daný objekt jako svoji návratovou hodnotu. Přetížení metody (method overloading) je konstrukce, při níž je v rámci stejného datového typu definováno více metod se stejným názvem, ale s různými sadami typů jejich parametrů. Tyto sady se musejí lišit buď počtem parametrů, anebo typy stejnolehlých parametrů. Strana 342 z 351

343 Příloha A: Používané termíny 343 Zakrytí metody (method hiding) je konstrukce, při níž datový typ potomka definuje metodu, která má stejnou (nebo alespoň kompatibilní) signaturu jako některá z nevirtuálních metod předka. Při zaslání příslušné zprávy objektu proto bude vždy zavolána metoda odpovídající typu, za jehož instanci se v danou chvíli oslovený objekt vydává. V jazyku Java je možno zakrývat pouze metody tříd. V některých jiných jazycích (C++, C #, ) je možno zakrývat i metody instancí. Přebití metody (method overriding) je konstrukce, při níž datový typ potomka definuje metodu, která má stejnou (nebo alespoň kompatibilní) signaturu jako některá z virtuálních metod předka. Při zaslání příslušné zprávy objektu proto bude vždy zavolána přebíjející metoda bez ohledu na to, za čí instance se v danou chvíli objekt vydává. Předefinování metody (method redefining) je operace, která (většinou při zavádění dané metody do paměti) změní definici dané metody (např. při aplikaci aspektů). Přepsání metody (method rewriting) je operace související s refaktorací kódu, při níž se mění struktura kódu, aniž by se (většinou) měnila jeho funkčnost. Přepsání proto většinou vede na změnu zdrojového kódu. Instance versus objekt Objektové programování vychází z postulátu, že v OOP je vše objekt, přesněji řečeno objektem je vše, co můžeme označit podstatným jménem. Objektem jsou proto nejenom takové věci jako pes, auto či trojúhelník, ale i abstraktní pojmy typu krása, velikost, připojení, výpočet apod. Objektem je proto i třeba třída nebo interfejs. Takovéto objekty však v jazyku Java a v jazycích jemu podobných nejsou (na rozdíl od některých jiných jazyků např. Smalltalk) instancemi žádné třídy (a nemůžeme je proto např. uložit do proměnné). Z toho vyplývá, že termín objekt je obecnější než termín instance, protože zahrnuje i takové objekty, které nejsou ničí instancí. Budu-li proto v dalším textu hovořit o objektu tříd/interfejsu Xxx, nebudu tím myslet jeho instanci, ale přímo objekt představující daný datový typ. Class-objekt datového typu Xxx (class-object of the Xxx data type) Jak jsme si právě řekli, v jazyku Java nemáme, na rozdíl od některých jiných jazyků, standardní přístup k objektům datových typů; jenom k jejich instancím. Mů- Strana 343 z 351

344 Příloha A: Používané termíny 344 žeme se na ně obracet prostřednictvím jejich literálů, kterými jsou názvy těchto typů (např. Integer.parseInt(?)), ale nemůžeme je např. uložit do proměnné. Java a jí podobné jazyky tuto nemožnost obcházejí zavedením datového typu Class, jehož instance, které označujeme jako class-objekty, reprezentují jednotlivé datové typy 30. Každý datový typ včetně interfejsů a primitivních datových typů má přiřazen právě jeden class-objekt a naopak, každý class-objekt reprezentuje právě jeden datový typ. Class-objekt můžeme uložit do proměnné a obracet se na něj, až budeme po jím reprezentovaném datovém typu něco chtít. Datový typ Class je v Javě definován jako generický, přičemž jeho typovým parametrem je jím reprezentovaný datový typ např. Class<String> clsstr = String.class; Kontejner je objekt, který slouží k uchovávání jiných objektů. Kontejnery dělíme na statické a dynamické. Statické nemohou měnit svoji velikost; řadíme mezi ně přepravky a pole. Dynamické mohou měnit počet uložených prvků během chodu programu. Typickými dynamickými kontejnery jsou kolekce a mapy. Nejznámějšími dynamickými kontejnery jsou kolekce a mapy. 30 Class-objekt může reprezentovat libovolný datový typ, nejenom třídu, ale i výčtový typ, interfejs a primitivní datový typ. Každý z nich má svůj class-objekt. Strana 344 z 351

345 Příloha B: Šablony zdrojových kódů 345 B Šablony zdrojových kódů Příloha B Šablony zdrojových kódů Šablona interfejsu Výpis B.1: Definice (polo)prázdného interfejsu s řádkovými komentáři vyznačujícími jednotlivé sekce kódu 1 /* The file is saved in UTF-8 codepage. 2 * Check: «Stereotype», Section mark-, Copyright-, Alpha-α, Beta-β, Smile- 3 */ 4 package eu.pedu.adventure15p; /******************************************************************************* 9 * Instance interfejsu {@code EmptyInterface} představují * Instances of interface {@code EmptyInterface} represent * 12 Rudolf PECINOVSKÝ yy-mm-dd 14 */ 15 public interface EmptyInterface 16 { 17 //== STATIC CONSTANTS ========================================================== 18 //== STATIC METHODS ============================================================ //############################################################################## 23 //== ABSTRACT GETTERS AND SETTERS ============================================== 24 //== OTHER ABSTRACT METHODS ==================================================== 25 //== DEFAULT GETTERS AND SETTERS =============================================== 26 //== OTHER DEFAULT METHODS ===================================================== //############################################################################## 31 //== NESTED DATA TYPES ========================================================= 32 } Strana 345 z 351

346 Příloha B: Šablony zdrojových kódů 346 Šablona třídy Výpis B.2: Definice (polo)prázdné třídy s řádkovými komentáři vyznačujícími jednotlivé sekce kódu 1 /* The file is saved in UTF-8 codepage. 2 * Check: «Stereotype», Section mark-, Copyright-, Alpha-α, Beta-β, Smile- 3 */ 4 package eu.pedu; /******************************************************************************* 9 * Instance třídy {@code EmptyClass} představují * 11 Rudolf PECINOVSKÝ yy-mm-dd 13 */ 14 public class EmptyClass 15 { 16 //== CONSTANT CLASS ATTRIBUTES ================================================= 17 //== VARIABLE CLASS ATTRIBUTES ================================================= //############################################################################## 22 //== STATIC INITIALIZER (CLASS CONSTRUCTOR) ==================================== 23 //== CLASS GETTERS AND SETTERS ================================================= 24 //== OTHER NON-PRIVATE CLASS METHODS =========================================== 25 //== PRIVATE AND AUXILIARY CLASS METHODS ======================================= //############################################################################## 30 //== CONSTANT INSTANCE ATTRIBUTES ============================================== 31 //== VARIABLE INSTANCE ATTRIBUTES ============================================== //############################################################################## 36 //== CONSTUCTORS AND FACTORY METHODS =========================================== /*************************************************************************** 39 * 40 */ 41 public EmptyClass() 42 { 43 } Strana 346 z 351

347 Příloha B: Šablony zdrojových kódů //== ABSTRACT METHODS ========================================================== 48 //== INSTANCE GETTERS AND SETTERS ============================================== 49 //== OTHER NON-PRIVATE INSTANCE METHODS ======================================== 50 //== PRIVATE AND AUXILIARY INSTANCE METHODS ==================================== //############################################################################## 55 //== NESTED DATA TYPES ========================================================= 56 } Strana 347 z 351

348 Literatura 348 Literatura Literatura [1] PECINOVSKÝ R.: Návrhové vzory. 33 vzorových postupů pro objektové programování. Computer Press, 2007, ISBN [2] PECINOVSKÝ R.: Myslíme objektově v jazyku Java kompletní učebnice pro začátečníky, 2. aktualizované a rozšířené vydání. Grada ISBN [3] PECINOVSKÝ R.: OOP Naučte se myslet a programovat objektově. Computer Press 2010, ISBN [4] PECINOVSKÝ R.: Java 7 Učebnice objektové architektury pro začátečníky. 1. vyd. Grada Publishing, Praha, s. ISBN [5] PECINOVSKÝ, Rudolf, KOFRÁNEK, Jiří. The Experience with After-School Teaching of Programming for Parents and Their Children. Las Vegas In FECS'13 The 2013 International Conference on Frontiers in Education: Computer Science and Computer Engineering. [6] PECINOVSKÝ R.: OOP Learn Object Oriented Thinking and Programming. Tomas Bruckner ISBN [7] PECINOVSKÝ Rudolf: Java 8 Učebnice objektové architektury pro mírně pokročilé. Grada, stran. ISBN [8] PECINOVSKÝ, Rudolf: Výklad práce s regulárními výrazy. Strana 348 z 351

349 Rejstřík 349 Rejstřík Rejstřík A adventura koncepce, 31 zadání, 84 akce, 33 alternativní scénář, 62 H hlavní scénář, 62 hra akce, 33 koncepce, 31 příkaz, 33 zadání, 84 I ID, 40 integrační test, 51 J jednotkový test, 51 K kolekce, 344 L logger, 60 O Optional, 142 P příkaz, 33 R reflexe, 72 S ScenarioStep popis, 53 scénář, 52 alternativní, 62 hlavní, 62 krok scénáře, 53 typ, 53 základní, 62 T TDD, 51 terminologie, 28 test integrační, 51 jednotkový, 51 vývoj řízený testy, 51 Test Driven Development, 51 třída ScenarioStep, 53 typ Optional, 142 typ scénáře, 53 U úloha, 29 Strana 349 z 351

350 Rejstřík 350 V výpis AAction, 169 <clinit>, 162 <init>, 161 executecommand, 269, 303 getallactions, 225 initialize, 205, 276 startgame, 205 stopgame, 266 ActionCLose execute, 313 ActionExit <init>, 264 execute, 266 ActionHelp <init>, 223 execute, 227 ActionMove <clinit>, 228 <init>, 229 execute, 232 ActionOpen <init>, 278 execute, 281 ActionPickUp <init>, 234 execute, 238, 300 ActionPutDown <init>, 243 execute, 263 ActionPutOnGlasses execute, 291 ActionRead execute, 289 ActionSupportIcebox execute, 293 AItemContainer, 255 Apartment <init>, 198 getcurrentarea, 199 getoroom, 209 initialize, 207 BasicActions <init>, 139 Class, 360, 363 Conversation answer, 306 start, 302 waitingfortheage, 308 waitingfortheyear, 311 Hands, 260 removeitem, 286 tryadditem, 238 IAuthorPrototype, 43 IGamePlayer, 330 IGSMFactory, 46 getinstanceoffactory, 72 Item, 193 ItemContainer isfull, 293 ManagerWithConstants, 104 Room, 258 <init>, 182, 250 initialize, 208 initializeitems, 208 initializeneighbors, 210 removeitem, 237 RUPApartmentGame <init>, 149 getallactions, 224 getbag, 212 getbasicactions, 318 getworld, 200 RUPApartmentManager <init>, 221 RUPAppartmentGame executecommand, 163 getworld, 174 isalive, 153, 165 RUPGSMFactory, 122 getgame, 151 getscenariomanager, 106 RUPScenarioManagerCon main, 222 Strana 350 z 351

351 Rejstřík 351 REQUIRED_STEPS, 219 RUPScenarioManagerD <init>, 117 getinstance, 115 main, 118 setdelegate, 114 RUPScenarioManagerH <init>, 119 getinstance, 120 setchild, 120 RUPScenarioManagerLit, 87, 90, 92 ScenarioStep, 60 State, 276 Texts, 100, 279 další konstanty, 309 TypeOfScenario, 62 TypeOfStep, 56, 75 UIA_JOptionPane startgame, 324, 326 UIB_Scanner startgame, 328 UIC_GamePlayer, 331 UID_Multiplayer, 336 Y x, 247, 364 vývoj řízený testy, 51 Z základní scénář, 62 Strana 351 z 351

352

353 Rejstřík 353 Část II: KONEC Část II: KONEC Strana 353 z 351

354 Kapitola 0: 354 Strana 354 z 351

355 Kapitola 17: Prázdná kapitola Prázdná kapitola 1 Kapitola 17 Prázdná kapitola 1 Co se v kapitole naučíte Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Zopakujme si, co jsme se v této lekci dozvěděli:. Strana 355 z 351

356 Kapitola 18: Prázdná kapitola Prázdná kapitola 2 Kapitola 18 Prázdná kapitola 2 Co se v kapitole naučíte Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Zopakujme si, co jsme se v této kapitole dozvěděli:. Strana 356 z 351

357 Příloha C: Prázdná příloha 357 C Prázdná příloha Příloha C Prázdná příloha Instalace Java 2 SDK Strana 357 z 351

358 Příloha D: Prázdná příloha 358 D Prázdná příloha Příloha D Prázdná příloha Instalace Java 2 SDK Strana 358 z 351

359 Příloha E: Odkladky 359 E Odkladky Příloha E Odkladky Chyba! Nenalezen zdroj odkazů. Interfejs IGame Vraťme se ale ke správci scénářů. V minulé pasáži jsem sice uvedl, že správce scénářů by měl mít definovány metody pro získání hry a její mateřské třídy, ale stále jsem ještě nespecifikoval jejich přesnou deklaraci. Ještě jsme se totiž nedohodli, jakého typu budou hodnoty, které budou tyto metody vracet, tj. jakého typu budou naše hry. Musíme mít stále na paměti, že obdobnou úlohu bude řešit řada studentů, takže nebude existovat jedna jediná hra. Připravíme se proto hned na to, že bude existovat nějaký interfejs pojmenovaný např. IGame, který bude specifikovat, co má taková hra vlastně umět. Prozatím stačí znát jeho název. V dalším textu pak postupně navrhneme celou jeho definici. Hra každého studenta tedy bude muset implementovat interfejs IGame. Výše zmíněné metody bychom proto mohli deklarovat: public Class<? extend IGame> getgameclass(); public IGame getgame(); První řádek deklaruje metodu, která vrací class-objekt třídy implementující interfejs IGame, druhý řádek deklaruje metodu vracející přímo danou hru, která musí být instanci třídy implementující interfejs IGame (kontrakt upřesňuje, že musí být instancí třídy, jejíž class-objekt vrací první z metod). Strana 359 z 351

360 Příloha E: Odkladky Vylepšování architektury refaktorace Kapitola 19 Vylepšování architektury refaktorace Co se v kapitole naučíte Při definici metody execute(string...) instancí třídy ActionSupportIcebox (viz výpis na straně 293) jsem vám říkal, že metoda je nechutně dlouhá. Výpis 19.1: 1 Definice Class OBR Obrázek 19.1 Vlastní popis obrázku (pozor na počáteční tabulátor zachovat!) 19.2 Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali Zopakujme si, co jsme v této kapitole dozvěděli a co jsme naprogramovali. Strana 360 z 351

361 Příloha E: Odkladky 361 Ke stavu vývoje projektu, který jsme shrnuli v podkapitole Shrnutí co jsme se v kapitole dozvěděli a co jsme prozatím naprogramovali na straně 296, jsme přidali následující: Definovali jsme třídu State jako schránku na nejrůznější příznaky, které je třeba si pamatovat v průběhu hry. Každý příznak je definovaný jako statický atribut s příslušnými přístupovými metodami. Definovali jsme třídu ActionOpen, jejíž instance je zodpovědná za správnou reakci na příkaz pro otevření ledničky. Opravili jsme chybu v metodě execute(string...) instancí třídy ActionPutDown. Definovali jsme třídu ActionRead, jejíž instance je zodpovědná za správnou reakci na příkaz k přečtení zadaného h-objektu papíru nebo časopisu. Definovali jsme třídu ActionSupportIcebox, jejíž instance je zodpovědná za správnou reakci na příkaz pro podložení ledničky. Diagram tříd současného stavu naší (rozpracované) hry si můžete prohlédnout na obrázku Obrázek 19.2 Aktuální diagram tříd projektu poté, co byly definovány třídy reprezentující nestandardní příkazy hry bez podpory rozhovoru Strana 361 z 351

OOP. Verze : 365 NS, odstavců, slov, znaků, bajtů. a Java 8

OOP. Verze : 365 NS, odstavců, slov, znaků, bajtů. a Java 8 Verze 15.57.5745 2015-10-24: 365 NS, 8 253 odstavců, 89 019 slov, 657 239 znaků, 2 963 968 bajtů OOP a Java 8 Návrh a vývoj složitějšího projektu vyhovujícího zadanému rámci Rudolf Pecinovský 49R_Adventura_TXT_ZLOM.doc

Více

Verze 0.13.5299: 226 NS, 5 496 odstavců, 54 943 slov, 406 692 znaků, 2 052 608 bajtů. Adventura. Rudolf Pecinovský 2015

Verze 0.13.5299: 226 NS, 5 496 odstavců, 54 943 slov, 406 692 znaků, 2 052 608 bajtů. Adventura. Rudolf Pecinovský 2015 Verze 0.13.5299: 226 NS, 5 496 odstavců, 54 943 slov, 406 692 znaků, 2 052 608 bajtů Návrh semestrálního projektu a jeho rámce Adventura Rudolf Pecinovský 2015 49R_Adventura.doc verze 0.13.5299, uloženo:

Více

14.4.2010. Obsah přednášky 7. Základy programování (IZAPR) Přednáška 7. Parametry metod. Parametry, argumenty. Parametry metod.

14.4.2010. Obsah přednášky 7. Základy programování (IZAPR) Přednáška 7. Parametry metod. Parametry, argumenty. Parametry metod. Základy programování (IZAPR) Přednáška 7 Ing. Michael Bažant, Ph.D. Katedra softwarových technologií Kancelář č. 229, Náměstí Čs. legií Michael.Bazant@upce.cz Obsah přednášky 7 Parametry metod, předávání

Více

ČÁST 1. Zahřívací kolo. Co je a k čemu je návrhový vzor 33

ČÁST 1. Zahřívací kolo. Co je a k čemu je návrhový vzor 33 Stručný obsah Část 1: Zahřívací kolo Kapitola 1 Co je a k čemu je návrhový vzor 33 Kapitola 2 Zásady objektově orientovaného programování 39 Kapitola 3 Co konstruktor neumí (Jednoduchá tovární metoda Simple

Více

Generátor kódu. a jeho uplatnění ve výuce programování. Rudolf PECINOVSKÝ rudolf@pecinovsky.cz

Generátor kódu. a jeho uplatnění ve výuce programování. Rudolf PECINOVSKÝ rudolf@pecinovsky.cz Generátor kódu a jeho uplatnění ve výuce programování Rudolf PECINOVSKÝ rudolf@pecinovsky.cz Trendy poslední doby Další a další státy si uvědomují nutnost zařazení výuky programování do učiva základních

Více

11 Diagram tříd, asociace, dědičnost, abstraktní třídy

11 Diagram tříd, asociace, dědičnost, abstraktní třídy 11 Diagram tříd, asociace, dědičnost, abstraktní třídy Studijní cíl Tento studijní blok má za cíl pokračovat v základních prvcích jazyka Java. Konkrétně bude věnována pozornost diagramům tříd, asociaci,

Více

1. Programování proti rozhraní

1. Programování proti rozhraní 1. Programování proti rozhraní Cíl látky Cílem tohoto bloku je seznámení se s jednou z nejdůležitější programátorskou technikou v objektově orientovaném programování. Tou technikou je využívaní rozhraní

Více

Obsah. Úvod Začínáme s PowerPointem Operace se snímky Pro koho je kniha určena...10 Použité konvence...11

Obsah. Úvod Začínáme s PowerPointem Operace se snímky Pro koho je kniha určena...10 Použité konvence...11 Obsah Úvod... 9 Pro koho je kniha určena...10 Použité konvence...11 Začínáme s PowerPointem... 13 1.1 Základní pojmy...14 1.2 Podokno úloh...16 1.3 Zobrazení dokumentu...17 1.4 Uložení prezentace...21

Více

Ukázka knihy z internetového knihkupectví www.kosmas.cz

Ukázka knihy z internetového knihkupectví www.kosmas.cz Ukázka knihy z internetového knihkupectví www.kosmas.cz U k á z k a k n i h y z i n t e r n e t o v é h o k n i h k u p e c t v í w w w. k o s m a s. c z, U I D : K O S 1 8 0 5 8 4 U k á z k a k n i h

Více

1. Dědičnost a polymorfismus

1. Dědičnost a polymorfismus 1. Dědičnost a polymorfismus Cíl látky Cílem této kapitoly je představit klíčové pojmy dědičnosti a polymorfismu. Předtím však je nutné se seznámit se základními pojmy zobecnění neboli generalizace. Komentář

Více

Tento studijní blok má za cíl pokračovat v základních prvcích jazyka Java. Konkrétně bude věnována pozornost rozhraním a výjimkám.

Tento studijní blok má za cíl pokračovat v základních prvcích jazyka Java. Konkrétně bude věnována pozornost rozhraním a výjimkám. 13 Rozhraní, výjimky Studijní cíl Tento studijní blok má za cíl pokračovat v základních prvcích jazyka Java. Konkrétně bude věnována pozornost rozhraním a výjimkám. Doba nutná k nastudování 2 2,5 hodiny

Více

10 Balíčky, grafické znázornění tříd, základy zapozdření

10 Balíčky, grafické znázornění tříd, základy zapozdření 10 Balíčky, grafické znázornění tříd, základy zapozdření Studijní cíl Tento studijní blok má za cíl pokračovat v základních prvcích jazyka Java. Konkrétně bude věnována pozornost příkazům balíčkům, grafickému

Více

Obsah. Úvod 11 Základy programování 11 Objektový přístup 11 Procvičování 11 Zvláštní odstavce 12 Zpětná vazba od čtenářů 12 Errata 13

Obsah. Úvod 11 Základy programování 11 Objektový přístup 11 Procvičování 11 Zvláštní odstavce 12 Zpětná vazba od čtenářů 12 Errata 13 Úvod 11 Základy programování 11 Objektový přístup 11 Procvičování 11 Zvláštní odstavce 12 Zpětná vazba od čtenářů 12 Errata 13 KAPITOLA 1 Na úvod o Javě 15 Počítačový program 15 Vysokoúrovňový programovací

Více

3. Je defenzivní programování technikou skrývání implementace? Vyberte jednu z nabízených možností: Pravda Nepravda

3. Je defenzivní programování technikou skrývání implementace? Vyberte jednu z nabízených možností: Pravda Nepravda 1. Lze vždy z tzv. instanční třídy vytvořit objekt? 2. Co je nejčastější příčinou vzniku chyb? A. Specifikace B. Testování C. Návrh D. Analýza E. Kódování 3. Je defenzivní programování technikou skrývání

Více

Vyřešené teoretické otázky do OOP ( )

Vyřešené teoretické otázky do OOP ( ) Vyřešené teoretické otázky do OOP (16. 1. 2013) 1) Vyjmenujte v historickém pořadí hlavní programovací paradigmata a stručně charakterizujte každé paradigma. a) Naivní chaotičnost, špatná syntaxe a sémantika

Více

Microsoft Word základní

Microsoft Word základní Časový rozsah: 2 dny (8:30-14:00) Cena: 2400 Kč + DPH Microsoft Word základní Tvorba kratších dokumentů se zaměřením na korespondenci. Základy tvorby a formátování písma a odstavců. Vkládání tabulek a

Více

Metodika. Architecture First. Rudolf Pecinovský rudolf@pecinovsky.cz

Metodika. Architecture First. Rudolf Pecinovský rudolf@pecinovsky.cz Copyright Rudolf Pecinovský, Soubor: 2014_Comm_PrW_Architecture First Methodology.doc, verze 1.00.2413, uloženo po 9.6.2014 14:43 1 z 39 Metodika Architecture First Rudolf Pecinovský rudolf@pecinovsky.cz

Více

OBJEKTOVÉ PROGRAMOVÁNÍ V C++ V PŘÍKLADECH 8 Proudová knihovna 8.1 Hierarchie proudů... 8-1 8.2 Standardně zavedené proudy... 8-1 8.

OBJEKTOVÉ PROGRAMOVÁNÍ V C++ V PŘÍKLADECH 8 Proudová knihovna 8.1 Hierarchie proudů... 8-1 8.2 Standardně zavedené proudy... 8-1 8. David MATOUŠEK OBJEKTOVÉ PROGRAMOVÁNÍ V C++ V PØÍKLADECH Praha 2011 David Matoušek Objektové programování v C++ v pøíkladech Lektoroval Ing. Bohumil Brtník, Ph.D. Bez pøedchozího písemného svolení nakladatelství

Více

Informace k e-learningu

Informace k e-learningu Informace k e-learningu Příprava na testy bude probíhat samostatně formou e-learningových školení přístupných způsobem popsaným níže. Zkušební testy, pomocí kterých se budete připravovat na závěrečný test,

Více

Vývoj a ověřování metodiky výuky programování

Vývoj a ověřování metodiky výuky programování Copyright Rudolf Pecinovský, Soubor: 2016_INF_Architecture First.doc, verze 1.00.2413, uloženo út 19.1.2016 10:03 1 z 11 Vývoj a ověřování metodiky výuky programování Rudolf Pecinovský Informatika XXIX

Více

Vaše jistota na trhu IT. Balíčky. Rudolf Pecinovský rudolf@pecinovsky.cz

Vaše jistota na trhu IT. Balíčky. Rudolf Pecinovský rudolf@pecinovsky.cz Vaše jistota na trhu IT Balíčky Rudolf Pecinovský rudolf@pecinovsky.cz Problémy velkých aplikací Rozsáhlé aplikace používají velké množství názvů objektů a jejich zpráv, které různé části programu sdílí

Více

8 Třídy, objekty, metody, předávání argumentů metod

8 Třídy, objekty, metody, předávání argumentů metod 8 Třídy, objekty, metody, předávání argumentů metod Studijní cíl Tento studijní blok má za cíl pokračovat v základních prvcích jazyka Java. Konkrétně bude věnována pozornost třídám a objektům, instančním

Více

Práce se styly 1. Styl

Práce se styly 1. Styl Práce se styly 1. Styl Styl se používá, pokud chceme, aby dokument měl jednotný vzhled odstavců. Můžeme si nadefinovat styly pro různé úrovně nadpisů, jednotlivé popisy, charakteristiky a další odstavce.

Více

7 Formátovaný výstup, třídy, objekty, pole, chyby v programech

7 Formátovaný výstup, třídy, objekty, pole, chyby v programech 7 Formátovaný výstup, třídy, objekty, pole, chyby v programech Studijní cíl Tento studijní blok má za cíl pokračovat v základních prvcích jazyka Java. Konkrétně bude věnována pozornost formátovanému výstupu,

Více

Obsahy kurzů MS Office

Obsahy kurzů MS Office Obsahy kurzů MS Office V současné době probíhají kurzy MS Office 2010 s následující osnovou: 1. Základy práce na PC, MS Office - praktické užití Kurz je určen pro všechny, kteří mají s prací na PC minimální

Více

knihovna programátora

knihovna programátora knihovna programátora Učebnice pro ty, kteří nechtějí zůstat obyčejnými kodéry, ale chtějí se stát špičkovými architekty Postupuje podle metodiky Architecture First Soustředí se na návrh programů a osvojení

Více

Ukázka knihy z internetového knihkupectví

Ukázka knihy z internetového knihkupectví Ukázka knihy z internetového knihkupectví www.kosmas.cz Věnováno mé rodině ACCESS 2007 PODROBNÝ PRŮVODCE 5 Úvod... 13 Komu je tato kniha určena...13 Co v této knize naleznete...14 Použité konvence a struktura

Více

Základy objektové orientace I. Únor 2010

Základy objektové orientace I. Únor 2010 Seminář Java Základy objektové orientace I Radek Kočí Fakulta informačních technologií VUT Únor 2010 Radek Kočí Seminář Java Základy OO (1) 1/ 20 Téma přednášky Charakteristika objektově orientovaných

Více

Aplikační software B

Aplikační software B Cíl předmětu: Cílem předmětu je prohloubit znalosti studentů ze základních aplikačních programů. Jedná se především o pokročilejší nástroje z aplikací MS Word a MS Excel. Dále se naučí zpracovávat prezentace

Více

Předměty. Algoritmizace a programování Seminář z programování. Verze pro akademický rok 2012/2013. Verze pro akademický rok 2012/2013

Předměty. Algoritmizace a programování Seminář z programování. Verze pro akademický rok 2012/2013. Verze pro akademický rok 2012/2013 Předměty Algoritmizace a programování Seminář z programování Verze pro akademický rok 2012/2013 Verze pro akademický rok 2012/2013 1 Přednášky Jiřina Královcová MTI, přízemí budovy A Tel: 48 53 53 521

Více

2. Modelovací jazyk UML 2.1 Struktura UML 2.1.1 Diagram tříd 2.1.1.1 Asociace 2.1.2 OCL. 3. Smalltalk 3.1 Jazyk 3.1.1 Pojmenování

2. Modelovací jazyk UML 2.1 Struktura UML 2.1.1 Diagram tříd 2.1.1.1 Asociace 2.1.2 OCL. 3. Smalltalk 3.1 Jazyk 3.1.1 Pojmenování 1. Teoretické základy modelování na počítačích 1.1 Lambda-kalkul 1.1.1 Formální zápis, beta-redukce, alfa-konverze 1.1.2 Lambda-výraz jako data 1.1.3 Příklad alfa-konverze 1.1.4 Eta-redukce 1.2 Základy

Více

Programování v jazyce C a C++

Programování v jazyce C a C++ Programování v jazyce C a C++ Příklad na tvorbu třídy Richter 1 4. prosince 2017 1 Ing. Richter Miloslav, Ph.D., UAMT FEKT VUT Brno Dvourozměrné pole pomocí tříd Zadání Navrhněte a napište třídu pro realizace

Více

Úvodem... 9 Kapitola 1 Karetních

Úvodem... 9 Kapitola 1 Karetních Úvodem... 9 Základní znalosti o programovacích jazycích...10 Jazyk C# a platforma.net...10 Visual C# 2010 Express...11 Instalace platformy.net 4.0 a Visual C# 2010 Express...11 Zdrojový kód aplikací...12

Více

Objektově orientované programování v jazyce Python

Objektově orientované programování v jazyce Python Objektově orientované programování v jazyce Python Co to je objektově orientované programování Python není přímo objektově orientovaný jazyk, ale podporuje nejdůležitější části objektově orientovaného

Více

Formy komunikace s knihovnami

Formy komunikace s knihovnami Formy komunikace s knihovnami Současné moderní prostředky Jiří Šilha a Jiří Tobiáš, Tritius Solutions a.s., Brno Osnova Základní požadavky na komunikaci s knihovnami Historie komunikace s knihovnami Confluence

Více

7. přednáška - třídy, objekty třídy objekty atributy tříd metody tříd

7. přednáška - třídy, objekty třídy objekty atributy tříd metody tříd 7. přednáška - třídy, objekty třídy objekty atributy tříd metody tříd Algoritmizace (Y36ALG), Šumperk - 7. přednáška 1 Třída jako zdroj funkcionality Třída v jazyku Java je programová jednotka tvořená

Více

KAPITOLA 5 - POKROČILÉ ZPRACOVÁNÍ TEXTU

KAPITOLA 5 - POKROČILÉ ZPRACOVÁNÍ TEXTU KAPITOLA 5 - POKROČILÉ ZPRACOVÁNÍ TEXTU KLÍČOVÉ POJMY Oddíly, styly, poznámka pod čarou, revize, obsah, rejstřík, záložka, citace a seznamy literatury, vzorce, vložené a propojené objekty, oddíly, zabezpečení.

Více

KOMU JE KNIHA URČENA?

KOMU JE KNIHA URČENA? 7 Kapitola 0. O této knížce KOMU JE KNIHA URČENA? Tuto učebnici jsem vytvářel na základě mých přednášek a úvodních kursů na Vysoké škole manažerské informatiky a ekonomiky. Většina mých studentů měla malou

Více

Programové konvence, dokumentace a ladění. Programování II 2. přednáška Alena Buchalcevová

Programové konvence, dokumentace a ladění. Programování II 2. přednáška Alena Buchalcevová Programové konvence, dokumentace a ladění 2. přednáška Alena Buchalcevová Proč dodržovat programové konvence? velkou část životního cyklu softwaru tvoří údržba údržbu provádí většinou někdo jiný než autor

Více

INFORMATIKA. Libovolná učebnice k MS OFFICE 200x (samostatné učebnice k textovému procesoru MS Word 200x, tabulkovému procesoru MS Excel 200x).

INFORMATIKA. Libovolná učebnice k MS OFFICE 200x (samostatné učebnice k textovému procesoru MS Word 200x, tabulkovému procesoru MS Excel 200x). Cíl předmětu: Cílem předmětu je prohloubit znalosti studentů ze základních aplikačních programů. Jedná se především o pokročilejší nástroje z aplikací MS Word a MS Excel. Jednotlivé semináře se zaměřují

Více

Vzdělávací oblast: Informatika a informační a komunikační technologie Vzdělávací obor: Programování. Předmět: Programování

Vzdělávací oblast: Informatika a informační a komunikační technologie Vzdělávací obor: Programování. Předmět: Programování Vzdělávací oblast: Informatika a informační a komunikační technologie Vzdělávací obor: Programování Vzdělávací oblast Informatika a informační a komunikační technologie pro vzdělávací obor Programování

Více

Obsah. Předmluva 13 Zpětná vazba od čtenářů 14 Zdrojové kódy ke knize 15 Errata 15

Obsah. Předmluva 13 Zpětná vazba od čtenářů 14 Zdrojové kódy ke knize 15 Errata 15 Předmluva 13 Zpětná vazba od čtenářů 14 Zdrojové kódy ke knize 15 Errata 15 KAPITOLA 1 Úvod do programo vání v jazyce C++ 17 Základní pojmy 17 Proměnné a konstanty 18 Typy příkazů 18 IDE integrované vývojové

Více

DUM 06 téma: Tvorba makra pomocí VBA

DUM 06 téma: Tvorba makra pomocí VBA DUM 06 téma: Tvorba makra pomocí VBA ze sady: 03 tematický okruh sady: Tvorba skript a maker ze šablony: 10 Algoritmizace a programování určeno pro: 4. ročník vzdělávací obor: 18-20-M/01 Informační technologie

Více

Klíčová slova: OOP, konstruktor, destruktor, třída, objekt, atribut, metoda

Klíčová slova: OOP, konstruktor, destruktor, třída, objekt, atribut, metoda Anotace sady: Úvod do objektově orientovaného programování, VY_32_INOVACE_PRG_OOP_01 Autor: Blanka Sadovská Klíčová slova: OOP, konstruktor, destruktor, třída, objekt, atribut, metoda Druh učebního materiálu:

Více

11.5.2012. Obsah přednášky 9. Skrývání informací. Skrývání informací. Zapouzdření. Skrývání informací. Základy programování (IZAPR, IZKPR) Přednáška 9

11.5.2012. Obsah přednášky 9. Skrývání informací. Skrývání informací. Zapouzdření. Skrývání informací. Základy programování (IZAPR, IZKPR) Přednáška 9 Obsah přednášky 9 Základy programování (IZAPR, IZKPR) Přednáška 9 Základy dědičnosti, přístupová práva Ing. Michael Bažant, Ph.D. Katedra softwarových technologií Kancelář č. 03 022, Náměstí Čs. legií

Více

Kapitola 1 První kroky v tvorbě miniaplikací 11

Kapitola 1 První kroky v tvorbě miniaplikací 11 Obsah Úvodem 9 Komu je kniha určena 9 Kapitola 1 První kroky v tvorbě miniaplikací 11 Co je to Postranní panel systému Windows a jak funguje 12 Co je potřeba vědět před programováním miniaplikací 16 Vaše

Více

TÉMATICKÝ OKRUH Softwarové inženýrství

TÉMATICKÝ OKRUH Softwarové inženýrství TÉMATICKÝ OKRUH Softwarové inženýrství Číslo otázky : 24. Otázka : Implementační fáze. Postupy při specifikaci organizace softwarových komponent pomocí UML. Mapování modelů na struktury programovacího

Více

Objektově orientované programování v jazyce Python

Objektově orientované programování v jazyce Python Objektově orientované programování v jazyce Python Základní pojmy objektově orientovaného programování Objekt vychází z reálného světa. Má dva charakteristické rysy. Všechny objekty mají stav Všechny objekty

Více

Obsah. Úvod do studia 11 Co byste měli předem znát 13. Úvod do obsluhy AutoCADu 23. Kapitola 1 11. Kapitola 1 23

Obsah. Úvod do studia 11 Co byste měli předem znát 13. Úvod do obsluhy AutoCADu 23. Kapitola 1 11. Kapitola 1 23 Předmluva 9 Komu je tato kniha určena 11 Kapitola 1 11 Úvod do studia 11 Co byste měli předem znát 13 CAD technologie 13 Product Lifecycle Management 14 AutoCAD není jenom CAD, je to vývojová platforma

Více

KOMU JE KNIHA URČENA?

KOMU JE KNIHA URČENA? 7 Kapitola 0. O této knížce KOMU JE KNIHA URČENA? Tuto učebnici jsem vytvářel na základě mých přednášek a úvodních kurzů na vysokých školách i ve firmě moderníprogramování. Většina mých studentů měla malou

Více

III/2 Inovace a zkvalitnění výuky prostřednictvím ICT

III/2 Inovace a zkvalitnění výuky prostřednictvím ICT Číslo a název šablony Číslo didaktického materiálu Druh didaktického materiálu Autor Jazyk Téma sady didaktických materiálů Téma didaktického materiálu Vyučovací předmět Cílová skupina (ročník) Úroveň

Více

VYSOKÁ ŠKOLA FINANČNÍ A SPRÁVNÍ, O.P.S. Základy informatiky

VYSOKÁ ŠKOLA FINANČNÍ A SPRÁVNÍ, O.P.S. Základy informatiky Metodické listy pro předmět Základy informatiky Cíl předmětu: Cílem předmětu je seznámit studenty kombinovaného studia s vytvářením a formátováním textových dokumentů, využitím tabulkových procesorů a

Více

TEORIE ZPRACOVÁNÍ DAT

TEORIE ZPRACOVÁNÍ DAT Vysoká škola báňská - Technická univerzita Ostrava Fakulta elektrotechniky a informatiky TEORIE ZPRACOVÁNÍ DAT pro kombinované a distanční studium Jana Šarmanová Ostrava 2003 Jana Šarmanová, 2003 Fakulta

Více

Quo vadis programování? Automatizace vyhodnocování studentských úloh

Quo vadis programování? Automatizace vyhodnocování studentských úloh Vaše jistota na trhu IT Quo vadis programování? Automatizace vyhodnocování studentských úloh Rudolf PECINOVSKÝ rudolf@pecinovsky.cz Vladimír Oraný vladimir.orany@gmail.com Vaše jistota na trhu IT Obsah

Více

SIPVZ. Máme Z a co dál? Cíle projektu P1 a P2. Co je cílem projektu P0? Obsah modulu P0. Obsah modulu P0. Oblast P Úvodní modul 2003/2004

SIPVZ. Máme Z a co dál? Cíle projektu P1 a P2. Co je cílem projektu P0? Obsah modulu P0. Obsah modulu P0. Oblast P Úvodní modul 2003/2004 SIPVZ Oblast P Úvodní modul 2003/2004 Máme Z a co dál? Celá koncepce úrovně P je tří-modulová: P0 úvodní modul (20 + 10h) Absolvování docházka a aktivní přístup k řešení problémů a úloh Další dva volitelné

Více

Předmluva 11 Typografická konvence použitá v knize 12. 1 Úvod do Excelu 2003 13

Předmluva 11 Typografická konvence použitá v knize 12. 1 Úvod do Excelu 2003 13 Předmluva 11 Typografická konvence použitá v knize 12 1 Úvod do Excelu 2003 13 Spuštění a ukončení Excelu 14 Spuštění Excelu 14 Ukončení práce s Excelem 15 Přepínání mezi otevřenými sešity 16 Oprava aplikace

Více

1 Strukturované programování

1 Strukturované programování Projekt OP VK Inovace studijních oborů zajišťovaných katedrami PřF UHK Registrační číslo: CZ.1.07/2.2.00/28.0118 1 Cíl Seznámení s principy strukturovaného programování, s blokovou strukturou programů,

Více

knihovna programátora

knihovna programátora knihovna programátora Učebnice pro ty, kteří nechtějí zůstat obyčejnými kodéry, ale chtějí se stát špičkovými architekty Postupuje podle metodiky Architecture First Soustředí se na návrh programů a osvojení

Více

OBSAH. Kontrola aktualizací... 18

OBSAH. Kontrola aktualizací... 18 2013 Albatros Media a. s. Toto CD je součástí knihy Adobe InDesign CS6, Oficiální výukový kurz a je samostatně neprodejné. Všechna práva vyhrazena. Nelegální kopie tohoto disku jsou zakázány. K2059_potisk.indd

Více

Překladač a jeho struktura

Překladač a jeho struktura Překladač a jeho struktura Překladače, přednáška č. 1 Šárka Vavrečková Ústav informatiky, FPF SU Opava sarka.vavreckova@fpf.slu.cz http://fpf.slu.cz/ vav10ui Poslední aktualizace: 23. září 2008 Definice

Více

Marketingová komunikace. 2. soustředění. Mgr. Pavel Vávra 9103@mail.vsfs.cz. Kombinované studium Skupina N9KMK1aPH/N9KMK1bPH (um1a1ph/um1b1ph)

Marketingová komunikace. 2. soustředění. Mgr. Pavel Vávra 9103@mail.vsfs.cz. Kombinované studium Skupina N9KMK1aPH/N9KMK1bPH (um1a1ph/um1b1ph) Marketingová komunikace Kombinované studium Skupina N9KMK1aPH/N9KMK1bPH (um1a1ph/um1b1ph) 2. soustředění Mgr. Pavel Vávra 9103@mail.vsfs.cz http://vavra.webzdarma.cz/home/index.htm Minulé soustředění úvod

Více

Aplikační software 2

Aplikační software 2 Metodické listy pro předmět Aplikační software 2 Cíl předmětu: Cílem předmětu je prohloubit znalosti studentů ze základních aplikačních programů patřících do balíku programů MS Office 2003. Studenti se

Více

1. Témata maturitních prací. 2. Termín závazného zadání maturitní práce. 3. Termín odevzdání maturitní práce. 4. Kritéria hodnocení maturitní práce

1. Témata maturitních prací. 2. Termín závazného zadání maturitní práce. 3. Termín odevzdání maturitní práce. 4. Kritéria hodnocení maturitní práce 1. Témata maturitních prací 1. Vytvoření dynamických WWW stránek. 2. Vytvoření sad tesů v prostředí Moodle nebo Response zahrnujících učivo prvního nebo druhého ročníku IVT. 3. Vytvoření Corporate design

Více

Příloha č. 1 k Vyhláška rektora č. 01/2011 o bakalářských pracích

Příloha č. 1 k Vyhláška rektora č. 01/2011 o bakalářských pracích Příloha č. 1 k Vyhláška rektora č. 01/2011 o bakalářských pracích Struktura písemné práce Z formálního hlediska by bakalářská práce měla splňovat požadavky kladené na psaní odborných publikací, tzn. přehlednost,

Více

Programování v C++ 2, 4. cvičení

Programování v C++ 2, 4. cvičení Programování v C++ 2, 4. cvičení statické atributy a metody, konstruktory 1 1 Fakulta jaderná a fyzikálně inženýrská České vysoké učení technické v Praze Zimní semestr 2018/2019 Přehled Přístupová práva

Více

Tvorba kurzu v LMS Moodle

Tvorba kurzu v LMS Moodle Tvorba kurzu v LMS Moodle Před počátkem práce na tvorbě základního kurzu znovu připomínám, že pro vytvoření kurzu musí být profil uživatele nastaven administrátorem systému minimálně na hodnotu tvůrce

Více

Virtuální metody - polymorfizmus

Virtuální metody - polymorfizmus - polymorfizmus - potomka lze použít v místě, kde je možné použít předka - v dosud probraných situacích byly vždy volány funkce, které jsou známy již v době překladu. V situaci, kdy v době překladu není

Více

Ukázka knihy z internetového knihkupectví www.kosmas.cz

Ukázka knihy z internetového knihkupectví www.kosmas.cz Ukázka knihy z internetového knihkupectví www.kosmas.cz U k á z k a k n i h y z i n t e r n e t o v é h o k n i h k u p e c t v í w w w. k o s m a s. c z, U I D : K O S 1 8 0 7 4 4 U k á z k a k n i h

Více

[BAL-MLP] Multiplayer

[BAL-MLP] Multiplayer České vysoké učení technické v Praze Fakulta elektrotechnická Semestrální práce D2 předmětu A7B39PDA [BAL-MLP] Multiplayer Tomáš Kozák (další členové týmu: Tomáš Bruštík, Jaroslav Havelík) LS 2012/2013

Více

Obsah. Zpracoval:

Obsah. Zpracoval: Zpracoval: houzvjir@fel.cvut.cz 03. Modelem řízený vývoj. Doménový (business), konceptuální (analytický) a logický (návrhový) model. Vize projektu. (A7B36SIN) Obsah Modelem řízený vývoj... 2 Cíl MDD, proč

Více

Popis ovládání. Po přihlášení do aplikace se objeví navigátor. Navigátor je stromově seřazen a slouží pro přístup ke všem oknům celé aplikace.

Popis ovládání. Po přihlášení do aplikace se objeví navigátor. Navigátor je stromově seřazen a slouží pro přístup ke všem oknům celé aplikace. Popis ovládání 1. Úvod Tento popis má za úkol seznámit uživatele se základními principy ovládání aplikace. Ovládání je možné pomocí myši, ale všechny činnosti jsou dosažitelné také pomocí klávesnice. 2.

Více

Po ukončení tohoto kurzu budete schopni

Po ukončení tohoto kurzu budete schopni PREZENTACE Vladimír Bureš Tereza Otčenášková Alena Šandová Cíle kurzu Po ukončení tohoto kurzu budete schopni promítnout prezentaci, nastavit vlastnosti prezentace, vytvářet a upravovat snímky, volit různá

Více

VÝVOJ DISTRIBUOVANÝCH APLIKACÍ V SYSTÉMU PLAANT

VÝVOJ DISTRIBUOVANÝCH APLIKACÍ V SYSTÉMU PLAANT VÝVOJ DISTRIBUOVANÝCH APLIKACÍ V SYSTÉMU PLAANT Rudolf Pecinovský Amaio Technologies, Inc., rudolf@pecinovsky.cz ABSTRAKT: Systém Plaant je nástrojem pro vývoj a následnou údržbu distribuovaných databázových

Více

Výčtový typ strana 67

Výčtový typ strana 67 Výčtový typ strana 67 8. Výčtový typ V této kapitole si ukážeme, jak implementovat v Javě statické seznamy konstant (hodnot). Příkladem mohou být dny v týdnu, měsíce v roce, planety obíhající kolem slunce

Více

Objektové programování

Objektové programování Objektové programování - přináší nové možnosti a styl programování - vytváří nový datový typ, který umí vše co standardní datové typy + to co ho naučíme - překladač se k tomuto typu chová stejně jako k

Více

MS Word 2007 Šablony programu MS Word

MS Word 2007 Šablony programu MS Word MS Word 2007 Šablony programu MS Word Obsah kapitoly V této kapitole se seznámíme s: Možností využití šablon při vytváření nových dokumentů Vytvářením vlastních šablon Studijní cíle Po absolvování této

Více

Stránka se dá otevřít dvěma způsoby

Stránka se dá otevřít dvěma způsoby Co je potřeba Mozek, to zaprvé. Budete potřebovat počítač, na kterém běží alespoň nějaký jednoduchý textový editor (Poznámkový blok). Potřebujete webový prohlížeč. Hodí se připojení na internet. Kdo nemá

Více

Ukázka knihy z internetového knihkupectví www.kosmas.cz

Ukázka knihy z internetového knihkupectví www.kosmas.cz Ukázka knihy z internetového knihkupectví www.kosmas.cz k á z k a k n i h y z i n t e r n e t o v é h o k n i h k u p e c t v í w w w. k o s m a s. c z, U I D : K O S 1 8 0 8 2 1 U k á z k a k n i h y

Více

Předmluva k aktuálnímu vydání Úvod k prvnímu vydání z roku Typografické a syntaktické konvence... 20

Předmluva k aktuálnímu vydání Úvod k prvnímu vydání z roku Typografické a syntaktické konvence... 20 Obsah 5 Obsah Předmluva k aktuálnímu vydání 15 1 Úvod k prvnímu vydání z roku 2000 16 Typografické a syntaktické konvence................ 20 2 Základní pojmy 21 2.1 Trocha historie nikoho nezabije................

Více

Osnova. Koncept a použití prezentací. Seznámení s pracovním prostředím MS Word Režimy zobrazení. Užitečná nastavení. Základní práce s dokumenty

Osnova. Koncept a použití prezentací. Seznámení s pracovním prostředím MS Word Režimy zobrazení. Užitečná nastavení. Základní práce s dokumenty PowerPoint 2007 Osnova Koncept a použití prezentací Seznámení s pracovním prostředím MS Word 2007 Režimy zobrazení Užitečná nastavení Základní práce s dokumenty Práce s textem a objekty Doporučení, jak

Více

Google Apps. weby 1. verze 2012

Google Apps. weby 1. verze 2012 Google Apps weby verze 0 Obsah Obsah... Úvod... Zahájení práce... Nastavení webu... Úprava stránky... Popis prostředí... Rozložení stránky... Nadpis stránky... Úprava textu... Vložení odkazu... 8 Vložení

Více

Využití OOP v praxi -- Knihovna PHP -- Interval.cz

Využití OOP v praxi -- Knihovna PHP -- Interval.cz Page 1 of 6 Knihovna PHP Využití OOP v praxi Po dlouhé teorii přichází na řadu praxe. V následujícím textu si vysvětlíme možnosti přístupu k databázi pomocí různých vzorů objektově orientovaného programování

Více

Ukazka knihy z internetoveho knihkupectvi www.kosmas.cz

Ukazka knihy z internetoveho knihkupectvi www.kosmas.cz Ukazka knihy z internetoveho knihkupectvi www.kosmas.cz O autorovi Rudolf Pecinovský patří ke špičkovým odborníkům na výuku programování. Publikoval již 39 učebnic, které byly přeloženy do pěti jazyků,

Více

PRVNÍ ELASTICKÝ INFORMAČNÍ SYSTÉM : QI

PRVNÍ ELASTICKÝ INFORMAČNÍ SYSTÉM : QI PRVNÍ ELASTICKÝ INFORMAČNÍ SYSTÉM : QI Cyril Klimeš a) Jan Melzer b) a) Ostravská univerzita, katedra informatiky a počítačů, 30. dubna 22, 701 03 Ostrava, ČR E-mail: cyril.klimes@osu.cz b) DC Concept

Více

Microsoft Access tvorba databáze jednoduše

Microsoft Access tvorba databáze jednoduše Microsoft Access tvorba databáze jednoduše Časový rozsah: 2 dny (9:00-16:00) Cena: 3300 Kč + DPH Úvod do relačních databází. Funkce databázových objektů Microsoft Access. Návrh tabulek, definice základních

Více

Elektronická podpora výuky předmětu Komprese dat

Elektronická podpora výuky předmětu Komprese dat Elektronická podpora výuky předmětu Komprese dat Vojtěch Ouška ouskav1@fel.cvut.cz 19. června 2006 Vojtěch Ouška Elektronická podpora výuky předmětu Komprese dat - 1 /15 Co je to SyVyKod? SyVyKod = Systém

Více

DSL manuál. Ing. Jan Hranáč. 27. října 2010. V této kapitole je stručný průvodce k tvorbě v systému DrdSim a (v

DSL manuál. Ing. Jan Hranáč. 27. října 2010. V této kapitole je stručný průvodce k tvorbě v systému DrdSim a (v DSL manuál Ing. Jan Hranáč 27. října 2010 V této kapitole je stručný průvodce k tvorbě v systému DrdSim a (v současné době krátký) seznam vestavěných funkcí systému. 1 Vytvoření nového dobrodružství Nejprve

Více

Ukázka knihy z internetového knihkupectví www.kosmas.cz

Ukázka knihy z internetového knihkupectví www.kosmas.cz Ukázka knihy z internetového knihkupectví www.kosmas.cz U k á z k a k n i h y z i n t e r n e t o v é h o k n i h k u p e c t v í w w w. k o s m a s. c z, U I D : K O S 1 8 1 1 4 5 Oracle průvodce správou,

Více

Úvodem... 4 Co je to vlastně formulář Co je to šablona dokumentu Jak se šablona uloží Jak souvisí formulář se šablonou...

Úvodem... 4 Co je to vlastně formulář Co je to šablona dokumentu Jak se šablona uloží Jak souvisí formulář se šablonou... Obsah Úvodem... 4 Co je to vlastně formulář... 5 Co je to šablona dokumentu... 5 Jak se šablona uloží... 6 Jak souvisí formulář se šablonou... 7 Jak se formulář vytváří... 8 Návrh formuláře... 8 Co jsou

Více

Úvodní příručka. Získání nápovědy Kliknutím na otazník přejděte na obsah nápovědy.

Úvodní příručka. Získání nápovědy Kliknutím na otazník přejděte na obsah nápovědy. Úvodní příručka Microsoft Access 2013 vypadá jinak než ve starších verzích, proto jsme vytvořili tuto příručku, která vám pomůže se s ním rychle seznámit. Změna velikosti obrazovky nebo zavření databáze

Více

Zadání maturitní práce ve školním roce 2016/2017

Zadání maturitní práce ve školním roce 2016/2017 Zadání maturitní práce ve školním roce 2016/2017 vydané podle 15 odst. 1 vyhlášky č. 177/2009 Sb., o bližších podmínkách ukončování vzdělávání ve středních školách maturitní zkouškou, ve znění pozdějších

Více

xrays optimalizační nástroj

xrays optimalizační nástroj xrays optimalizační nástroj Optimalizační nástroj xoptimizer je součástí webového spedičního systému a využívá mnoho z jeho stavebních bloků. xoptimizer lze nicméně provozovat i samostatně. Cílem tohoto

Více

M4 PDF rozšíření. Modul pro PrestaShop. http://www.presta-addons.com

M4 PDF rozšíření. Modul pro PrestaShop. http://www.presta-addons.com M4 PDF rozšíření Modul pro PrestaShop http://www.presta-addons.com Obsah Úvod... 2 Vlastnosti... 2 Jak modul funguje... 2 Zdroje dat... 3 Šablony... 4 A. Označení šablon... 4 B. Funkce Smarty... 5 C. Definice

Více

Vysoká škola báňská Technická univerzita Ostrava TEORIE ÚDRŽBY. učební text. Jan Famfulík. Jana Míková. Radek Krzyžanek

Vysoká škola báňská Technická univerzita Ostrava TEORIE ÚDRŽBY. učební text. Jan Famfulík. Jana Míková. Radek Krzyžanek Vysoká škola báňská Technická univerzita Ostrava TEORIE ÚDRŽBY učební text Jan Famfulík Jana Míková Radek Krzyžanek Ostrava 2007 Recenze: Prof. Ing. Milan Lánský, DrSc. Název: Teorie údržby Autor: Ing.

Více

Metodické listy pro předmět Aplikační software 1 (B_ASA)

Metodické listy pro předmět Aplikační software 1 (B_ASA) Metodické listy pro předmět Aplikační software 1 (B_ASA) Cíl předmětu: Cílem předmětu je prohloubit znalosti studentů ze základních aplikačních programů pod Windows, především půjde o použití pokročilejších

Více

Metodický list pro předmět Aplikační software v řízení podniku

Metodický list pro předmět Aplikační software v řízení podniku Metodický list pro předmět Anotace Cílem předmětu je prohloubit znalosti základních uživatelských aplikací a na praktických příkladech vyzkoušet pokročilejší techniky práce v aplikacích MS Word a MS Excel,

Více

Objektově orientovaný přístup

Objektově orientovaný přístup Objektově orientovaný přístup 1 Historie programovacích jazyků 1945: John von Neumann článek o nové metodě pro ukládání programů 1945: Grace Hopper poprvé termín "bug" 1946: Konrad Zuse Plankalkul - první

Více

Vytváříme prezentaci její strukturu a celkový vzhled

Vytváříme prezentaci její strukturu a celkový vzhled Vytváříme prezentaci její strukturu a celkový vzhled Práce se snímky Máme tedy spuštěný PowerPoint, otevřeli jsme nový soubor, máme patrně před sebou i první prázdný snímek, ale samozřejmě to je jen začátek.

Více

9. Rozšiřující desky Evb_Display a Evb_keyboard

9. Rozšiřující desky Evb_Display a Evb_keyboard 9. Rozšiřující desky Evb_Display a Evb_keyboard Čas ke studiu: 2-3 hodiny Cíl Po prostudování tohoto odstavce budete něco vědět o Výklad Zobrazovacích displejích Principu činnosti a programování čtyřřádkového

Více