Studijní opora pro OOM
Transkript
VŠB - Technická univerzita Ostrava Fakulta elektrotechniky a informatiky BAKALÁŘSKÁ PRÁCE Rok 2005 Jakub Černoch VŠB - Technická univerzita Ostrava Fakulta elektrotechniky a informatiky Katedra informatiky Studijní podpory pro OOM Rok 2005 Jakub Černoch Poděkování Na tomto místě bych rád poděkoval svému vedoucímu bakalářské práce, panu Ing. Michalu Radeckému za pomoc při vedení správného zpracování bakalářské práce, za pomoc při odstraňování chyb, za náměty pro zdokonalení práce. Dále bych chtěl poděkovat Cecílii Kudělové za konzultaci grafické úpravy přiložené multimediální aplikace. Prohlášení Prohlašuji, že jsem tuto bakalářskou práci vypracoval samostatně a uvedl jsem všechny literární prameny a publikace, ze kterých jsem čerpal. V Ostravě dne 6. ledna 2005 Jakub Černoch Abstrakt Každý tvůrce softwaru, který se zabývá vytvářením složitějších softwarových aplikací, se alespoň jednou v životě setká s potřebou vyvíjet svůj software co nejefektivnějším způsobem. Efektivnost v jeho případě znamená nejen přehlednost jeho vlastního produktu, ale také dynamická schopnost jeho produktu reagovat na změny prostředí, ve kterém se produkt využívá. Během jeho vytváření se také může objevit v situaci, ve které se již někdy ocitl nebo v situaci, ve které by uvítal zjednodušení svého vlastního návrhu, aniž by byl nucen zavrhnout ten dosavadní a vytvořit úplně nový. Popsanou problematikou se zabývá právě tato bakalářská práce. Jejím cílem je na úvod seznámit s vytvořenou přiloženou multimediální aplikací pro studijní podporu a následně uvést tvůrce softwarových aplikací do problematiky základních a pokročilejších termínů objektově orientovaných metod související s fázemi vývoje softwarového díla podle standardu UML. Termíny jsou využity v poslední části práce, která se zabývá vlastními návrhovými vzory. Klíčová slova multimediální aplikace, objektově orientované metody, fáze vývoje softwarového díla, návrhový vzor 1 OBSAH OBSAH..................................................................................................................................1 ÚVOD ....................................................................................................................................2 MULTIMEDIÁLNÍ APLIKACE ...........................................................................................3 Logická struktura multimediální studijní podpory .............................................................................. 4 Implementace......................................................................................................................................... 4 OBJEKTOVĚ ORIENTOVANÉ METODY ........................................................................17 Úvod do problematiky ................................................................................................................17 Základní pojmy.................................................................................................................................... 17 Rational unified process (životní cyklus vývoje softwarového díla).................................................... 18 Unifikovaný proces vývoje aplikací pro OOM ..........................................................................19 Požadavky ............................................................................................................................................ 19 Analýza ................................................................................................................................................ 24 Návrh ................................................................................................................................................... 33 Implementace ....................................................................................................................................... 36 NÁVRHOVÉ VZORY ..........................................................................................................39 Úvodem .......................................................................................................................................39 Co je návrhový vzor? ........................................................................................................................... 39 Popis návrhových vzorů ...................................................................................................................... 39 Jak návrhový vzor vybírat................................................................................................................... 40 Katalog návrhových vzorů .........................................................................................................40 Návrhové vzory Tvořivé (creational patterns) .................................................................................... 40 Návrhové vzory Strukturální (structural patterns) ............................................................................ 55 Návrhové vzory Chování (behavioral patterns) .................................................................................. 77 ZÁVĚR .............................................................................................................................. 100 PŘÍLOHY .......................................................................................................................... 101 LITERATURA ................................................................................................................... 101 2 ÚVOD V softwarovém světě se můžeme setkat s širokou škálou programovacích technik. Jednou z velmi rozšířených je objektově orientované programování. Pro perfektní ovládání umění objektově orientované programování je důležité osvojit si specifika zmíněného programování. Nejedná se pouze o vlastní psaní zdrojového kódu, ale také o možnost znovupoužitelnosti takto naprogramovaných aplikací. Než přikročíme k vlastnímu tématu zadání práce, budete na úvod seznámeni s přiloženou multimediální podporou. Následně budete seznámeni se základními i pokročilejšími pojmy z oblasti objektově orientovaných metod, které vám pomohou lépe porozumět poslední kapitole týkající se návrhových vzorů, které se v objektovém programování hojně využívají. 3 MULTIMEDIÁLNÍ APLIKACE Na výběr pro vytvoření multimediální studijní podpory jsem měl hned několik produktů. Pro zpracování vizuální podoby jsem po úvaze nakonec použil program Macromedia Flash MX Professional 2004. Flash jsem vybral hned z několika důvodů: - široké možnosti v oblasti grafických úprav - interaktivita v komunikaci s uživatelem možnost vytvoření obsahu materiálu, který je pak možné dynamicky upravovat, pokud dojde k jeho změně - velký výběr exportovaných formátů (html, exe, swf ...) - kvalitní přehledná struktura, co se týče knihovny, časové osy ve vlastním programu Prvním krokem bylo vytvoření grafického návrhu této aplikace tak, aby z ní bylo možné studovat. Usoudil jsem, že nejlepší bude, aby ovládání a přesun mezi kapitolami byl logický a inteligentně strukturovaný. Taky jsem ponechal velký prostor pro samotné texty s obrázky a ovládání. Po vykreslení grafických prvků jsem implementoval první animace tlačítek a přiřadil jim první action skripty. Jakmile všechna tlačítka reagovala správným způsobem, do souborů kapitol jsem vložil komponenty TextArea (viz. dále). Následně jsem jim přiřadil action skripty pro načítání externího obsahu a tento externí obsah jsem vytvořil. Obsahovalo to vytvoření textových souborů příslušných názvů, které obsahovaly teorii, kterou jsem chtěl zobrazit. Do textu jsem také vložil html tagy, které umožnily lepší přehlednost textu tím, že jej naformátovaly. Napomohl také externí soubor s kaskádovými styly, který jsem umístil do adresáře css. Po sepsání veškerého nutného obsahu, včetně úpravy obrázků, jsem do aplikace doplnil čas a datum, základní informace o mě jako autorovi, indikaci načítání Loaderů (viz. dále) a úvodní animaci pro spuštění aplikace. Výslednou multimediální studijní podporu jsem exportoval do html formátu, tedy webovských stránek a do formátu exe, což je spustitelný program pod windows obsahující vlastní flash přehrávač nutný pro spuštění. Multimediální studijní podpora je spustitelná na každém počítači, který má nainstalovaný Macromedia Flash Player 7 a vyšší řady, který lze spustit bez hardwarových problémů. Taky je nutné mít kompletní adresářovou strukturu celé multimediální studijní podpory. Multimediální studijní podpora se spouští následujícím způsobem: - html stránky: v adresáři s html soubory najděte „index.html“. Po odkliknutí se spustí váš webovský prohlížeč. Pokud nemáte nainstalován zmíněný Macromedia Flash Player 7 a výše, budete přesměrování na stránku, která vám umožní instalaci tohoto pluginu. Pokud Flash Player nainstalován máte, po chvíli budete přesměrování na samotné swf soubory, konkrétně na main.swf. Pokud i přesto, že máte určitě nainstalován Flash Player nelze aplikaci spustit, můžete ji spustit přes soubor s názvem „index_content.html“. - exe soubor: v adresáři s exe soubory najděte „main.exe“. Po odkliknutí se spustí Flash Player, který vám umožní další práci s materiálem Osobně bych doporučil také se zaměřit na výběr rozlišení obrazovky. Moje doporučení pro minimální rozlišení je 1024x768 pixelů, nejlépe v barvách. Pokud budete multimediální studijní podporu spouštět v tomto rozlišení v Internet Exploreru, doporučuju jej přepnout na celou obrazovku tlačítkem F11 (zpět opět F11). Tím se vyhnete možným posouváním posuvníku u zmíněného prohlížeče. 4 Logická struktura multimediální studijní podpory Teoretický text je rozdělen do dvou hlavních kapitol: Objektově orientované metody (OOM) a Katalog návrhových vzorů (KNV). Každá z těchto hlavních kapitol obsahuje teorii k danému tématu. Kapitola OOM je zpracováním pouze informativním, tedy připomíná pouze některé postupy či termíny související s objektově orientovaným programováním či s vývojem softwaru. Tyto termíny se mohou využit v rozsáhleji zpracované kapitole KNV. Základní struktura multimediální studijní podpory vypadá následovně: Každá z kapitol Úvod, Požadavky, Analýza, Návrh, Implementace obsahuje podkapitoly podle své teorie. Každá z kapitol návrhových vzorů obsahuje následující podkapitoly: Účel, Motivace, Použití, Struktura, Důsledky, Implementace, Příklad, Vzory v praxi. Implementace Všechny kapitoly jsou uspořádány v logicky uspořádané adresářové struktuře, které obsahují textové soubory obsahující teorii jednotlivých kapitol, obrázky k jednotlivým tématům a flashové spustitelné soubory vlastní grafiky. Obsah textových souborů, které využívají jednoduché html formátovací tagy a kaskádové styly, je možné dynamicky pozměňovat podle potřeby nebo z důvodů aktualizace informací v nich obsažených, včetně přiložených obrázků. Každý textový soubor má svůj jedinečný název podle své kapitoly či podkapitoly. Je to soubor se sice ukládá jako txt, ale sám obsahuje html kód. Každý textový soubor je načítán do tzv. TextArea, což je komponenta Flashe pro textový obsah. U této komponenty je možné nastavit vzhled, velikost, jestli se text v ní zobrazený bude formátova jako html, zalamování řádků apod. V následujícím textu je ukázka nastavení komponenty TextArea použitá v aplikaci. U TextArea se nastavuje barva a přiřazuje konkrétní styl z kaskádových stylů (viz. níže). tacolor = 0xCCCCCC; tasadaucel_txt.backgroundColor = tacolor; tasadaucel_txt.styleSheet = styles_css; Následuje jedna ukázka načítání textového souboru do TextArea s názvem „tasadaucet_txt“ strukturálního návrhového vzoru Adapter, kapitola Účel: //styl pro textarea tasadaucel_txt.backgroundColor = tacolor; tasadaucel_txt.styleSheet = styles_css; 5 //text pro text area se nejprve nacte do promenne a pak priradi konkrétní komponente TextArea var tasadaucel_lv:LoadVars = new LoadVars(); tasadaucel_lv.load("design_patterns/structural/adapter/sada_ucel.txt "); tasadaucel_lv.onLoad = function(success:Boolean) { if(success) { tasadaucel_txt.text = this.content; } else { trace("unable to load text file"); } }; Při aktualizaci textových souborů je důležité dodržet následující podmínky: začátek každého souboru musí začínat tímto řetězcem „&content=“ - Ukázka z textového souboru: &content=<p class='headline'>Účel</p><br> <p>Zapouzdří žádost do objektu a tím umožní...... je nutné dodržet adresářovou strukturu všech souborů - soubor nesmí pro správné zobrazení celého textu obsahovat znaky, které se využívají pro načítání souboru nebo jeho formátování; např. +, <, & apod. (využijte tyto náhradní symboly: v pořadí PLUS, OS, AA) je velmi důležité a pro zobrazení českých znaků nutné dodržet kódování textového dokumentu, které nese označení UTF-8 při ukládání nesmíme zapomenout na dodržení názvu souboru (každý název nese označení kapitoly a podkapitoly) Dalším důležitým souborem je soubor s kaskádovými styly „styles.css“, který je umístěn v adresáři „css“. Kaskádové styly jsou určeny pro textový html obsah výše zmíněné komponenty TextArea. Neexistence souboru s kaskádovými styly zapříčiní nenaformátovaný text ve všech komponentách TextArea všech kapitol multimediální studijní podpory. Následuje zdrojový kód flashových souborů, který načítá kaskádový styl, který je následně využit u všech TextArea u všech kapitol: //styl text area var styles_css = new TextField.StyleSheet(); styles_css.load("css/styles.css"); styles_css.onLoad = function(success:Boolean) { if(!success) { trace("error loading css file..."); } }; 6 Je vytvořena proměnná „styles_css“, do které se nahraje příslušný kaskádový styl. Pokud není načtení úspěšné, vypíše se programátorovi hlášení o neúspěchu. Posledními soubory, o kterých bych se chtěl jenom okrajově zmínit, jsou obrázky k některým podkapitolám. Všechny obrázky jsou uloženy do adresáře images v adresáři kapitoly, ke které mají vztah. V souvislosti s obrázky a jejich případnou aktualizací bych zmínil pouze následující tři doporučení: - v případě jejich úprav dbejte na to, aby byly hlediska místa na disku co nejmenší, ne na úkor kvality – čím menší obrázky budou, tím rychleji se budou načítat - jejich horizontální rozlišení by nemělo přesáhnout 700px – to je z důvodů velikosti komponenty TextArea u všech podkapitol - aby byly správně načteny všechny obrázky, měly by být ve formátu jpg Hlavní „main.swf“ soubor obsahuje časovou osu, na které se mimo úvodní animace objevuje pouze jedna oblast s komponentou Loader, která pomocí action scriptu načítá externí swf soubory podkapitol podle zmáčknutého tlačítka. Je to jednodušší a úspornější řešení než mít na časové ose desítky komponent Loader. Tím by vzrostla velikost hlavního „main.swf“ souboru a dosáhlo by se nepřehlednosti. Soubory kapitol (např. „command.swf“ z kapitoly Návrhové vzory chování) mají na rozdíl od hlavního souboru více oblastí. Jejich počet závisí na počtu podkapitol. Každá tato oblast má svou komponentu TextArea, která má svůj jedinečný název a do které se mohou nerušeně načítat externí data. Tímto je možné zasáhnout do jednotlivých oblastí bez ovlivnění těch druhých. 7 Po úvodní krátké animaci (což jsou grafiky nebo MovieClipy rozpohybované pomocí změn souřadnic, kterým může být přidána animovaná vlastnost – jas, alpha, barva…) se objeví úvodní obrazovka: Tento zdrojový kód načítá úvodní obrazovku: //autoload hlavniho loaderu this.mainLoader_ldr.autoLoad = false; 8 //nacteni uvodni obrazovky _root.mainLoader_ldr.contentPath = "main/hello.swf"; _root.mainLoader_ldr.load(); Vpravo nahoře je dynamické textové pole, ve kterém se vám objeví nadpis hlavní kapitoly, ve které se zrovna pohybujete – O aplikaci, Oom nebo Návrhové vzory. Text do tohoto textového pole se načítá následovně (v našem případě se na posledním řádku načte text NÁVRHOVÉ VZORY): mainName = new Array(); mainName[0] = "O APLIKACI"; mainName[1] = "OBJEKTOVĚ ORIENTOVANÉ METODY"; mainName[2] = "NÁVRHOVÉ VZORY"; main.text = mainName[2]; Nad tímto textovým polem je aktuální čas, který umožňuje mít přehled nad dobou strávenou při studiu. V prvním okénku animace se načte aktuální čas: cas = new Date(); this.dat_txt.text = cas.getDate() + ". " + (cas.getMonth() + 1) + ". " + cas.getFullYear() + ", " + cas.getHours() + ":" + cas.getMinutes() + ":" + cas.getSeconds(); A pomocí druhého okénka animace se znovu a znovu aktuální čas obnovuje: gotoAndPlay(1); Při načítání externích souborů, vyjma těch textových a obrázků, se vedle datumu pokud zrovna načítáte nějakou z hlavních kapitol, objeví „LOADING...“. Toto oznámení je zde uvedeno z důvodů pomalého načítání některých větších externích swf souborů. Indikuje, že komponenta Loader načítá obsah, i když se na obrazovce zrovna nic neobjevilo po stisku některého z tlačítek. Je to instance textové pole s názvem „loading_txt“, které má programově nastaveno naslouchání událostí, jež se dějí v komponentě Loader. Následující zdrojový text ukazuje příklad naslouchání: //informace o loadovani (text loading...) loadListener = new Object(); loadListener.progress = function() { _root.loading_txt._visible = true; }; loadListener.complete = function(eventObj) { _root.loading_txt._visible = false; }; //nove naslouchani udalosti pri dokonceni loadovani _root.mainLoader_ldr.addEventListener("complete", loadListener); Je vytvořen nový objekt, kterému jsou přiřazeny nové funkce pro naslouchání během načítání (progress) a pro naslouchání na konci načtení (complete). Tyto jsou pak přiřazeny konkrétnímu Leaderu, jenž tyto funkce použije. Světle šedé pole – je zde umístěna flash komponenta Loader – umožňuje načítat externí flashový obsah nebo obrázky. V našem případě načítá jednotlivé kapitoly. Výhodou je možnost dynamického načítání dat. Není nutné, aby se všechna data načetla do paměti, dokud si to uživatel nevyžádá (výhoda pro umístění na webovou síť). 9 Pro všechna načítání je použit podobný zdrojový kód, který je uveden níže. Liší se pouze názvem načítaného souboru a názvy instancí (zvýrazněno kurzívou), které načítání odstartují: this.fieldAbout_mc.obsah_mc.obsah_btn.onRelease = function() { _root.mainLoader_ldr.addEventListener("progress", loadListener); _root.mainLoader_ldr.addEventListener("complete", loadListener); mainLoader_ldr.contentPath = "info/obsah.swf"; mainLoader_ldr.load(); }; Opět se zde využívá naslouchání událostí pro informaci o načítání (obě funkce pro naslouchání událostí Loaderu jsou uvedené výše). Vlevo nahoře jsou tři hlavní tlačítka – O aplikaci, Oom, Návrhové vzory. Po jejich stisku se nic nenačítá. Pouze po najetí myši se rozbalí příslušné podnabídky. Tyto podnabídky jsou instance MovieClip s vlastní časovou osou. Pro lepší orientaci v pohybu jsou určité části časové osy pojmenovány (slidedown, slideup apod.). Částí řešení komunikace mezi jednotlivými podnabídkami jsou cykly, které kontrolují stavy jednotlivých instancí MovieClip. up = "slideup"; down = "slidedown"; //hlavni nabidka About this.about_btn.onRollOver = function() { if (aboutmenu == 0) { fieldAbout_mc.gotoAndPlay(down); aboutmenu = 1; } if (oommenu == 1) { fieldOom_mc.gotoAndPlay(up); oommenu = 0; } if (patmenu == 1) { fieldPattern_mc.gotoAndPlay(up); patmenu = 0; } }; 10 Pro komunikaci instancí klipu MovieClip se navíc využívá neviditelné tlačítko. Pomocí neviditelného tlačítka je řešeno rozbalování a sbalování podmenu. Neviditelné proto, že obsahuje nějakou grafiku pouze ve stavu Hit (tlačítka ve flashi mají tyto stavy: Up – počáteční, Down – myš klikne, Over, myš přes něj přejede, Hit – aktivní oblast tlačítka pro kliknutí), takže je mu dána pouze oblast aktivity. V actionscriptu se uvede, že po nájezdu myši na toto neviditelné tlačítko se dá vědět výsuvným menu a ta se uvedou do stavu vysunutého nebo zasunutého. Výpis funkčnosti neviditelného tlačítka z hlavního „main.swf“ souboru je následující: //promenne pro ovladani tlacitek var aboutmenu:Number = 0; var oommenu:Number = 0; var patmenu:Number = 0; var behfield:Number = 0; var crefield:Number = 0; var strfield:Number = 0; //obsluha neviditelneho tlacitka invField_btn this.invField_btn.onRollOver = function() { if (aboutmenu == 1) { fieldAbout_mc.gotoAndPlay(up); aboutmenu = 0; } if (oommenu == 1) { fieldOom_mc.gotoAndPlay(up); oommenu = 0; 11 } if (patmenu == 1) { fieldPattern_mc.gotoAndPlay(up); patmenu = 0; } if (behfield == 1) { chapterBehavioral_mc.gotoAndPlay(left); behfield = 0; } if (crefield == 1) { chapterCreational_mc.gotoAndPlay(left); crefield = 0; } if (strfield == 1) { chapterStructural_mc.gotoAndPlay(left); strfield = 0; } }; Neviditelné tlačítko má jednu nevýhodu. Protože jsem TextArea podkapitol řešil tak, aby bylo možné označit text a kopírovat jej do schránky, může se stát, že některé výsuvné menu zůstane viset ve vzduchu. Tento stav setrvá dokud se myší nenajede nad neviditelné tlačítko. Je to ta modrozelená oblast přibližně po obvodu aplikace na následujícím obrázku. Návrhové vzory mají navíc ještě svoje podnabídky podle tří hlavních rozdělení návrhových vzorů. Jejich pohyb je řešen podobně jako u hlavní nabídky s tím rozdílem, že nyní se pohybují doleva nebo doprava a navíc jsou maskována jinou vrstvou, aby byla vidět až ve vhodnou chvíli: 12 left = "slideleft"; right = "slideright"; this.fieldPattern_mc.behavioral_mc.behavioral_btn.onRollOver = function() { if (behfield == 0) { _root.chapterBehavioral_mc.gotoAndPlay(right); behfield = 1; } if (crefield == 1) { _root.chapterCreational_mc.gotoAndPlay(left); crefield = 0; } if (strfield == 1) { _root.chapterStructural_mc.gotoAndPlay(left); strfield = 0; } }; Z úvodní obrazovky se můžete vzápětí přesunout do nabídky obsahu. Stejná možnost je přes tlačítka O aplikaci – Obsah. Z obsahu se můžete dostat do jakékoliv kapitoly, do které se můžete dostat přes tlačítkovou nabídku. Tato aktivita je opět řešena neviditelnými tlačítky (v aplikaci nejdou vidět). Nyní však ale odkazují na externí soubory, které se načtou do hlavního Loaderu: //ovladani obsahu o aplikaci 13 this.obpouziti_btn.onRelease = function() { _root.mainLoader_ldr.contentPath = "info/about.swf"; _root.mainLoader_ldr.load(); }; a podobně Poslední zmínku ze souboru „main.swf“ jsem si ponechal na údaje o mě jako autorovi. K nim se lze dostat po stisknutí tlačítka O AUTOROVI. Je to MovieClip, který obsahuje příslušné tlačítko s animací a textové pole s údaji o autorovi, které se vysune nebo zasune po stisknutí tlačítka. Text je maskován, aby nedošlo k jeho zviditelnění, kdy si to nepřejeme. Kontakt je aktivní text. Po kliknutí se otevře váš emailový klient, který vám umožní napsat autorovi. 14 Pro ovládání tlačítka a textového pole, tedy celého MovieClipu slouží následující zdrojový kód: //tlacitko o autorovi this.oautorovi_mc.oautorovi_btn.onRelease = function() { if (_root.oautorovi_mc._currentframe == 1) { _root.oautorovi_mc.gotoAndPlay(up); } else if (_root.oautorovi_mc._currentframe == 8) { _root.oautorovi_mc.gotoAndPlay(down); } }; Po stisknutí tlačítka se kontroluje, na kterém snímku se instance MovieClipu nachází a podle toho jej tlačítko ovládá nahoru nebo dolů. Nevyužívá se neviditelné tlačítko. Proto je nutné pro zasunutí opětovné kliknutí. Funkčnost jednotlivých kapitol si ukážeme na kapitole Flyweight ze strukturálních návrhových vzorů. 15 První obrazovkou je hlavní nadpis. Vpravo nahoře jsou šipky pro postupný pohyb mezi jednotlivými podkapitolami. Obsahují jednouchý kód přičítající nebo odečítající aktuální snímek z časové osy: on (release) { gotoAndPlay(_currentframe - 10); } Počet podkapitol lze snadno uhádnout podle počtu kulatých tlačítek, které jsou navíc pro jednoduchost orientace očíslovány. Jejich animace fungují na základě čtyř zmíněných stavů pro tlačítka. Po nájezdu na ně, se objeví další možnost přesunu mezi jednotlivými podkapitolami. Tyto vysouvající se tlačítka jsou maskována a lze je vidět pouze po rozbalení. Jejich funkčnost a komunikace mezi nimi je řešena podobně jako funkčnost hlavní nabídky, tedy pomocí neviditelného tlačítka, které je kolem tohoto menu. Nad oblastí kulatých tlačítek je místo pro název kapitoly a její podkapitoly. Je to dynamické textové pole, do kterého se načítá text z pole hodnot: //pole nazvu kapitol chapterNumber = new Array(); chapterNumber[0] = "1. "; chapterNumber[1] = "2. "; chapterNumber[2] = "3. "; chapterNumber[3] = "4. "; chapterNumber[4] = "5. "; chapterNumber[5] = "6. "; chapterNumber[6] = "7. "; 16 chapterNumber[7] = "8. "; chapterName = new Array(); chapterName[0] = ": účel"; chapterName[1] = ": motivace"; chapterName[2] = ": použití vzoru"; chapterName[3] = ": popis struktury"; chapterName[4] = ": důsledky použití vzoru"; chapterName[5] = ": implementace"; chapterName[6] = ": příklad"; chapterName[7] = ": vzory v praxi"; patternName = "Flyweight"; Tyto hodnoty se pak v dynamickém textovém poli objevují podle skriptu: chapter.text = chapterNumber[0] + patternName + chapterName[0]; Největší oblast je věnována komponentě TextArea, do které se dynamicky načítají textové html soubory. Každá tato komponenta má v dané podkapitole svůj jedinečný název, podle kterého se na ně odkazu v actionscriptu. Pro úpravu ve flashi je dobré vědět, že pro každý soubor existuje knihovna. Tato knihovna obsahuje všechny použité grafiky, movie klipy, buttony či komponenty, které jsou v daném souboru využity. Knihovnu jsem uspořádal podle druhu komponenty a pak podle toho, čemu náleží, jestli pozadí, tlačítkům…: 17 OBJEKTOVĚ ORIENTOVANÉ METODY Úvod do problematiky Základní pojmy Co je to objektově orientovaná metoda Vlastní definici objektově orientované metody předřadíme zavedení následujících základních pojmů vztažených a definovaných v kontextu softwarového inženýrství: 1. metoda je promyšlený postup vývoje softwarového systému 2. architektura je organizovaná struktura a s ní spjaté chování systému 3. objekt je entita s jasně definovanou hranicí a identitou zahrnující její chování a stavy 4. objektově orientovaná architektura systému je tedy struktura propojených objektů, které prostřednictvím vzájemné komunikace (interakce) definují výsledné chování celého systému Z výše uvedeného tedy vyplývá, že objektově orientovaná metoda je promyšlený způsob vývoje softwarového systému postaveného na objektově orientované architektuře. Metoda reprezentuje ideální postup, který jednotlivé firmy podmínek do podoby svého tzv. softwarového procesu, tedy realizaci jednotlivých softwarových projektů. Vztah metody a dokumentovat na analogii s laboratorním vývojem (metoda) proces) nějakého produktu. přizpůsobují podle svých konkrétních podnikového procesu používaného k softwarového procesu tak lze nejlépe a průmyslovou výrobou (softwarový Co je účelem použití objektově orientované metody Účelem objektově orientovaných metod je dát softwarovým inženýrům k dispozici dobře definovaný popis cesty jak vyvíjet softwarové systémy objektově orientované architektury, a zajistit tak maximální kvalitu vytváření všech softwarových artefaktů spjatých s tímto vývojem. Softwarový produkt není dán pouze výsledným kódem, ale je tvořen celou řadou dokumentů popisujících specifikace požadavků, modely analýzy a návrhu, stejně jako popis fyzické architektury systému a způsob jeho rozmístění u zákazníka. Metody mohou být neformální, bez přesně definované syntaxe a sémantiky, nebo naopak formální, kdy je syntaxe a sémantika přesně daná. Náročnost zvládnutí metody stoupá s mírou formalizace dané metody. Čistě formální metoda nutně musí vycházet z matematicky definovaných formulí a bývá pro běžnou praxi díky své náročnosti hůře použitelná. Kompromisem jsou metody semiformální, které sice nejsou postaveny na jazyce matematiky, ale využívají přesně daného specifikačního jazyka (obvykle grafického), který je kompromisem přijatelným pro širokou obec softwarových inženýrů. Historie vývoje objektově orientovaných metod Historie vývoje objektově orientovaných je stejně stará jako vývoj jazyků používaných pro objektově orientované programování. Tyto metody vznikaly z důvodů zajištění metodiky efektivního použití těchto jazyků. Od druhé poloviny 80. let se tak objevila celá řada metod, které doznaly svého uplatnění v praxi. Ty nejvýznačnější, které se staly základem jazyka UML (Unified Modeling Language) jsou metoda OMT (Objekt Modeling Technique), Boochova metoda a OOSE (ObjectOriented Software Engineering). V dnešní době je standardem pro výše uvedené účely vývoje jazyk UML, který navazuje na historicky úspěšné metody z první poloviny devadesátých let a jehož řízená specifikace je garantována skupinou OMG (Object Management Group). Oficiální dokumentace specifikace jednotlivých verzí 18 jazyka UML jsou k dipozici na webových stránkách OMG na adrese http://www.uml.org. Rational unified process (životní cyklus vývoje softwarového díla) Iterace, fáze a cykly vývoje softwarového díla Tento proces definuje při vývoji softwaru otázky kdo, co, kdy a jak. V současném období, kdy se předmětem vývoje staly softwarové systémy vysoké úrovně sofistikace, je nemožné nejprve specifikovat celé zadání, následně navrhnout jeho řešení, vytvořit softwarový produkt implementující toto zadání, vše otestovat a předat zadavateli k užívání. Jediným možným řešením takového problému je přístup postavený na postupném upřesňování cílového produktu cestou jeho inkrementálního rozšiřovaní z původní hrubé formy do výsledné podoby. Softwarový systém je tak vyvíjen ve verzích, které lze průběžně ověřovat se zadavatelem a případně je pozměnit pro následující cyklus. Samotný životní cyklus vývoje softwarového díla je rozložen do čtyř základních fází (zahájení, rozpracování, tvorba a předání), přičemž pro každou z nich je typická realizace několika iterací umožňujících postupné detailnější rozpracování produktu. Každý cyklus vede k vytvoření takové verze systému, kterou lze předat uživatelům, a která implementuje jimi specifikované požadavky. Iterace je pak úplná vývojová smyčka vedoucí k vytvoření spustitelné verze systému reprezentující podmnožinu vyvíjeného cílového produktu. Tento je postupně rozšiřován každou iterací až do výsledné podoby. V rámci každé iterace proběhnou činnosti vázané na byznys modelování, následují specifikace požadavků, analýza a návrh, implementace, testování a nasazení (instalace). K tomu probíhá celá řada podpůrných činností týkajících se správy konfigurací, řízení projektu a přípravy prostředí, ve kterém je systém vyvíjen a nasazen. Popis procesů a jimi vytvářených modelů Z výše uvedeného tedy vyplývá, že vývoj softwarového systému je dán celou řadou v iteracích prováděných činností uspořádaných do následujících procesů charakteristických dle účelu svého použití: 1. byznys modelování popisuje strukturu a dynamiku podniku či organizace 2. specifikace požadavků definuje funkcionalitu softwarového systému cestou specifikace tzv. případů použití 3. 4. integraci analýza a návrh se zaměřuje na specifikaci architektury softwarového produktu implementace reprezentuje vlastní tvorbu softwaru, testování komponent a jejich 5. testování se zaměřuje na činnosti spjaté s ověřením správnosti řešení softwaru v celé jeho složitosti 19 6. nasazení se zabývá problematikou konfigurace výsledného produktu na cílové počítačové infrastruktuře Unifikovaný proces vývoje aplikací pro OOM Požadavky Požadavky a jejich specifikace Většina prací spojená s definicí a specifikací požadavků je vykonána ve fázích začátku RUP. Následující metamodel ukazuje, že existují dva způsoby zachycení požadavků: - funkční a nefunkční požadavky - případy užití a účastníci Detail pracovního postupu tvorby požadavků v metodice RUP obsahuje následující aktivity, které nás jako objektově orientované analytiky budou zajímat: - vyhledání účastníků a případu užití - detaily případu užití - struktura modelu případů užití Standardní pracovní postup tvorby požadavků je rozšířeno tímto způsobem: - účastník: tvůrce požadavků - aktivita: vyhledání funkčních požadavků - aktivita: vyhledání nefunkčních požadavků - aktivita: stanovení priorit jednotlivých požadavků - aktivita: sledování požadavků až k případům užití Význam požadavků je dalekosáhlý. Přibližně 25% projektů končí nezdarem v důsledku problémů, které vznikly v procesu inženýrství požadavků. Existují dva typy požadavků: 1. funkční požadavky, které popisují požadovanou službu systému 20 2. nefunkční požadavky, které definují vlastnosti systému a omezení, za nichž musí systém pracovat Správně formulované požadavky by měly být vyjádřeny jednoduchým strukturovaným jazykem s použitím klíčového slova „bude“, aby je bylo možné v nástrojích pro podporu inženýrství požadavků snadno zpracovat. Např. <id><system>bude<funkce> (jedinečný identifikátor-název systému-klíčové slovo-funkce). Specifikace systémových požadavků obsahuje funkční i nefunkční požadavky na vznikající systém. Specifikace může mít podobu dokumentu nebo databáze v nástroji pro správu požadavků. To, že máme mapu systému neznamená ještě, že máme systém samotný. Naše volba v nepřeberném množství informací musí projít následujícími stavy: - odstraněním (odfiltrování informací) - deformací (což jsou pozměněné informace) - zobecněním (pravidla, víra, zásady, které byly vytvořeny o pravdě a nepravdě) Univerzální kvantifikátory (např. všichni, někteří, nikdo) mohou naznačovat hranice omezenosti vidění daného systému. Je třeba prověřit všechny možnosti. Modelování případů užití Modelování případů užití je součástí pracovního procesu Požadavky. Klíčovými aktivitami modelování případů užití jsou „nalezení účastníků a případů užití“ a detail případů užití. Účastník specifikuje roli, kterou určitá externí entita přijímá v okamžiku, kdy začíná daný systém bezprostředně používat. Může vyjadřovat roli uživatele, roli dalšího systému, který se dotýká hranic našeho systému. Modelování případů užití je další formou inženýrství požadavků. Tuto aktivitu lze rozdělit na následující etapy: - nalezení hranic systému - nalezení účastníků - nalezení případů užití Účastníci jsou takové role přidělené vnějším entitám, které přímo komunikují se systémem. K označení účastníka napomůže, jestliže si uvědomíme, kdo nebo co používá systém, či s ním bezprostředně komunikuje. Účastníkem může být často i abstraktní pojem, např. čas. Případy užití jsou funkce, které systém vykonává jménem jednotlivých účastníků nebo v jejich prospěch. V nalezení případů užití nám může pomoci, když si uvědomíme, jak jednotliví účastníci komunikují se systémem. Diagramy případů užití znázorňují následující: - hranice systému - účastníky - případy užití 21 - interakce K definici klíčových obchodních termínů (synonyma a homonyma, která definují systém a jeho funkčnost) nám slouží slovníček pojmů daného projektu. Pro specifikaci případů užití použijeme následující záchytné body: název, jedinečný identifikátor, vstupní podmínky (omezení, která ovlivňují realizaci případů užití), tok událostí (posloupnost časově uspořádaných kroků případů užití) a výstupní podmínky. Počet případů užití můžeme omezit omezeným počtem rozvětvení jejich toků událostí: (když) - u větví, které nastanou v určitém kroku hlavního toku, použijeme klíčové slovo „if“ potřebujeme-li zachytit rozvětvení, použijeme oddíly „alternative flow“ (alternativní tok) Opakování uvnitř toku událostí můžeme vyjádřit pomocí: - for (výraz iterace) - while (booleovská podmínka) Komplexní případy užití lze a často je vhodné rozložit na několik scénářů. Každý komplexní případ užití obsahuje jeden hlavní scénář, který předpokládá, že během případu užití nedojde k ničemu nepředvídanému. A nakonec každý komplexní případ užití může obsahovat jeden nebo více vedlejších scénářů, což jsou alternativní cesty pro případy výjimek, rozvětvení nebo přerušení. Vedlejší scénáře lze najít prozkoumáním hlavního scénáře, ve kterém se budou vyhledávat alternativy, možné chyby a přerušení. Je vhodné vytvářet vedlejší scénáře pouze pokud to hlavní scénář nějak obohatí. Modelování případů užití je vhodné u systémů, v nichž převládají funkční požadavky nebo v nichž se vyskytuje mnoho účastníků nebo je v systému obsaženo mnoho rozhraní k dalším systémům. V opačném případě buď vymodelovat jednoduché případy užití nebo je nemodelovat vůbec. Pokročilé modelování případů užití K vyčlenění chování, které je společné dvěma a více účastníkům se často používá generalizace (zobecnění) účastníků do jednoho rodičovského účastníka. Rodičovský účastník je obecnější než jeho potomci a potomci jsou specializovanější než jejich předek. Zděděného účastníka (potomka) můžeme obsadit všude tam, kde bychom mohli očekávat i výskyt rodičovského účastníka. Rodičovský účastník je obvykle abstraktní (specifikuje abstraktní roli). 22 Zdědění účastníci jsou většinou konkrétní (specifikují konkrétní roli). Zobecněním účastníků lze případy užití radikálně zjednodušit. Zobecnění případů užití umožňuje funkce společné více případům užití vyčlenit do rodičovského případu užití. Odvozené případy užití dědí všechny vlastnosti a funkce od svých předků (účastníky, relace, vstupní a výstupní podmínky, tok událostí, alternativní toky...). Odvozené případy užití mohou být doplněny o nové funkce a vlastnosti. Odvozené případy užití mohou překrývat charakteristiku zděděnou od svých předků: - relace s účastníky nebo s jinými případy užití mohou být děděny nebo přidávány - vstupní a výstupní podmínky mohou být děděny, překrývány nebo přidávány kroky v hlavním toku nebo v alternativních tocích mohou být děděny, překrývány nebo přidávány - atributy mohou být děděny nebo přidávány - operace mohou být děděny, překrývány nebo přidávány K velmi dobrému zvyku patří, že rodičovské případy užití jsou obvykle abstraktní. Relace <<include>> umožňuje kroky opakující se v několika tocích případů užití vyčlenit do samostatného případu užití, který lze zahrnout v případě potřeby do bázového případu užití. Klíčové slovo include(název případu užití) se používá k zahrnutí chování jiného případu užití. Případ užití, který je jiným případem užití zahrnován, se označuje jako dodavatelský. Klient není úplný bez svých dodavatelů. 23 Dodavatelé mohou být: - úplní (v tomto případě jsou to normální případy užití a lze vytvářet jejich instance) - neúplní (obsahují pouze část chování a nelze vytvářet jejich instance) Další relací je relace <<extend>>, která přidává do existujícího případu užití nové chování. Existující případ užití obsahuje body rozšíření (extension points), které jsou umístěny v samostatné vrstvě překrývající hlavní tok událostí. Body rozšíření jsou umístěny mezi očíslovanými kroky toku událostí. Rozšiřující případy užití poskytují vkládané segmenty. Jsou to části chování, které lze zahrnout do bodu rozšíření. Existující případ užití je úplný i bez vkládaných segmentů. Rozšiřovaný případ užití neví nic o možných bodech rozšíření. Poskytuje pouze prostředek pro jejich umístnění. Rozšiřující případ užití je obvykle neúplný. Často se skládá z jednoho vkládaného segmentu (insertion segment). Obsahuje-li rozšiřující případ užití vstupní podmínky, musí být splněny, jinak ke spuštění rozšiřujícího případu nedojde. Výstupní podmínky rozšiřující případu užití omezují stav systému po vykonání rozšiřujícího případu užití. Rozšiřující případ užití může obsahovat mnoho vkládaných segmentů. Existující případ užití může být rozšířen o dva nebo více rozšiřujících případů užití. Pořadí splnění je ale nejisté. Je možné podmínit rozšíření booleovskou funkcí. V případě nesplnění podmínky se rozšíření nevytvoří. 24 Analýza Pracovní postup Analýza spočívá v tvorbě modelů, které zachycují podstatné požadavky a charakteristické rysy požadovaného systému – analytické modelování je strategické. Pracovní postupy analýzy a požadavků se často překrývají, především ve fázi rozpracování. Možnost analýzy požadavků je obvykle výhodou, protože pomáhá v odhalení chybějících nebo zkreslených požadavků. Analytický model: - je vytvořen v jazyce daného odvětví - zachycuje problém z určité perspektivy - obsahuje artefakty, jež modelují problémovou doménu - vypráví příběh o požadovaném systému - je užitečný pro maximální počet uživatelů a zúčastněných osob Výstupem analýzy jsou dva artefakty analýzy. Jsou to analytické třídy, které tvoří klíčové pojmy v obchodní doméně. Dále jsou to realizace případů užití, jež názorně ukazují, jak mohou instance analytických tříd vzájemně komunikovat s cílem realizovat chování systému specifikované případem užití. Analýza systému zahrnuje architektonickou analýzu, analýzu případů užití, analýzu třídy a analýzu balíčku. Zavedené odhady: - analytický model středně velkého systému obvykle obsahuje přibližně 50 až 100 tříd do modelu by měly být zahrnuty pouze třídy, které modelují slovníček pojmů problémové domény - v analýze se neimplementuje - vhodné jsou minimalizace vazeb - pro přirozenou hierarchii abstrakcí využívejme dědičnost - analytický model by měl být co nejjednodušší Třídy a objekty Třídy a objekty jsou stavebními bloky všech objektově orientovaných systémů. Je tedy velmi důležité jejich správné pochopení. Objekty jsou soudržné jednotky, ve kterých se snoubí data s funkčností. Ukrývání dat uvnitř objektu se nazývá zapouzdření. To umožňuje manipulaci s objekty prostřednictvím funkcí poskytovaných příslušným objektem. 25 Operace jsou abstraktními specifikacemi pro funkce objektu vytvořené během analýzy. Metody jsou konkrétní specifikace funkcí objektu vytvořených v etapě návrhu. Každý objekt je instancí třídy (třída definuje společné vlastnosti sdílené všemi objekty dané třídy). Každý objekt se vyznačuje následujícími vlastnostmi: identita; je jedinečná identifikace existence objektu, kterou používáme jako jedinečný odkaz na specifický objekt stav; je smysluplná množina hodnot atributů objektu v určitém časovém okamžiku (přechod stavu je přesun jednoho objektu do druhého) chování; je vyjádřením služeb objektu poskytovaných dalším objektům (během analýzy je to množina operací, během návrhu jako množina metod) Vzájemná interakce jednotlivých objektů generuje výsledné chování celého systému. Interakce zahrnuje objekty zasílající, ale i přijímající zprávy. Po přijetí zprávy je automaticky zavolána odpovídající metoda, která může způsobit přechod z jednoho stavu do druhého. Každý objekt má v notaci dva oddíly: - horní obsahuje název objektu a případně název třídy (vše podtrženo) - spodní obsahuje názvy atributů a jejich hodnoty přiřazené k názvům znakem rovnítka Třída je „objekt“, který definuje charakteristické vlastnosti (atributy, operace, metody, relace, chování...) určité množiny objektů. Každý objekt je instancí právě jedné třídy. Různé objekty stejné třídy mohou mít ale různé hodnoty a tím se taky mohou různě chovat. Instanční relaci mezi třídou a jedním z jejích objektů lze vyjádřit pomocí závislosti s předdefinovaným stereotypem <<instantiate>>. Relace slouží ke spojování předmětů. Relace závislosti označuje, že změna dodavatele ovlivní klienta. 26 Tvorba instance je proces, v němž jako šablonu k tvorbě nového objektu používáme třídu. Většina orientovaných jazyků používá k tvorbě nových objektů metody zvané konstruktory. Ty nastavují nebo inicializují objekty a patří třídě. Při vymazávání objektů se mohou využívat destruktory. Ty slouží k úklidu prostředků po vymazání objektů a též patří příslušné třídě. Notace tříd. Oddíl názvu obsahuje název třídy, který začíná velkým písmenem. Každý atribut v oddílu atributů se vyznačuje následujícími vlastnostmi: typ viditelnosti, který řídí přístup k vlastnostem a funkcím třídy (+ veřejný/public; soukromý/private; # chráněný/protected, ~ balíček/package) - název, který začíná malým písmenem - násobnost (např. 2...*, 1...1 atd) - datový typ - stereotyp a označené hodnoty Každou operaci v oddílu operací charakterizují následující vlastnosti: - typ viditelnosti (viz výše) - název - seznam argumentů (název a typ jednotlivých argumentů) - typ návratové hodnoty - stereotyp - označené hodnoty Signatura operace se skládá z: - názvu - seznamu argumentů (typy všech argumentů) - typu návratové hodnoty Každá operace nebo metoda třídy musí mít jedinečnou signaturu. 27 Atributy a operace instance se vztahují pouze ke specifickým objektům. Operace instance mohou používat další atributy nebo operace instance. Operace instance mohou používat všechny atributy a operace mající platnost třídy. Atributy a operace třídy se vztahují na celou třídu objektů. Operace třídy mohou používat pouze další atributy nebo operace třídy. Konstruktory objekty vytvářejí a destruktory ruší objekty a stopy po nich. Relace Relace jsou sémantickou vazbou mezi předměty a abstrakcemi. Vazby mezi objekty se nazývají spojením. Ke spojení dochází vždy, když jeden objekt obsahuje odkaz na další objekt. Objekty mezi sebou spolupracují pomocí předávání zpráv prostřednictvím spojení. Po přijetí zprávy objekt vykoná odpovídající metodu. Objektové diagramy ukazují objekty a jejich vzájemná spojení v daném čase. Jsou to snímky běžícího objektově orientovaného systému. Každý objekt může vůči druhému hrát různé role. Více než dva objekty může spojit n-nární spojení. Pro spojení se využívají cesty (spojují modelované elementy). Je důležité zůstat u jednoho stylu zákresu cest (pravoúhlé, šikmé...). Sémantickou vazbou mezi třídami nazýváme asociace. Je-li mezi dvěma různými objekty spojení, je logická i existence asociace tříd těchto objektů. Asociace mohou mít nepovinně následující vlastnosti: název asociace (sloveso či slovesná fráze, černá šipka ukazující směr asociace, začíná malým písmenem) - názvy rolí na jednom nebo obou koncích 28 - řiditelnost (znázorněno šipkou na příslušném konci relace) násobnost (označuje interval objektů, které lze zahrnout do relace v daném čase, je to limitní hodnota existence objektů v daném čase) Asociace mezi dvěma objekty, třídami apod. je totéž, jako kdyby měla jedna třída pseudoatribut, který nese odkaz na objekt jiné třídy. Lze často zaměňovat asociace a atributy. Může existovat tzv. asociační třída, která je zároveň asociací i třídou. Může mít vlastní atributy, operace a relace. Může se použít, je-li mezi dvěma objekty přesně jedno jedinečné spojení. V případě více spojení je nutné nahradit asociační třídu třídou normální. Asociace s kvalifikátorem redukují vazbu M:N na 1:N tím, že specifikují jedinečný objekt cílové sady. Kvalifikátor musí být v cílové množině jedinečným klíčem. Relace, v nich se změna v dodavateli automaticky projeví rovněž v klientovi se nazývá závislostí. Závislosti jsou znázorňovány tečkovanými šipkami od klienta k dodavateli. U závislostí se využívá: stereotyp <<use>> (klient používá dodavatele jako argument, návratovou hodnotu nebo jako element vlastní implementace) - stereotyp <<call>> (klientská operace volá dodavatelskou operaci) stereotyp <<parameter>> (dodavatel je argumentem nebo návratovou hodnotou jedné z klientských metod) - stereotyp <<send>> (klient odesílá dodavatele k určitému cíli) - stereotyp <<instantiate>> (klient je instancí dodavatele) Závislosti, které modelují závislosti mezi předměty, které jsou na různých stupních abstrakce, se nazývají abstrakční závislosti: - stereotyp <<trace>> (klient je historickým vývojem dodavatele) - stereotyp <<refine>> (klient je další verzí dodavatele) - stereotyp <<derive>> (klient může být odvozen od dodavatele) Schopnost jednoho předmětu přistupovat k dalšímu předmětu využívá závislosti na základě oprávnění: 29 stereotyp <<access>> (závislost mezi balíčky, díky níž může klientský balíček používat veškerý veřejný obsah dodavatelského balíčku) stereotyp <<import>> (závislost mezi balíčky, v níž může klientský balíček používat veškerý veřejný obsah dodavatelského balíčku) stereotyp <<friend>> (řízené narušení zapouzdření, kdy může klient používat soukromé členy dodavatele) Dědičnost a polymorfismus Relace mezi obecnějším a přesněji specifikovaným se nazývá generalizace (zobecnění). Konkrétnější předměty jsou důsledně konzistentní s obecnějšími předměty. Obecnější předmět lze vždy nahradit konkrétnějším typem. Všechny předměty na stejné úrovni hierarchie zobecnění by měly být na stejném stupni abstrakce. K dědění tříd dochází v relaci zobecnění mezi třídami. Je asi zřejmé, že potomek dědí od svého předka (nadtřídy) následující charakteristiku – atributy, operace, relace, omezení. Potomci mohou následující: - přidat si novou charakteristiku překrývat zděděné operace (potomek definuje novou operaci se stejnou signaturou, jakou má operace předka – název operace, typ všech argumentů ve správném pořadí, návratový typ operace) Abstraktní operace nemá vlastní implementaci. Slouží jako držitel prostoru. Všechny konkrétní podtřídy (potomci) musí implementovat všechny zděděné abstraktní operace. Abstraktní třída obsahuje alespoň jednu abstraktní operaci, neumožňuje tvorbu vlastních instancí a definují dohodu jako množinu abstraktních operací, jež musí být implementovány ve všech podtřídách (potomcích). Dalším pojmem je polymorfismus (mnohotvárnost). Umožňuje návrh systémů, jež využívají abstraktní třídu, kterou pak za běhu programu nahradí jejími konkrétními potomky. Dají se pak jednoduše přidávat další podtřídy. Polymorfní operace mají více implementací. Různé třídy mohou implementovat stejnou polymorfní operaci různým způsobem. Polymorfismus umožňuje instancím různých tříd reagovat na stejnou zprávu odlišným způsobem. Realizace případů užití Realizace případů užití nám umožňuje ověření teorie v praxi a to explicitním znázorněním spolupráce skupin objektů pro dosažení požadovaného chování systému. Realizace případů užití ukazují spolupráci instancí analytických tříd pro zajištění funkčních požadavků specifikovaných v případech užití. Každá realizace by měla zachycovat přesně jeden 30 případ užití. Realizace případů užití se skládají z následujících součástí: - diagramy tříd - diagramy interakcí - uvedením speciálních požadavků - upřesňování případů užití Diagramy interakce mohou mít podobu diagramů interakce instancí nebo deskriptorů. Obecné diagramy interakce ukazují: - role klasifikátorů (role přidělované instancím klasifikátorů) - role asociací (role přidělované instancím asociací) - zprávy a tok zpráv Konkrétní diagramy interakce ukazují: - instance klasifikátorů (objekty) - spojení tvorbu a uvolnění instancí a spojení (omezení: (new) – instance je vytvořena během interakce; (destroyed) – instance je během interakce uvolněna; (transient) – instance je během interakce vytvořena a následně uvolněna) - zprávy a tok zpráv - existují tři typy toků: 1. synchronní komunikace – odesílatel čeká, dokud příjemce nedokončí úlohu 2. asynchronní komunikace – odesílatel pokračuje ve své činnosti bezprostředně po odeslání zprávy a nečeká na odpověď příjemce 3. - iteraci - větvení návrat z volání procedury Diagram spolupráce a sekvenční diagram jsou si velmi podobné. Diagramy spolupráce ale zdůrazňují spolupráci mezi objekty. Násobné objekty, které zastupují množinu objektů: kvalifikátory umožňují výběr specifického objektu z násobného objektu pomocí jedinečného identifikátoru lze předpokládat, že násobné objekty mohou mít alespoň následující implicitní metody: najdi(jedinečnýidentifikátor), obsahuje(Objekt), sečíst() Iterace. Obsahuje-li zpráva odeslaná násobnému objektu prefix v podobě specifikátoru iterace, je zpráva odeslána všem objektům uloženým v násobném objektu: - * znamená sekvenční zpracování instancí uložených v násobném objektu - *// znamená souběžné zpracování instancí uložených v násobném objektu Za specifikátorem iterace můžeme zadat výraz iterace, jenž určí počet opakování: žádný výraz, i:=1..n, while (booleovský výraz), until (booleovský výraz), for each (výraz, jehož výsledkem je kolekce objektů. 31 Pro větvení stačí vsunout podmínku, která je chráněna booleovskou hodnotou. Zpráva se odešle pouze v případě, že je podmínka splněna. Každý aktivní objekt má vlastní řídící vlákno – souběžnost. Aktivní objekty jsou instance aktivních tříd. Aktivní objekty a třídy mají v diagramech tučné ohraničení nebo obsahují v obdélníku vlastnost (active). Každé vlákno má název. Stav objektu je znázorněný v hranatých závorkách za názvem třídy. Dalším diagramem je sekvenční diagram. Ten zdůrazňuje interakci mezi dvěma instancemi. Interakce popisuje chronologicky uspořádanou posloupnost zpráv předávaných mezi instancemi. Instance a role klasifikátorů jsou umístěny podél horního okraje diagramu (časová osa směřuje odshora dolů). Pro znázornění iterace se používá obdélník ohraničující posloupnost odeslání zpráv, která má být opakována. Výraz iterace je umístěn pod ohraničujícím obdélníkem. Při větvení je vhodné chránit každou větev podmínkou, kterou je třeba umístit před první zprávu v každé větvi. Všechny podmínky se musí navzájem vylučovat, jinak by vznikla souběžnost. Souběžnost znázorňujeme pomocí aktivních objektů. Aktivace se rozdělí a následně je vhodn é opět spojit (po ukončení souběžnosti). Stav objektu lze zobrazit pomocí stavových symbolů umístěných na vhodných bodech čáry života. 32 Diagram aktivit Diagramy aktivit jsou objektově orientovanými vývojovými diagramy. Lze je použít k modelování všech typů procesů, připojit k libovolnému modelovanému elementu a zachytit jeho chování. Zachycuje pouze jeden specifický aspekt chování systému. Akce diagramu aktivit jsou zjednodušenou formou stavů, jež obsahují pouze jednu akci. Jsou nedělitelné, nepřerušitelné a okamžité. Počáteční stav označuje počátek diagramu aktivit a koncový stanovuje konec diagramu aktivit. Dílčí aktivity obsahují graf celé aktivity, lze je dělit na další dílčí aktivity, lze je přerušovat a mohou trvat pouze po určitou dobu. Přechody označují přesun z jednoho stavu do druhého – automatický přechod je vyvolán bezprostředně po ukončení aktivity. Hlavní tok může být rozdělen na více alternativních cest. K tomu každý přechod je chráněn booleovskou podmínkou, které se musí navzájem vylučovat. Pokud není splněna žádná ze stanovených podmínek, označíme to slovíčkem „jinak“ (else). Rozvětvení (fork) umožňuje rozdělení přechodu na více souběžných toků. Má jeden vstupní a několik výstupních přechodů, které jsou vykonány souběžně. Spojení (join), které má několik vstupních a jeden výstupní přechod, synchronizuje jeden nebo více souběžných toků. Je vykonán až po ukončení všech vstupních aktivit. Zóny (swimlanes) umožňují rozdělit aktivity v diagramu aktivit a mohou reprezentovat následující: organizační jednotky, případy užití, třídy, procesy... Toky objektů ukazují, co do objektu vstupuje, co je jeho výstupem a jak se objekt mění v důsledku změny stavů akcí nebo dílčích aktivit. Typem událostí může být signál. Ten je způsobem vyjádření balíčku informací předávaných asynchronně mezi dvěma objekty. Signály jsou modelovány jako třídy se standardním stereotypem <<signal>>, mají pouze atributy a mají jednu implicitní metodu (send(cílováMnožina)), která 33 umožňuje odeslání signálu cílovým objektům. Odeslání signálu je stav, který signál odesílá a příjem signálu je stav, který signál přijímá. Návrh Pracovní postup návrh Pracovní postup během návrhu spočívá v přesném určení implementace funkcí specifikovaných v analytickém modelu. Analýza a návrh mohou do určité míry probíhat souběžně. Návrhový model obsahuje jeden návrhový systém, návrhové podsystémy, návrh realizace případů užití, rozhraní, návrhové třídy a první verzi diagramu nasazení. Návrhové třídy Návrhové třídy jsou stavebními bloky návrhového modelu. Jejich specifikace je již na takové 34 úrovni, že je lze implementovat. Návrhové třídy obsahují kompletní specifikaci: kompletní sadu atributů včetně názvu, typu, nepovinně implicitní hodnoty, typu viditelnosti metody (název, názvy a typy všech argumentů, případně hodnoty nepovinných argumentů, návratový typ, typ viditelnosti Správně formulované návrhové třídy se vyznačují následujícími charakteristikami: - veřejné metody třídy definují dohodu mezi třídou a jejími klienty - úplnost je podmíněna tím, zda třída poskytuje klientům vše, co od ní očekávají dostatečnost slouží k ujištění, že všechny metody třídy jsou zcela zaměřeny na realizaci zamýšleného účelu třídy - jednoduchost (služby by měly být jednoduché, nedělitelné a jedinečné) - vysoká soudržnost - minimalizace vazeb Pokud mezi dvěma analytickými třídami existuje jasná a nedvojsmyslná relace „je“, je vhodné použít dědičnost. Je to v elmi silná vazb a mezi d v ma ě n ebo v cí e třídami. Změn y v nadtřídě se automaticky přenášejí na všechny podtřídy. Pro vyjádření role je vhodnější agregace. V jazyce C++ je možné implementovat dědění od více předků. Musí být splněno následující: všechny rodičovské třídy musí být sémanticky nespojité, zásady nahraditelnosti by měly platit mezi podtřídou a všemi nadtřídami, nadtřída by většinou neměla mít žádného předka. Při použití dědění můžeme získat rozhraní (veřejné metody nadtříd) a implementaci (různé atributy, asociace, chráněné a soukromé metody nadtříd…). Při realizaci rozhraní získáváme pouze množinu veřejných operací bez předem definované implementace. Dědění se hodí v situacích, kdy chceme od nadtřídy dědit určité vlastnosti (metody, atributy…). Realizace rozhraní se hodí, když chceme definovat dohodu, ale nechceme dědit implementační detaily. Tvorbu parametrizovaných tříd umožňují šablony. Místo přesného určení skutečných typů atributů, typů návratových hodnot nebo atributů metod můžeme definovat třídu, která bude sama obsahovat zástupné symboly nebo parametry. Ty budou při tvorbě nové třídy nahrazeny skutečnými hodnotami. Novou třídu lze ze šablony vytvořit pomocí závislosti s předdefinovaným stereotypem <<bind>>. Tento postup se nazývá explicitní vazbou (relace znázorňuje skutečné hodnoty, každé instanci šablony můžeme přiřadit název). Na straně druhé je tu implicitní vazba. K implicitní tvorbě třídy ze šablony potřebujeme seznam skutečných hodnot uzavřený do lomených závorek za názvem šablony. Uvnitř existující třídy lze vytvořit tzv. vnořené třídy. Vnořená třída je deklarována uvnitř jmenného prostoru obklopující třídy a používat ji může pouze tato třída nebo její objekty. V Javě se používají obvykle k obsluze událostí v třídách tvořících grafické uživatelské rozhraní. V Javě neexistuje mnohonásobná dědičnost. Upřesňování analytických relací Postupné upřesňování analytických asociací do návrhových asociací zahrnuje upřesnění asociací do podoby agregace nebo kompozice, implementaci tříd asociací, implementaci asociací 1:N, M:N, obousměrných asociací, přidávaní řiditelnosti, přidávání násobnosti na obou koncích asociace, přidávání názvů rolí na obou koncích asociace. Agregace a kompozice. Je to relace typu celek/součást, kde objekty jedné třídy hrají roli celku, 35 zatímco objekty jiné třídy hrají roli součásti. Celek využívá služeb součásti. Služba součásti vyžaduje součinnost celku. Celek je v relaci dominantní a řídicí silou. Součást je spíše pasivní. Agregace. Celek může existovat nezávisle na součástech, jindy zase ne. Součásti mohou existovat nezávisle na celku. Chybí-li součásti, je celek v jistém smyslu neúplný. Jednotlivé součásti mohou být sdíleny více celky. Lze vytvářet hierarchie a sítě agregací. Celek ví o svých součástech. Kompozice. Je to silnější forma agregace. Součásti patří jen a pouze jednomu celku. Celek má výhradní odpovědnost za použití svých součástí. Je-li celek zničen, musí zničit rovněž všechny svoje součásti nebo převést odpovědnost na ně. Každá součást patří pouze jednomu celku. Typy asociací: - asociace typu 1:1 (téměř vždy je upřesněna do kompozice) - asociace typu M:1 (má sémantiku agregace) asociace typu 1:N (na straně součásti je kolekce objektů, používá se vestavěné pole nebo třída kolekce) kolekce (třídy, které se specializují na správu kolekci jiných objektů; obsahují metody pro přidávání a odstraňování objektů do kolekce, získávání odkazu na objekt uložený v kolekci, procházení kolekce prvním objektem počínaje a posledním konče) konkretizované relace (asociace typu M:N - většinou se provádí náhrada relace za třídu a rozložení vazeb na 1:N; obousměrné asociace se nahrazují jednosměrnými agregacemi nebo kompozicemi a jednosměrnými asociacemi nebo závislostmi; třídy asociací – relace se nahradí třídou a do relace se vloží omezení prostřednictvím poznámky) Rozhraní a podsystémy Rozhraní umožňují návrh softwaru zaměřený na dohodu, nikoli na specifickou implementaci. Rozhraní specifikuje množinu operací, kde každá musí obsahovat úplnou signaturu, specifikaci popisující sémantiku operace, nepovinně stereotyp, omezující pravidla a označené hodnoty. Rozhraní odděluje specifikaci od implementace a lze jej připojit třídám, podsystémům, komponentám. Každá entita implementující rozhraní souhlasí s dohodou definovanou prostřednictvím operací specifikovaných v rozhraní. Rozhraní nesmí obsahovat atributy, implementaci operací, relace řiditelné z rozhraní. Subsystémy neboli podsystémy jsou typem balíčků. Návrhové podsystémy obsahují návrhové elementy - návrhové třídy a návrhová rozhraní, realizace případů užití, další podsystémy. Implementační podsystémy obsahují implementační elementy - rozhraní, komponenty, další podsystémy. Podsystémy se používají k osamostatnění zájmů, k vyjádření obsáhlých komponent a k zapouzdření starších systémů. Realizace případů užití – návrh Návrhové realizace případů užití jsou seskupeními spolupracujících návrhových objektů a tříd, které slouží k realizaci případů užití. 36 Skládají se z návrhových diagramů interakce a návrhových diagramů tříd. Základní stavové diagramy Diagramy aktivit jsou speciálním případem stavových diagramů. V nich jsou všechny stavy vyjádřeny jako akce nebo stavy vnořených aktivit a přechody spouštěny automaticky po ukončení předchozí akce nebo aktivity. Reaktivní objekty (poskytuje kontext stavového diagramu) reagují na externí události, mají určitý životní cyklus, který lze modelovat jako posloupnost stavů, přechodů a událostí, mají aktuální chování, které vyplývá z předchozího chování. Akce je proces, který proběhne rychle a je nepřerušitelný. K nim může docházet uvnitř stavu přidruženého k internímu přechodu nebo vně stavu přidruženého k externímu přechodu. Aktivita je proces, který trvá určitou dobu a který lze před ukončením přerušit. K aktivitám může docházet pouze uvnitř stavu. Stav je sémanticky významná podmínka objektu. Stav objektu je určen hodnotami atributů daného objekt, relacemi s dalšími objekty a aktuálně vykonávanou aktivitou. Přechod je posunem z jednoho stavu do dalšího. Obsahuje událost (externí či interní zahájení přechodu), podmínku (podmínění splnění přechodu), akce (část díla přidruženého k přechodu, k níž dochází při zahájení přechodu). Specifikací něčeho významného, co se stane v u rčitém čase a p rostoru a to, co n emá trván í se nazývá událost. Může být rozlišena událost volání (spuštění metody objektu), signální událost (přijetí signálu), událost změny (dochází k ní po splnění booleovské podmínky), časová událost (dochází k ní po uplynutí stanovené doby). K pamatování toho, v jakém podstavu se hlavní stav nacházel při přechodu do externího stavu se používá historie: mělká historie umožňuje hlavnímu stavu pamatovat si poslední podstav na stejné úrovni, na jaké je použit indikátor historie hluboká historie umožňuje hlavnímu stavu pamatovat si poslední podstav před opuštěním stavu na libovolné úrovni Implementace 37 Pracovní postup – implementace Pracovní postup Implementace je primárně zaměřen na tvorbu kódu. Spočívá v transformaci návrhového modelu do spustitelného kódu. Architektonická implementace vytváří následující: - diagram komponent - diagram nasazení Komponenty Komponenty jsou fyzickou nahraditelnou částí systému. Jsou obalem návrhových tříd nebo realizací rozhraní. V diagramu komponent ukazujeme, jak budou návrhové třídy a rozhraní rozděleny do jednotlivých komponent. Příklady komponent: zdrojové soubory, implementační podsystémy, ovládací prvky ActiveX, objekty modelu JavaBeans, stránky JSP… Komponenty mohou být závislé na dalších komponentách. Pokud využijeme rozhraní jako prostředníků závislostí mezi komponentami, je možné zajistit redukci vazeb. Nasazení Diagramy nasazení umožňují modelovat distribuci softwarového systému na fyzickém hardwaru. Mapuje architekturu softwaru na architekturu hardwaru. Lze jej použít k modelování typů hardwaru, softwaru a spojení. Ukazuje uzly (typy hardwaru), relace (typy spojení mezi uzly), komponenty (typy komponent nasazených na určité uzly a popisuje celou množinu možných nasazení. Pro popis jednoho specifického nasazení systému se pak využívá diagram konkrétního nasazení. 38 39 NÁVRHOVÉ VZORY Úvodem Navrhování objektově orientovaného programového vybavení je obtížné. Obtížnější je však návrh softwaru pro opětovné využití. Návrh by měl být specifický pro náš problém, ale zároveň dost obecný, aby se mohl v budoucnu znovu použít. Zkušení programátoři vědí, že každý problém není dobré a efektivní řešit od píky. Je lepší poohlédnout se po řešeních, které byly použity v minulost a využít je v novém programu. Jedním z takovýchto přístupů jsou návrhové vzory. Co je návrhový vzor? Návrhový vzor můžeme chápat jako abstrakci imitování užitečných částí jiných softwarových produktů. Pokud tedy zjistíme, že k řešení určitého problému používáme již dříve úspěšné řešení, které se opakuje v různých produktech z různých doménových oblastí, můžeme pak zobecnění tohoto řešení nazvat návrhovým vzorem. Každý takový návrhový vzor je popsán množinou komunikujících objektů a jejich tříd, které jsou přizpůsobeny řešení obecného problému návrhu v daném konkrétním kontextu, tedy již existujícímu okolí. Vzor se obecně skládá ze čtyř základních prvků: 1. důsledků název vzoru je záhlaví, jež slouží k popisu návrhového problému, jeho řešení a 2. problém popisuje, kdy se má vzor používat; je zde vysvětlen problém v širším kontextu; může popisovat třídní, objektové struktury, popisovat specifické návrhové problémy 3. řešení popisuje prvky, z nichž se návrh skládá – vztahy, povinnosti a spolupráci. Nepopisuje konkrétní návrh či implementaci. Vzor je pouze šablona. 4. důsledky jsou výsledky a kompromisy pro využití vzoru. K něm patří mj. také vliv na tvárnost, rozšiřitelnost a přenositelnost systému. Návrhové vzory nejsou o programových návrzích, které by šlo naprogramovat a znovupoužívat tak, jak jsou. Ani nejsou komplexními návrhy pro celé systémy. Slouží pouze k popisu komunikace a rozložení tříd a objektů, které jsou upraveny k řešení obecného návrhového problému v určitém kontextu. Popis návrhových vzorů Každý vzor je v tomto materiálu rozdělen podle následující šablony. Název a klasifikace vzoru Účel Motivace Použití Struktura název vystihuje podstatu vzoru a klasifikace odráží schéma popíše ve stručnosti účel vzoru popisuje scénář ilustrující návrhový problém a způsob jeho řešení pomocí třídní struktury popisuje příklady využití daného návrhového vzoru jako grafické vyjádření tříd vzoru pomocí OMT 40 Součásti Spolupráce jsou třídy nebo objekty, které jsou součástí návrhového vzoru popisuje spolupráci objektů vykonávající povinnosti Důsledky rozeberou, jak návrhový vzor podporuje své cíle, faktory a výsledky použití vzoru Implementace upozorní na nástrahy, tipy a postupy, o nichž bychom měli při implementaci vzoru vědět Příklad uvede zlomky zdrojového textu, které ilustrují implementaci v C++ nebo Smalltalk Známá použití jsou příklady známých systémů, kde je vzor využit Příbuzné vzory srovná s podobnými vzory Jak návrhový vzor vybírat Je vhodné zvážit, jak návrhové vzory řeší návrhové problémy (granularita, hledání vhodných objektů, specifikace rozhraní). Poté je dobré projít si účelové oddíly. Popis účelu jednotlivých vzorů může napomoci při hledání toho správného návrhového vzoru. Pomoci může také vhodné prostudování vzájemných propojení a vztahů jednotlivých návrhových vzorů, což by mohlo nasměrovat k použití správné skupiny vzorů nebo vzoru samotného. Prostudujte si vzory s podobným účelem. Vyšetřete příčinu přepracovaného návrhu. Podívejte se na přepracovaný návrh už od začátku a najděte souvislosti s možným použitím vzoru. Uvažujte, co se ve vašem návrhu mění. Zaměřte se na to, co chcete být schopni měnit bez nutnosti předělání návrhu. Katalog návrhových vzorů Návrhové vzory Tvořivé (creational patterns) Návrhové vzory „Tvořivé“ zabstraktňují proces tvorby instancí a pomáhají budovat systém, který je nezávislý na způsobu tvorby, skládání a vyjadřování jeho objektů. V těchto vzorech se opakují dvě schémata: tyto vzory zapouzdřují znalost o tom, které konkrétní třídy systém používá - vzory skrývají způsob vytváření instancí tříd (jediné, co se o objektech ví, je znalost jejich rozhraní) Mezi návrhové vzory Tvořivé patří následující: - ABSTRACT FACTORY (abstraktní továrna) - BUILDER (stavitel) – uveden v programové části - FACTORY METHOD (tovární metoda) - PROTOTYPE (prototyp) - SINGLETON (jedináček) – uveden v programové části Pro představení si realizace těchto podobných vzorů použijeme společný příklad – budování bludiště pro počítačovou hru. 41 Třídy Room, Door a Wall definují komponenty bludiště použité ve všech příkladech. Budeme ignorovat mnoho podrobností (co může být v bludišti, je-li hráč jeden nebo více…). Následující diagram zobrazuje vztahy mezi třídami: Každá místnost má čtyři strany. Pro určení severní, jižní, východní a západní použijeme v C++ výčet Direction: enum Direction {North, South, East, West}; Třída MapSite je běžný abstraktní třída pro všechny komponenty bludiště. MapSite bude definovat jedinou operaci: Enter. Zkusíme-li vstoupit do dveří, nastane pouze jedna ze dvou věcí: jsou-li dveře otevřené vstoupíme, jinak ne. class MapSite { public: virtual void Enter() = 0; }; Room je konkrétní podtřídou třídy MapSite, která definuje klíčový vztah mezi komponentami v bludišti. Udržuje odkazy na další objekty MapSite a uchovává číslo, které identifikuje místnosti v bludišti. class Room : public MapSite { public: Room(int roomNo); MapSite* GetSide(Direction) const; void SetSide(Direction, MapSite*); virtual void Enter(); private: MapSite* _sides[4]; int _roomNumber; }; Následující třídy představují stěnu nebo dveře, které jsou na každé straně místnosti. class Wall : public MapSite { public: Wall(); virtual void Enter(); }; class Door : public MapSite { public: Door(Room* = 0, Room* = 0); virtual void Enter(); Room* OtherSideFrom(Room*); private: Room* _room1; 42 Room* _room2; bool _isOpen; }; Musíme vědět více než jen o částech bludiště. Abychom vyjádřili kolekci místností, definujeme také třídu Maze, která může rovněž najít určitou místnost pomocí své operace RoomNo při zadaném číslu místnosti. class Maze { public: Maze(); void AddRoom(Room*); Room* RoomNo(int) const; private: // ... }; Nadefinujeme třídu MazeGame, která bludiště vytvoří. Následující metoda vytvoří bludiště skládající se ze dvou místností, mezi nimiž jsou dveře: Maze* MazeGame::CreateMaze () { Maze* aMaze = new Maze; Room* r1 = new Room(1); Room* r2 = new Room(2); Door* theDoor = new Door(r1, r2); aMaze->AddRoom(r1); aMaze->AddRoom(r2); r1->SetSide(North, new Wall); r1->SetSide(East, theDoor); r1->SetSide(South, new Wall); r1->SetSide(West, new Wall); r2->SetSide(North, new Wall); r2->SetSide(East, new Wall); r2->SetSide(South, new Wall); r2->SetSide(West, theDoor); return aMaze; } Skutečný problém v této třídě je její netvárnost. Tvořivé vzory ukazují, jak udělat tento návrh tvárnější, ale nikoli nutně menší. Hlavně usnadňují změny tříd, které definují komponenty bludiště. Abstract Factory –Abstraktní továrna (objekt) Účel Poskytuje rozhraní pro vytváření řad příbuzných nebo závislých objektů, aniž by se musely specifikovat konkrétní třídy. Jiný název Kit (souprava) Motivace Vezměme soupravu nástrojů pro uživatelská rozhraní, jež podporuje více standardů vzhledu. Jiný vzhled definuje odlišná zobrazení a chování pomůcek pro uživatelské rozhraní (posuvníky, okna, tlačítka). Přenositelnost aplikace spočívá v tom, že pomůcky nebudou naprogramované pevně na určitý vzhled neboť by to mohlo znepříjemnit pozdější změnu vzhledu. Tento problém můžeme řešit definování abstraktní třídy WidgetFactory, která deklaruje rozhraní pro tvorbu všech základních druhů pomůcek. Pro každý druh pomůcky též existuje abstraktní třída a konkrétní podtřídy implementující pomůcky daných standardů vzhledu. Rozhraní třídy WidgetFactory obsahuje operaci, jež vrací nový pomůckový objekt pro každou abstraktní 43 pomůckovou třídu. Klienti tyto operace volají, aby získali pomůckové instance (nevědí nic o jejich třídách). Pro každý standard vzhledu existuje konkrétní podtřída třídy WidgetFactory. Aby vytvořila pomůcku pro příslušný vzhled, každá podtřída implementuje dané operace. Třeba operace CreateScrollBar u třídy MotifWidgetFactory spustí instanci a vrátí posuvník pro Motif. Klienti tvoří pomůcky výhradně pomocí rozhraní WidgetFactory. Třída WidgetFactory také uplatňuje závislosti mezi konkrétními pomůckovými třídami. Použití Použijeme v těchto případech: - systém má být nezávislý na tom, jak se produkty vytvářejí, skládají a představují - systém má být konfigurován pomocí jedné z více produktových řad - řada příbuzných produktových objektů je navržena pro společné použití - chceme poskytnout třídní knihovnu produktů, která má odhalit pouze jejich rozhraní a nikoli implementace Struktura 44 Součásti AbstractFactory (WidgetFactory) - deklaruje rozhraní pro operace, které vytvářejí abstraktní produktové objekty ConcreteFactory (MotifWidgetFactory, PMWidgetFactory) - implementuje operace k vytváření konkrétních produktových objektů AbstractProduct (Windows, ScrollBar) - deklaruje rozhraní pro typ produktového objektu ConcreteProduct (MotifWindow, MotifScrollBar) - definuje produktový objekt, který se má vytvořit odpovídající konkrétní továrnou - implementuje rozhraní AbstractProduct Client - používá pouze rozhraní deklarovaná třídami AbstractFactory a AbstractProduct Spolupráce Běžně je za běhu vytvořena jediná instance třídy ConcreteFactory. Tato konkrétní továrna vytváří produktové objekty, které mají určitou implementaci. Pro tvorbu jiných produktů je nutné využít jinou továrnu. AbstractFactory ConcreteFactory. odkládá tvorbu produktových objektů na svou podtřídu Důsledky Vzor Abstract Factory má ty to omezení a výhody: - izoluje konkrétní třídy (izolace klientů od implementačních tříd) - usnadňuje výměnu produktových řad (to, že se třída konkrétní továrny v aplikaci vyskytuje pouze jednou usnadňuje změnu konkrétní továrny, kterou aplikace používá) - zvyšuje konzistenci mezi produkty - podpora nových druhů produktů je obtížná (není snadné rozšířit abstraktní továrny a vytvářet nové druhy produktů, podpora nových druhů produktů vyžaduje rozšíření továrního rozhraní) Implementace Továrna jako jedináčci. Aplikace obvykle potřebuje jen jednu instanci ConcreteFactory na produktovou řadu, takže se zpravidla nejlépe implementuje jako Singleton. Tvorba produktů. AbstractFactory deklaruje pouze rozhraní pro tvorbu produktů. Je na podtřídách ConcreteProduct, aby je skutečně vytvořily. Nejběžněji se toho docílí definováním tovární metody pro každý produkt. Jestliže lze používat mnoho produktových řad, může být konkrétní továrna implementována pomocí vzoru Prototype. Konkrétní továrna je inicializována pomocí prototypové instance všech produktů v řadě a vytváří nový produkt pomocí klonu svého prototypu. Definice rozšířitelné továrny. AbstractFactory obvykle definuje různé operace pro každý druh produktu, který může vytvářet. Druhy produktů jsou dány v operačních signaturách. Místo změny rozhraní AbstractFactory a všech závislých tříd je tvárnější, ale méně bezpečné přidat parametr operacím, které objekty vytvářejí. Tento parametr určuje druh vytvářeného objektu (třídní 45 identifikátor, celé číslo, řetězec). Pak třída AbstractFactory potřebuje k přístupu pouze operaci Make s parametrem indikujícím druh vytvářeného objektu. Příklad Vzor Abstract Factory použijeme k tvorbě bludišť, jež jsme uvedli na začátku kapitoly. Třída MazeFactory může vytvářet komponenty bludišť: staví místnosti, stěny a dveře mezi místnostmi. Aby mohl programátor specifikovat třídy konstruovaných místností, stěn a dveří, mají programy stavějící bludiště za argument MazeFactory. class MazeFactory { public: MazeFactory(); virtual Maze* MakeMaze() const { return new Maze; } virtual Wall* MakeWall() const { return new Wall; } virtual Room* MakeRoom(int n) const { return new Room(n); } virtual Door* MakeDoor(Room* r1, Room* r2) const { return new Door(r1, r2); } }; Vzpomeňme si, že metoda CreateMaze staví malé bludiště, které se skládá ze dvou místní a dveří mezi nimi. CreateMaze pevně programuje názvy tříd a tím znesnadňuje tvorbu bludišť s různými komponentami. Zde je verze funkce CreateMaze napravující tento nedostatek tím, že má MazeFactory jako parametr: Maze* MazeGame::CreateMaze (MazeFactory& factory) { Maze* aMaze = factory.MakeMaze(); Room* r1 = factory.MakeRoom(1); Room* r2 = factory.MakeRoom(2); Door* aDoor = factory.MakeDoor(r1, r2); aMaze->AddRoom(r1); aMaze->AddRoom(r2); r1->SetSide(North, factory.MakeWall()); r1->SetSide(East, aDoor); r1->SetSide(South, factory.MakeWall()); r1->SetSide(West, factory.MakeWall()); r2->SetSide(North, factory.MakeWall()); r2->SetSide(East, factory.MakeWall()); r2->SetSide(South, factory.MakeWall()); r2->SetSide(West, aDoor); return aMaze; } Továrnu kouzelná bludiště EnchantedMazeFactory lze implementovat vytvořením podtříd MazeFactory. class EnchantedMazeFactory : public MazeFactory { public: EnchantedMazeFactory(); virtual Room* MakeRoom(int n) const { return new EnchantedRoom(n, CastSpell()); } virtual Door* MakeDoor(Room* r1, Room* r2) const { return new DoorNeedingSpell(r1, r2); } protected: Spell* CastSpell() const; 46 }; Nyní předpokládejme, že chceme vytvořit bludišťovou hru, ve které může být v místnosti nastražená bomba. Bomba po výbuchu zničí minimálně zdi. Vytvoříme podtřídu Room ke sledování toho, zda místnost obsahuje bombu, nebo bomba vybuchla. Ke sledování stěn použijeme podtřídu Wall. Třídy nazveme RoomWithABomb a BombedWall. Jako poslední definujeme třídu BombedMazeFactory (podtřídu MazeFactory), která zajišťuje, že stěny jsou třídy BombedWall a místnosti jsou třídy RoomWithBomb. Wall* BombedMazeFactory::MakeWall () const { return new BombedWall; } Room* BombedMazeFactory::MakeRoom(int n) const { return new RoomWithABomb(n); } Jednoduché bludiště, které může obsahovat bomby, postavím pouhým zavoláním funkce CreateMaze včetně BombedMazeFactory. MazeGame game; BombedMazeFactory factory; game.CreateMaze(factory); K budování kouzelných bludišť EnchantedMazeFactory. si může CreateMaze vzít stejně tak instanci Známá použití Aplikace, které generují různě složené objekty v závislosti na požadovaném schématu. Např. schéma, které je koncepčně horizontální, může vyžadovat jiné složené objekty v závislosti na orientaci dokumentu (na výšku, na šířku). K dosažení přenosnosti mezi různými okenními systémy (xwindow systém a sunview). Základ tvoří abstraktní třída WindowSystem definující rozhraní pro vytváření objektů (MakeWindow, Makedony, MakeColor). Příbuzné vzory Třídy AbstractFactory se často implementují pomocí Factory Method či Prototype. Konkrétní továrna je často Singleton. Factory Method – Tovární metoda (třída) Účel Definuje rozhraní pro vytváření objektu. Rozhodnutí, u které třídy se má spustit její instance, ale přenechává podtřídám. Faktory Method umožňuje třídě, aby odložila rozhodnutí o vytvoření instance na své podtřídy. Jiné názvy Virtual Constructor (virtuální konstruktor). Motivace Rámcové systémy používají abstraktní třídy k definování a udržování vztahů mezi objekty. Rámcový systém je často odpovědný za tvorbu těchto objektů. Uvažujeme o rámcovém systému pro aplikace, který mohou uživateli zobrazovat více dokumentů najednou. Klienti tvoří podtřídy abstraktních tříd Application a Document. Kreslící aplikaci vytvoříme např. tak, že definujeme třídy DrawingApplication a DrawingDocument. Třída Application je odpovědná za správu Dokumentů a vytváří je podle potřeby (otevřít, nový…). 47 Určitá podtřída třídy Document, jejíž instance se má vytvořit, je specifická dané aplikaci. Třída Application nemůže předvídat podtřídu třídy Document na vytvoření instance (třída Application pouze ví, kdy by měl být nový dokument vytvořen, ne jaký druh dokumentu vytvořit). Rámcový systém musí vytvořit instance tříd, ale ví jen o abstraktních třídách, jejichž instance vytvářet nemůžeme. Vzor Factory Method nabízí řešení: zapouzdřuje znalost toho, jaká se má vytvořit podtřída třídy Document a přesune tento poznatek mimo rámcový systém. Aplikační podtřídy předefinují abstraktní operaci CreateDocument u třídy Application a vrátí příslušnou podtřídu třídy Document. Když je vytvořená instance podtřídy třídy Application, může vytvořit instance aplikačně specifické Dokumenty bez znalosti jejich třídy. CreateDocument nazýváme tovární metodou, neboť je zodpovědná za výrobu objektu. Použití Vzor Factory Method použijeme v těchto případech: - třída nemůže předjímat třídu objektů, které musí vytvářet - třída chce, aby její podtřídy specifikovaly objekty, které vytvoří - třídy delegují odpovědnost na jednu z několika pomocných podtříd, a chceme lokalizovat poznatek o tom, která podtřída je delegátem Struktura Součásti Product (Document) - definuje rozhraní objektů vytvářených tovární metodou ConcreteProduct (MyDocument) - implementuje rozhraní Productu 48 Creator (Application) - deklaruje tovární metodu, která vrací objekt typu Product - může volat tovární metodu, aby vytvořila objekt Product ConcreteCreator (MyApplication) - překrývá tovární metodu, aby vrátila instanci ConcreteProduct Spolupráce Při definici tovární metody spoléhá Creator na své podtřídy, aby mohla vrátit instanci příslušného ConcreteProduct. Důsledky Factory Method eliminuje nutnost vázat aplikačně specifické třídy do zdrojového textu. Zdrojový text se zabývá pouze rozhraním Productu, a proto může pracovat s libovolnými uživatelsky definovanými třídami ConcreteProduct. Možnou nevýhodou je to, že klienti si budou muset vytvořit podtřídy třídy Creator jen proto, aby vytvořili určitý objekt ConcreteProduct. Další důsledky: - poskytuje záchytné body pro vytvoření podtříd (tvorba objektů uvnitř třídy pomocí tovární metody je vždy tvárnější než přímá tvorba) - podporuje paralelní třídní hierarchie (např. u grafických obrázků, jež je možné natahovat, otáčet, přesouvat) Třída Figure poskytuje tovární metodu CreateManipulator, jež umožňuje klientům vytvořit Manipulator odpovídající třídě Figure. Podtřídy třídy Figure tuto metodu překrývají, aby vrátily instanci podtřídy třídy Manipulator, která je pro ně správná. Jinak může třída Figure implementovat CreateManipulator, aby vrátila výchozí instanci třídy Manipulator a podtřídy třídy Figure ji mohou jednoduše zdědit. Třídy Figure, které tak činí, nepotřebují žádné odpovídající podtřídy třídy Manipulator - proto jsou hierarchie jen částečně paralelní. Implementace Dva hlavní druhy. Dvěma hlavními variacemi jsou případ, kdy třída Creator je abstraktní třídou a neposkytuje implementaci pro tovární metodu, kterou deklaruje (vyžaduje, aby podtřídy definovaly implementaci) a případ, kdy je Creator konkrétní třídou a poskytuje výchozí implementaci pro tovární metodu (použití tovární metody kvůli tvárnosti). Parametrizované tovární metody. Jiná variace vzoru umožňuje tovární metodě vytvořit více druhů produktů. Tovární metoda má parametr, který identifikuje druh vytvářeného objektu, ty budou sdílet rozhraní Productu. 49 Některé systémy definují třídu Creator pomocí tovární metody Create. Při ukládání na disk se nejprve zapíše třídní identifikátor a potom jeho instanční proměnné. Při rekonstrukci objektu se z disku nejdříve načte třídní identifikátor. Po jeho načtení se zavolá metoda Create a předá mu identifikátor jako parametr. Create vyhledá konstruktora odpovídající třídy a použije ho pro vytvoření instance objektu. Nakonec Create zavolá operaci Read tohoto objektu, která načte zbývající informace z disku. Parametrizovaná tovární metoda má následující tvar. class Creator { public: virtual Product* Create(ProductId); }; Product* Creator::Create (ProductId id) { if (id == MINE) return new MyProduct; if (id == YOURS) return new YourProduct; // opakovani pro další produkty... return 0; } Překrytí parametrizované tovární metody nám umožňuje snadno a selektivně rozšířit či změnit produkty, které produkuje Creator. Např. podtřída MyCreator by mohla zaměnit MyProduct a YourProduct a podporovat novou třídu TheirProduct: Product* MyCreator::Create (ProductId id) { if (id == YOURS) return new MyProduct; if (id == MINE) return new YourProduct; // N.B.: zapina YOURS a MINE if (id == THEIRS) return new TheirProduct; return Creator::Create(id); // zavolano, je-li vse ostatní neuspesne } Poslední volání Create na rodičovskou třídu je dáno tím, že MyCreator::Create zachází pouze s YOURS, MINE a THEIRS jiným způsobem než rodičovská třída. O jiné třídy se nezajímá. Jazykově specifické varianty a záležitosti. Různé jazyky se propůjčují k dalším zajímavým variacím. Programy v jazyce Smalltalk často používají metodu, jež vrací třídu objektu, jehož instance se má vytvořit. Tovární metody v jazyce C++ jsou vždy virtuální funkce. Jen si musíme dát pozor, abychom nevolali tovární metody v konstruktoru Creator – tovární metoda ještě nebude v ConcreteCreator k dispozici. Obcházení tvorby podtříd pomocí šablon. Jedním ze zmíněných problémů může být, že nás metody mohou nutit vytvářet podtřídy jen pro to, aby se vytvořily příslušné objekty typu Product. C++ to řeší šablonovou podtřídou třídy Creator, jež je parametrizována pomocí třídy. class Creator { public: virtual Product* CreateProduct() = 0; }; template class StandardCreator: public Creator { public: virtual Product* CreateProduct(); }; template Product* StandardCreator::CreateProduct () { return new TheProduct; } 50 Pomocí této šablony klient dodává pouze produktovou třídu – není nutné vytvářet podtřídy třídy Creator. class MyProduct : public Product { public: MyProduct(); }; StandardCreator myCreator; Názvové konvence. Mělo by být jasné, že používáme tovární metody. Příklad Funkce CreateMaze staví a vrací bludiště. Jedním problémem této funkce je, že pevně programuje třídy bludiště, místností, dveří a stěn. Abychom umožnili výběr těchto komponent podtřídám, zavedeme tovární metody. Nejdříve definujeme tovární metody v MazeGame pro tvorbu objektů bludiště, místni, stěny a dveří: class MazeGame { public: Maze* CreateMaze(); // tovarni metody: virtual Maze* MakeMaze() const { return new Maze; } virtual Room* MakeRoom(int n) const { return new Room(n); } virtual Wall* MakeWall() const { return new Wall; } virtual Door* MakeDoor(Room* r1, Room* r2) const { return new Door(r1, r2); } }; Každá tovární metoda vrací bludišťovou komponentu daného typu. Nyní můžeme CreateMaze přepsat a tyto tovární metody použít: Maze* MazeGame::CreateMaze () { Maze* aMaze = MakeMaze(); Room* r1 = MakeRoom(1); Room* r2 = MakeRoom(2); Door* theDoor = MakeDoor(r1, r2); aMaze->AddRoom(r1); aMaze->AddRoom(r2); r1->SetSide(North, MakeWall()); r1->SetSide(East, theDoor); r1->SetSide(South, MakeWall()); r1->SetSide(West, MakeWall()); r2->SetSide(North, MakeWall()); r2->SetSide(East, MakeWall()); r2->SetSide(South, MakeWall()); r2->SetSide(West, theDoor); return aMaze; } Různé hry mohou vytvářet podtřídy třídy MazeGame a tím součásti bludiště specializovat. Podtřídy třídy MazeGame mohou předefinovat některé či všechny tovární metody a určovat variace v produktech. Např. bombedMazeGame může předefinovat produkty Room a Wall a vrátit jejich bombové varianty: class BombedMazeGame : public MazeGame { public: BombedMazeGame(); 51 virtual Wall* MakeWall() const { return new BombedWall; } virtual Room* MakeRoom(int n) const { return new RoomWithABomb(n); } }; Kouzelnou variantu EnchantedMazegame lze definovat např. takto: class EnchantedMazeGame : public MazeGame { public: EnchantedMazeGame(); virtual Room* MakeRoom(int n) const { return new EnchantedRoom(n, CastSpell()); } virtual Door* MakeDoor(Room* r1, Room* r2) const { return new DoorNeedingSpell(r1, r2); } protected: Spell* CastSpell() const; }; Známá použití Soupravy nástrojů a rámcové systémy. Generování příslušného typu zástupce, pokud si objekt vyžádá odkaz na vzdálený objekt. Příbuzné vzory Abstract Factory se často implementuje pomocí továrních metod. Tovární metody se obvykle volají v rámci Template Method (NewDocument). Prototypy často vyžadují operaci Initialize na třídě Product. Prototype – Prototyp (objekt) Účel Specifikuje vytvářené druhy objektů pomocí prototypické instance. Nové objekty vytváří tak, že tento prototyp kopíruje. Motivace Předpokládejme, že rámcový systém obsahuje abstraktní třídu Graphic pro grafické komponenty, např. noty a osnovy. Navíc poskytuje abstraktní třídu Tool k definování nástrojů, které jsou podobné nástrojům v paletě. Rámcový systém rovněž předem definuje podtřídu GraphicTool pro nástroje, které vytvářejí instance grafických objektů a přidávají je do dokumentu. GraphicTool představuje pro návrháře rámcových systémů problém. GraphicTool neví, jak vytvářet instance hudebních tříd a přidávat je do partitur. Pro každý druh hudebního objektu lze vytvořit podtřídy třídy GraphicTool, ale to vyprodukuje příliš mnoho podtříd odlišujících se pouze druhem hudebního objektu. Řešení spočívá v zajištění toho, aby GraphicTool vytvořil novou třídu Graphic pomocí kopírování či klonování instance podtřídy Graphic. Tuto instanci nazýváme prototyp. GraphicTool je parametrizován prototypem, jenž se má naklonovat a přidat k dokumentu. Pokud všechny podtřídy třídy Graphic podporují operaci Clone, potom GraphicTool může klonovat jakýkoli druh Graphic. V našem hudebním editoru jsou nástroje k tvorbě hudebního objektu instancí třídy GraphicTool, jež je inicializována pomocí jiného prototypu. Každá instance GraphicTool produkuje hudební objekt pomocí klonování jeho prototypu a přidání klonu do partitury. 52 Vzor Prototype lze použít ke snížení počtu tříd (např. délka not). Použití Vzor Prototype využijeme, chceme-li, aby systém byl nezávislý na tom, jak jsou produkty vytvářeny, skládány a vyjadřovány a : - jestliže jsou třídy, jejichž instance se mají vytvořit, specifikovány za běhu - chceme-li navrhnout budování třídní hierarchie továrny, jež je souběžná s třídní hierarchií produktů - pokud instance třídy mohou mít jej jednu z několika málo různých kombinací stavu Struktura Součásti Prototype (Graphic) - deklaruje rozhraní pro vlastní klonování ConcretePrototyp (Start, WholeNote, HalfNote) - implementuje operaci pro vlastní klonování Client (GraphicTool) - vytváří nový objekt požádáním prototypu, aby se naklonoval 53 Spolupráce Klient požádá prototyp, aby se naklonoval. Důsledky Skrývá konkrétní produktové třídy před klientem a tím se snižuje počet názvů, o kterých klienti vědí. Dále: - přidávání a odebírání produktů za běhu (Prototypy umožňují začlenění nové konkrétní produktové třídy do systému jednoduchou registrací u klienta) - specifikace nových objektů pomocí změn hodnot (umožněno definovat nové chování prostřednictvím objektové skladby) - specifikace nových objektů změnou struktury (některé aplikace budují velké objekty z malých (stavba obvodů) - snížená tvorba podtříd - dynamická konfigurace aplikace pomocí tříd (možné dynamické zavádění tříd do aplikace) Hlavní nevýhodou je, že každá podtřída třídy Prototype musí implementovat operaci Clone, což může být obtížné (pokud vnitřní objekty nepodporují kopírování). Implementace Prototype je užitečný u statických jazyků. Použití správce prototypů. Není-li počet prototypů v systému pevný, je vhodné udržovat registr dostupných prototypů. Implementace operace Clone. Smalltalk poskytuje implementaci copy, C++ poskytuje kopírovací konstruktor (neřeší mělkou vs. hlubokou kopii). Je nutné zajistit, aby byly komponenty klonovaného objektu klony komponent prototypu. Lze využít (pokud je to podporováno) operace Save (uložení do vyrovnávací paměti) a Load (načtení z paměti). Inicializace klonů. Obecně nelze vnitřní stavy klonu inicializované na hodnoty v operaci Clone předávat. Příklad Nadefinujeme podtřídu MazePrototypeFactory třídy MazeFactory (viz. Abstract Factory). Podtřída MazePrototypeFactory se zinicializuje pomocí prototypů objektů, které vytvoří. MazePrototypeFactory rozšiřuje rozhraní MazeFactory pomocí konstruktoru, který má prototypy jako argumenty: class MazePrototypeFactory : public MazeFactory { public: MazePrototypeFactory(Maze*, Wall*, Room*, Door*); virtual Maze* MakeMaze() const; virtual Room* MakeRoom(int) const; virtual Wall* MakeWall() const; virtual Door* MakeDoor(Room*, Room*) const; private: Maze* _prototypeMaze; Room* _prototypeRoom; Wall* _prototypeWall; Door* _prototypeDoor; }; Nový konstruktor jednoduše inicializuje své prototypy: 54 MazePrototypeFactory::MazePrototypeFactory ( Maze* m, Wall* w, Room* r, Door* d ) { _prototypeMaze = m; _prototypeWall = w; _prototypeRoom = r; _prototypeDoor = d; } Metody pro tvorbu stěn, místností a dveří jsou podobné (každá klonuje prototyp a pak jej zinicializuje). Zde jsou definice MakeWall a MakeDoor: Wall* MazePrototypeFactory::MakeWall () const { return _prototypeWall->Clone(); } Door* MazePrototypeFactory::MakeDoor (Room* r1, Room *r2) const { Door* door = _prototypeDoor->Clone(); door->Initialize(r1, r2); return door; } Podtřídu MazePrototypeFactory lze použít k vytvoření prototypového či výchozího bludiště její pouhou inicializací pomocí prototypů základních komponent bludiště: MazeGame game; MazePrototypeFactory simpleMazeFactory( new Maze, new Wall, new Room, new Door ); Maze* maze = game.CreateMaze(simpleMazeFactory); Abychom změnili typ bludiště, inicializujeme MazePrototypeFactory pomocí jiné sady prototypů. Toto volání vytvoří bludiště s vybuchlými dveřmi BombedDoor a místní obsahující bombu RoomWithABomb: MazePrototypeFactory bombedMazeFactory( new Maze, new BombedWall, new RoomWithABomb, new Door ); Objekt, jejž lze použít jako prototyp musí podporovat operaci Clone. Abychom umožnili klientům zinicializovat místnosti klonu, přidáme operaci Initialize do třídy Door. class Door : public MapSite { public: Door(); Door(const Door&); virtual void Initialize(Room*, Room*); virtual Door* Clone() const; virtual void Enter(); Room* OtherSideFrom(Room*); private: Room* _room1; Room* _room2; }; Door::Door (const Door& other) { _room1 = other._room1; _room2 = other._room2; } void Door::Initialize (Room* r1, Room* r2) { _room1 = r1; _room2 = r2; } 55 Door* Door::Clone () const { return new Door(*this); } Podtřída BombedWall musí operaci Clone překrýt a implementovat příslušný kopírovací konstruktor. class BombedWall : public Wall { public: BombedWall(); BombedWall(const BombedWall&); virtual Wall* Clone() const; bool HasBomb(); private: bool _bomb; }; BombedWall::BombedWall (const BombedWall& other) : Wall(other) { _bomb = other._bomb; } Wall* BombedWall::Clone () const { return new BombedWall(*this); } I když BombedWall::Clone vrací Wall*, její implementace vrací kazatel na novou instanci podtřídy. Klienti by nikdy neměli snižovat typ návratové hodnoty operace Clone na požadovaný typ. Známá použití Zformování objektu, který je pak možné povýšit na prototyp nainstalováním jej do knihovny znovu použitelných objektů. Popředí programů, jež poskytují rozhraní pro různé příkazově orientované ladící programy. Rámcové kreslící systémy. Příbuzné vzory Vzory Prototype a Abstract Factory se mohou využívat společně. Abstract Factory může ukládat sadu prototypů, ze které se mají klonovat a vracet produktové objekty. Dále jej mohou využívat vzory Composite a Decorator. Návrhové vzory Strukturální (structural patterns) Strukturální vzory se zabývají skládáním tříd a objektů a vytvářením rozsáhlejších struktur. Třídní strukturální vzory používají dědičnost ke skládání rozhraní či implementací (jak vícenásobná dědičnost sloučí dvě nebo více tříd do jedné). Tento vzor je zvláště užitečný k zajištění spolupráce nezávisle vyvinutých třídních knihoven. Mezi návrhové vzory Strukturální patří následující: - ADAPTER (adaptér) - BRIDGE (most) – uveden v programové části - COMPOSITE (skladba) - DECORATOR (dekorátor) - FACADE (fasáda) – uveden v programové části - FLYWEIGHT (muší váha) – uveden v programové části - PROXY (zástupce) 56 Adapter – Adaptér (třída, objekt) Účel Převádí rozhraní třídy na jiné rozhraní, které očekává klient. Adapter umožňuje spolupráci tříd, které by jinak spolu nefungovaly z důvodů nekompatibilních rozhraní. Jiné názvy Wrapper (obal) Motivace Jedna z velmi častých situací, která nastává při vývoji objektově orientovaných systémů, je nutnost zahrnout objekty tzv. třetích stran do vyvíjené aplikace. Tyto objekty poskytují požadovanou funkcionalitu, ale bohužel jejich rozhraní je nekompatibilní s požadavky vyvíjené aplikace. Mějme příklad grafického editoru, který manipuluje s různými tvary, jejichž supertřída Shape deklaruje požadované rozhraní. V našem případě se jedná o operaci getBoundingBox, která vrací obdélník opsaný danému tvaru. Zřejmě není problém tuto operaci implementovat pro třídu Line a jí obdobné. Stížená situace nastává v případě textových elementů jako je např. TextShape, kde implementace této funkcionality je komplikovaná použitými fonty, které ovlivňují velikost opsaného obdélníka. Na druhou stranu se nabízí využití již existující komponenty TextView, která tuto funkcionalitu implementuje, ale neposkytuje rozhraní, které by mohla třída DrawingEditor použít. Řešením je navrhnout třídu TextShape jako tzv. adaptér, který si udržuje vazbu na třídu TextView a v případě požadavku na výpočet opsaného obdelníku přepošle odpovídající zprávu getExtent na její instanci. Často je adaptér odpovědný za funkce, které přizpůsobená třída neposkytuje. Diagram ukazuje, jak adaptér může takové povinnosti plnit. Uživatel by měl mít možnost interaktivně „přetáhnout“ jakýkoli objekt Shape na nové místo, ale na to nebyl TextView navržen. TextShape může tuto chybějící funkčnost přidat pomocí implementací operace CreateManipulator objektu Shape. Tato operace vrací instanci příslušné podtřídy Manipulator. Manipulator je abstraktní třída pro objekty se znalostí animace objektu Shape jako reakce na vstup uživatele (např. přetažení tvaru na nové místo). Pro různé tvary existují podtřídy třídy Manipulator. TextShape přidává funkce, které TextView nemá, ale Shape je vyžaduje tím, že vrací instanci TextManipulator. Použití Vzor Adapter použijeme v těchto případech: - máme použít již existující třídu, ale její rozhraní se neshoduje s tím, které potřebujeme - chceme vytvořit třídu k znovupoužití, která spolupracuje s nepříbuznými nebo neznámými třídami 57 - (pouze objektový adaptér) musíme použít několik existujících podtříd, ale je nepraktické přizpůsobit jejich rozhraní vytvořením podtřídy pro každou z nich. Objektový adaptér umí přizpůsobit rozhraní své rodičovské třídy. Struktura Třídní adaptér používá vícenásobnou dědičnost k přizpůsobení dvou rozhraní: Objektový adaptér spoléhá na objektovou skladbu: Součásti Target (Shape) - definuje oblastně specifické rozhraní, které používá Client Client (DrawingEditor) - spolupracuje s objekty, které vyhovují cílovému rozhraní Target Adaptee (TextView) - definuje existující rozhraní, které potřebuje přizpůsobení Adapter (TextShape) - přizpůsobuje rozhraní Adaptee na cílové rozhraní Target Spolupráce Klienti volají operace na instanci Adapter. Vzápětí adaptér volá operace Adaptee, který žádost provede. Důsledky Aspekty třídního adaptéru: - adaptuje Adaptee na cíl Target tím, že angažuje konkrétní třídu Adaptee. V důsledku toho nebude třídní adaptér fungovat, chceme-li přizpůsobit třídu a všechny její podtřídy - Adapter může překrýt některé rysy chování adaptovaného Adaptee, neboť Adapter je podtřídou třídy Adaptee 58 - zavádí jen jeden objekt a nepotřebujeme již žádnou další nepřímost pomocí ukazatele, abychom se dostali k adaptovanému Aspekty objektového adaptéru: - jediný Adapter může pracovat s mnoha třídami Adaptee - znesnadňuje překrytí rysů chování třídy Adaptee. To vyžaduje vytvoření podtřídy třídy Adaptee a také to, aby se Adapter odkazoval na podtřídu, nikoli na třídu Adaptee. Měli bychom také vzít v úvahu: - do jaké míry adaptér přizpůsobuje? (objem práce, kterou adaptér vykoná, závisí na míře podobnosti rozhraní Target a Adaptee - zásuvné adaptéry (třídu lze znovupoužívat častěji, pokud se minimalizují předpoklady, které musí jiné třídy činit za účelem jejího použití) - poskytnutí průhlednosti pomocí obousměrných adaptérů (potenciálním problémem se může jevit to, že adaptéry nejsou pro všechny klienty průhledné) - možnost obousměrných adaptérů (jistý systém Unidraw má proměnnou StateVariable a QOCA má ConstraintVariable. Aby mohl systém Unidraw fungovat s QOCA, musí se ConstraintVariable přizpůsobit proměnné StateVariable; aby mohla souprava QOCA šířit řešení do Unidraw, musí se StateVariable přizpůsobit proměnné ConstraintVariable. Řešení spočívá v obousměrném třídním adaptéru ConstraintStateVariable. Tato podtřída ConstrainVariable a StateVariable vzájemně adaptuje dvě rozhraní.) Implementace Implementace třídních adaptérů v C++. Adapter by dědil od veřejné třídy Target a privátně od třídy Adaptee. Proto by Adapter byl podtypem Target, ale nikoli Adaptee. Zásuvné adaptéry. Prvním krokem je najít „úzké“ rozhraní pro Adaptee, tj. nejmenší množinu operací, jež umožňuje provést přizpůsobení. Úzké rozhraní vede ke třem implementačním přístupům: a) použití abstraktních operací. Definujeme odpovídající abstraktní operace pro úzké rozhraní Adaptee ve třídě TreeDisplay. Podtřídy musí implementovat abstraktní operace a přizpůsobit hierarchicky strukturovaný objekt. 59 DirectoryTreeDisplay vymezuje úzké rozhraní, aby mohlo zobrazit struktury adresářů skládající se z objektů FileSystemEntity. b) použití delegovaných objektů. U tohoto přístupu TreeDisplay předává žádosti o přístup k hierarchické struktuře delegovaného objektu. Prostřednictvím jiného delegáta může TreeDisplay použít jinou adaptační strategii. c) parametrizované adaptéry. Obvyklým způsobem podpory zásuvných adaptérů v jazyce Smalltalk je parametrizace adaptéru pomocí jednoho či více bloků. Blok může adaptovat žádost a adaptér může blok uložit pro každou individuální žádost. Např. TreeDisplay uloží jeden blok k převodu uzlu do GraphicNode a druhý blok pro přístup k potomkům uzlu (výborná alternativa pro tvorbu podtříd). directoryDisplay := (TreeDisplay on: treeRoot) getChildrenBlock: [:node | node getSubdirectories] createGraphicNodeBlock: [:node | node createGraphicNode]. Příklad Uvedeme si krátký náčrt implementace třídních a objektových adaptérů k příkladu z oddílu Motivace. Začneme třídami Shape a TextView. class Shape { public: Shape(); virtual void BoundingBox( Point& bottomLeft, Point& topRight ) const; virtual Manipulator* CreateManipulator() const; }; class TextView { public: TextView(); void GetOrigin(Coord& x, Coord& y) const; void GetExtent(Coord& width, Coord& height) const; virtual bool IsEmpty() const; }; Shape předpokládá rámeček ohraničení definovaný svými protilehlými rohy. TextView je definován výchozím bodem, výškou a šířkou. Shape též definuje operaci CreateManipulator 60 pro tvorbu objektu manipulátor, který umí animovat tvar, jestliže s ním uživatel manipuluje. Třída TextShape je adaptérem mezi těmito různými rozhraními. Třídní adaptér používá k adaptování rozhraní vícenásobnou dědičnost. Jedna větev dědičnosti se použije ke zdědění rozhraní (v C++ veřejně) a druhá ke zdědění implementace (v C++ privátně). class TextShape : public Shape, private TextView { public: TextShape(); virtual void BoundingBox( Point& bottomLeft, Point& topRight ) const; virtual bool IsEmpty() const; virtual Manipulator* CreateManipulator() const; }; Operace BoundingBox převádí rozhraní TextView tak, aby odpovídalo rozhraní Shape. void TextShape::BoundingBox ( Point& bottomLeft, Point& topRight ) const { Coord bottom, left, width, height; GetOrigin(bottom, left); GetExtent(width, height); bottomLeft = Point(bottom, left); topRight = Point(bottom + height, left + width); } Operace IsEmpty demonstruje přímé předání žádostí, jež jsou v realizacích adaptérů běžné: bool TextShape::IsEmpty () const { return TextView::IsEmpty(); } Nakonec definujeme CreateManipulator zcela od začátku (předpokládáme implementaci TextManipulator). Manipulator* TextShape::CreateManipulator () const { return new TextManipulator(this); } Objektový adaptér používá objektovou skladbu ke kombinaci tříd s různými rozhraními. V tomto přístupu si adaptér TextShape udržuje ukazatel na TextView. class TextShape : public Shape { public: TextShape(TextView*); virtual void BoundingBox( Point& bottomLeft, Point& topRight ) const; virtual bool IsEmpty() const; virtual Manipulator* CreateManipulator() const; private: TextView* _text; }; TextShape musí v konstruktoru inicializovat na instanci TextView. Také volá operace na svůj objekt TextView při volání vlastních operací. Předpokládáme, že klient vytvoří objekt TextView a předá jej konstruktoru TextShape: TextShape::TextShape (TextView* t) { _text = t; } 61 void TextShape::BoundingBox ( Point& bottomLeft, Point& topRight ) const { Coord bottom, left, width, height; _text->GetOrigin(bottom, left); _text->GetExtent(width, height); bottomLeft = Point(bottom, left); topRight = Point(bottom + height, left + width); } bool TextShape::IsEmpty () const { return _text->IsEmpty(); } Implementace CreateManipulator se podobá implementaci u třídního adaptéru. Objektový adaptér je složitější k sepsání, ale je tvárnější. Známá použití Programy, které vyžadují spustitelnost v nekompatibilním systému. Příbuzné vzory Bridge má strukturu podobnou objektovému adaptéru (ale má za úkol oddělit rozhraní od své implementace). Decorator zdokonaluje jiný objekt, aniž by měnil jeho rozhraní. Proxy definuje představitele či náhradníka jiného objektu a nemění jeho rozhraní. Composite – Skladba (objekt) Účel Skládá objekty do stromových struktur k vyjádření hierarchií typu část – celek. Composite umožňuje klientům jednotně zacházet s jednotlivými objekty i skladbami objektů. Motivace Stromová struktura je jedna z nejběžněji používaných způsobů jak skládat objekty. Typickým příkladem jsou uživatelská rozhraní, kde se vnější okno skládá z panelů obsahujících další panely, které jsou požadovanýcm způsobem rozmístěny a vnořovány do sebe. Nakonec jsou na definovaná místa vloženy primitivní prvky jako jsou textová pole, tlačítka apod. Stejně tak je možné vytvářet kresby, které se skládají z primitivních konstrukcí jako např. přímka, kružnice, obdélník, nebo z další vnořené kresby, se kterou však chceme manipulovat stejně jako s primitivními prvky (posouvat, kopírovat, vkládat atd.). Řešením takto rekurzivně definované struktury, kde se klient „nemusí starat“, zda-li manipuluje s primitivním prvkem nebo složeným je právě návrhový vzor Composite. Graphic je abstraktní třída definující rozhraní pro přidávaní (add), odebíraní (remove) nebo zpřístupnění prvku na dané pozici (getChild) a operaci draw, kterou budeme požadovat pro vykreslení grafického prvku. Podtřída Drawing je kompozicí, která umožňuje vkladát rekurzivně jiné kompozice nebo primitivní elementy. Třídy Rectangle i Circle dědí operace add, remove a getChild, ale tyto jsou prázdné, bez těla metody. To znamená, že např. zaslání zprávy add na instanci třídy Circle je ignorováno. 62 Následující obrázek ukazuje typickou strukturu složeného objektu s rekurzivně složenými objekty Graphic: Použití Vzor Composite použijeme v těchto případech: - chceme vyjádřit několik objektových hierarchií typu část – celek - chceme, aby klienti mohli ignorovat rozdíl mezi skladbami objektů a jednotlivými objekty (klienti zacházejí se všemi objekty ve složené struktuře jednotným způsobem) Struktura Typická objektová struktura Composite může vypadat takto: 63 Součásti Component (Graphic) - deklaruje rozhraní pro objekty ve skladbě - pokud se to hodí, implementuje výchozí chování rozhraní pro všechny třídy - deklaruje rozhraní pro přístup ke svým potomkovým komponentám a jejich správu - definuje rozhraní pro přístup k rodičům komponenty v rekurzivní struktuře a v případě vhodnosti implementuje Leaf (Rectangle, Line, Text…) - vyjadřuje listové objekty ve skladbě (list nemá žádné potomky) - definuje chování primitivních objektů ve skladbě Composite (Picture) - definuje chování komponent, které mají potomky - uchovává potomkové komponenty - implementuje operace týkající se potomků v rozhraní Component Client - zachází s objekty ve skladbě prostřednictvím rozhraní Component Spolupráce Klienti používají třídní rozhraní Component k interakci s objekty ve složené struktuře. Je-li příjemcem Leaf, je žádost zpracována přímo. Je-li příjemcem Composite, jsou žádosti obvykle předány jeho potomkovým komponentám s případným prováděním dalších operací předtím nebo potom. Důsledky Vzor Composite má tyto důsledky: - definuje třídní hierarchie, které se skládají z primitivních a složených objektů - zjednodušuje klienta (klienti mohou zacházet jednotně se složenými strukturami i jednotlivými objekty) - zjednodušuje přidávání nových druhů komponent - může návrh příliš zobecnit (ztěžuje omezení komponent skladby) Implementace Explicitní rodičovské odkazy. Udržování odkazů od potomkových komponent k jejich rodičům může zjednodušit procházení a správu složené struktury. Rodičovský odkaz zjednodušuje pohyb 64 strukturou nahoru a odstranění komponenty. Obvyklým místem pro definici rodičovského odkazu je třída Component. Třídy Leaf a Composite mohou dědit odkaz i operace, které odkaz spravují. Sdílení komponent. To z důvodů snížení požadavků na úložný prostor. Jestliže však komponenta nemůže mít více jak jednoho rodiče, poněkud se nám může sdílení komponent stát obtížnějším. Maximalizace rozhraní Component. Jedním z cílů vzoru Composite je dosáhnout toho, aby si klienti nebyli vědomi specifických tříd Leaf a Composite. Dosáhn e se to h otím, že třída Component definuje co nejvíce společných operací pro třídy Leaf a Composite, pro které nabídne výchozí implementace přičemž tyto implementace jsou překryty Leaf a Composite. Má třída Component implementovat seznam komponent? Vložení potomkového ukazatele do základní třídy způsobuje zvýšené prostorové nároky. Implementace je vhodná, jestliže existuje málo potomků. Řazení potomků. Příkladem by mohly být analyzační stromy. Pokud nepředstavuje řazení potomků problém, musíme k řízení jejich sekvence pečlivě navrhnout rozhraní pro přístup k potomkům a jejich správu. Zlepšení výkonu pomocí mezipaměti. Pokud skladby často procházíme, může mít Composite procházecí či vyhledávací informace o svých potomcích v mezipaměti. Kdo má komponenty odstraňovat? V případě, že třeba dojde ke zničení potomků, je dobré učinit třídu Composite odpovědnou za jejich odstranění. Jaká je nejlepší datová struktura k uložení komponent? Mohou být použity různé struktury, včetně propojených seznamů, stromů, řad a hašových tabulek. Příklad Zařízení, jako jsou počítače a zvukové komponenty, se často organizují do hierarchií typu část – celek. Tyto struktury se přirozeně modelují pomocí vzoru Composite. Třída Equipment definuje rozhraní pro všechna zařízení v hierarchii část – celek. class Equipment { public: virtual ~Equipment(); const char* Name() { return _name; } virtual Watt Power(); virtual Currency NetPrice(); virtual Currency DiscountPrice(); virtual void Add(Equipment*); virtual void Remove(Equipment*); virtual Iterator* CreateIterator(); protected: Equipment(const char*); private: const char* _name; }; Třída Equipment deklaruje operace, které vracejí atributy daného zařízení. Podtřídy tyto operace implementují pro určité druhy zařízení. K podtřídám třídy Equipment mohou patřit třídy Leaf, které představují diskové jednotky, integrované obvody a přepínače: class FloppyDisk : public Equipment { public: FloppyDisk(const char*); virtual ~FloppyDisk(); virtual Watt Power(); 65 virtual Currency NetPrice(); virtual Currency DiscountPrice(); }; CompositeEquipment je základní třídou pro zařízení obsahující jiná zařízení. Je také podtřídou třídy Equipment. class CompositeEquipment : public Equipment { public: virtual ~CompositeEquipment(); virtual Watt Power(); virtual Currency NetPrice(); virtual Currency DiscountPrice(); virtual void Add(Equipment*); virtual void Remove(Equipment*); virtual Iterator* CreateIterator(); protected: CompositeEquipment(const char*); private: List _equipment; }; CompositeEquipment definuje operace pro přístup k podzařízení a jeho správu. Operace Add a Remove vkládají a odstraňují zařízení ze seznam, jenž je uložen ve členu _equipment. Operace CreateIterator vrací iterátor, který tímto seznamem prochází. Výchozí implementaci NetPrice lze použít CreateIterator k sečtení cen netto daného podzařízení: Currency CompositeEquipment::NetPrice () { Iterator* i = CreateIterator(); Currency total = 0; for (i->First(); !i->IsDone(); i->Next()) { total += i->CurrentItem()->NetPrice(); } delete i; return total; } Nyní lze vyjádřit počítačové šasi (obsahuje diskové jednotky a základní desku…) jako podtřídu CompositeEquipment nazvanou Chassis. Chassis dědí operace týkající se potomků od třídy CompositeEquipment. class Chassis : public CompositeEquipment { public: Chassis(const char*); virtual ~Chassis(); virtual Watt Power(); virtual Currency NetPrice(); virtual Currency DiscountPrice(); }; Podobný způsobem lze definovat jiné kontejnery zařízení (Cabinet a Bus). Cabinet* cabinet = new Cabinet("PC Cabinet"); Chassis* chassis = new Chassis("PC Chassis"); cabinet->Add(chassis); Bus* bus = new Bus("MCA Bus"); bus->Add(new Card("16Mbs Token Ring")); 66 chassis->Add(bus); chassis->Add(new FloppyDisk("3.5in Floppy")); cout << "The net price is " << chassis->NetPrice() << endl; Známá použití Příklady vzoru Composite je možné nalézt v téměř všech objektově orientovaných systémech. Dále je vzor Skladba využíván také rámcovými systémem pro kompilátory jazyka Smalltalk. Další příklad se vyskytuje ve finanční sféře, kde portfolio seskupuje jednotlivá aktiva. Příbuzné vzory Decorator se často používá s Composite. Flyweight umožňuje sdílení komponent, ale ty se již nemohou odkazovat na své rodiče. Iterator je možné použít k procházení skladeb. Visitor lokalizuje operace a chování. Decorator – Dekorátor (objekt) Účel Dynamicky připojí k objektu další povinnosti. Dekorátoři poskytují při rozšiřování funkcí tvárnou alternativu k tvorbě podtříd. Jiné názvy Wrapper (obal). Motivace Někdy chceme přidat povinnosti jednotlivým objektům, ne celé třídě (souprava nástrojů pro grafická uživatelská rozhraní má umožňovat přidávání vlastností, jako je ohraničení či chování…). Dědičnost je jeden ze způsobů, jak přidávat povinnosti. Zdědění ohraničení od jiné třídy vloží ohraničení kolem instancí všech podtříd. To však není tvárné, neboť volba ohraničení je provedena staticky. Klient nemůže řídit způsob či okamžik obohacení komponenty. Tvárnější přístup je uzavřít komponentu do jiného objektu, jenž ohraničení přidá. Vnější objekt má název decorator. Decorator vyhovuje rozhraní komponenty, již obohacuje, takže je jeho přítomnost pro klienty komponenty průhledná. Decorator předává žádosti komponentě a může předtím nebo potom provádět další akce. Průhlednost umožňuje dekorátory rekurzivně vnořovat a tím povolovat neomezený počet přidávaných povinností. 67 Mějme uživatelské rozhraní, kde třída VisualComponent je supertřídou pro všechny viditelné elementy tvořící toto rozhraní. Jedním z nich může být např. třída TextView, jejíž operace draw má za cíl vykreslit tento element na obrazovce počítače. Tento textový element však bude třeba obohatit o další funkcionalitu, což může být orámování nebo možnost posouvání textu. Jedna z cest, jak tento problém řešit, je zavedení nových podtříd TextView rozšiřujících operaci draw o tyto funkce. Zřejmě se však jedná o statické řešení, které povede k explozi podtříd v případě většího počtu nově definovaných funkcí. Řešit lze tuto situaci zavedením speciálních tříd odvozených z třídy Decorator, které budou definovat požadované služby. Při běhu programu pak budou vytvořeny instance těchto dekorátérů podle potřeby s tím, že vytvoří řetěz objektů, na jehož konci je původní (dekorovaný) element. Operace draw nejprve vyvolá tutéž operaci následujícího objektu až po zmíněný původní a postupně se tento element doplní o služby poskytované jednotlivými dekorujícími objekty. Následující objektový diagram znázorňuje, jak skládat objekt TextView s objekty BorderDecorator a ScrollDecorator a vyprodukovat ohraničené a posouvatelné zobrazení textu: Použití Vzor Decorator použijeme v těchto případech: - dynamicky a průhledně přidáváme povinnosti jednotlivým objektům (bez ovlivnění jiných objektů) - povinnosti mohou být případně odebrány - když není rozšíření pomocí tvorby podtříd praktické 68 Struktura Součásti Component (VisualComponent) - definuje rozhraní pro objekty, k nimž lze dynamicky přidávat povinnosti ConcreteComponent (TextView) - definuje objekt, k němuž lze připojit další povinnosti Decorator - udržuje odkaz na objekt Component a definuje rozhraní, které vyhovuje rozhraní Component ConcreteDecorator (BorderDecorator, ScrollDecorator) - přidává povinnost komponentě Spolupráce Decorator předává žádosti svému objektu Component. Předtím nebo potom může volitelně provádět další operace. Důsledky Vzor Decorator má minimálně dva klady a dva zápory: - větší tvárnost než statická dědičnost (tvárnější způsob přidávání povinností objektům) - vyhýbá se funkcemi nabitým třídám v horní části hierarchie (nabízí přístup postupného přidávání povinností podle potřeby) - dekorátor a jeho komponenta nejsou identické (Decorator se chová jako průhledný obal – nelze spoléhat na totožnost objektů) - mnoho malých objektů (výsledkem návrhu jsou často systémy skládající se z mnoha malých objektů, které vypadají podobně) Implementace Přizpůsobení rozhraní. Objektové rozhraní dekorátoru musí vyhovovat rozhraní komponenty, kterou obohacuje. Třídy ConcreteDecorator proto musí dědit od společné třídy (alespoň v C++). Vynechání abstraktní třídy Decorator. Pokud potřebujeme přidat pouze jednu povinnost, není třeba definovat abstraktvní třídu Decorator (práce s existující třídní hierarchií na rozdíl od tvorby nové). 69 Udržování štíhlých tříd Component. Abychom zajistili vyhovující rozhraní, komponenty a dekorátoři musí pocházet ze společné třídy Component (společná třída by měla být štíhlá – má se zaměřit na definici rozhraní a nikoli na ukládání dat) Změna obalu objektu versus změna jeho vnitřku. Ke změnám vnitřku je vhodné použít vzor Strategy, příp adn ě tam, k d e je třída Component vnitřně robustní a tím je použití vzoru Decorator příliš nákladné. Protože vzor Decorator mění komponentu jenom zvenčí, komponenta nemusí vědět nic o svých dekorátorech; tj. dekorátoři jsou pro komponentu průhlední: Sama komponenta se strategiemi ví o možných rozšířeních. Takže s musí odkazovat na odpovídající strategie a udržovat je: Přístup na bázi Strategy může vyžadovat upravení komponenty k přijetí nových rozšíření. Příklad Následující zdroják ukazuje, jak implementovat dekorátory uživatelského rozhraní v C++. Předpokládáme, že existuje třída Component s názvem VisualComponent. class VisualComponent { public: VisualComponent(); virtual void Draw(); virtual void Resize(); }; Definujeme podtřídu třídy VisualComponent nazvanou Decorator, k níž vytvoříme podtřídu a tím získáme různé doplňky. class Decorator : public VisualComponent { public: Decorator(VisualComponent*); virtual void Draw(); virtual void Resize(); private: VisualComponent* _component; }; Podtřída Decorator obohacuje třídu VisualComponent, na niž odkazuje instanční proměnná _component, která je inicializována v konstruktoru. Pro každou operaci v rozhraní třídy VisualComponent podtřída Decorator definuje výchozí implementaci, která předává žádost do _component: void Decorator::Draw () { 70 _component->Draw(); } void Decorator::Resize () { _component->Resize(); } Podtřídy třídy Decorator definují specifické doplňky. Třída BorderDecorator, která je podtřídou třídy Decorator a která překrývá operaci Draw, přidává ohraničení. BorderDecorator též definuje privátní pomocnou operaci DrawBorder, jež vykreslení provádí. class BorderDecorator : public Decorator { public: BorderDecorator(VisualComponent*, int borderWidth); virtual void Draw(); private: void DrawBorder(int); private: int _width; }; void BorderDecorator::Draw () { Decorator::Draw(); DrawBorder(_width); } Podobná implementace by následovala pro ScrollDecorator a DropShadowDecorator. Následující zdrojový text ilustruje, jak lze dekorátory použít k tvorbě ohraničeného a posunovatelného TextView. Nejprve vložíme vizuální komponentu do okenního objektu (operace SetContents): void Window::SetContents (VisualComponent* contents) { } Nyní lze vytvořit textový náhled a okno, do něhož jej můžeme vložit: Window* window = new Window; TextView* textView = new TextView; TextView je VisualComponent, což umožňuje jeho vložení do okna: window->SetContents(textView); Nicméně chceme ohraničený a posunovatelný TextView. Takže než jej vložíme do okna, příslušně je obohatíme: window->SetContents( new BorderDecorator( new ScrollDecorator(textView), 1 ) ); Protože Windows přistupuje ke svému obsahu prostřednictvím rozhraní VisualComponent, není si vědoma přítomnosti dekorátoru. Známá použití Vzor Decorator nám dovoluje elegantním způsobem přidat do proudů povinnosti (komprimovat proudová data pomocí různých kompresních algoritmů, zredukovat proudová data do 7bit znaků ASCII pro přenos komunikačním kanálem ASCII). Příbuzné vzory Adapter: V Decoratoru se mění pouze povinnosti objektu a nikoli jeho rozhraní. 71 Composite: Decorator není určen k seskupení objektů. Strategy: Decorator poskytuje možnost měnit obal objektu, Strategy umožňuje měnit vnitřek. Proxy – Zástupce (objekt) Účel Poskytuje náhradníka či místodržitele za jiný objekt za účelem řízení přístupu k objektu. Jiné názvy Surrogate (náhradník) Motivace Jeden z důvodu pro řízení přístupu k objektu je odložení plných nákladů na jeho vytvoření a inicializaci až do doby, kdy jej skutečně potřebujeme použít. Vezměme si textový editor, jenž umí zakomponovat grafické objekty do dokumentu. Některé grafiky může být velmi nákladné vytvořit. To svádí k myšlence vytvářet všechny nákladné objekty na požádání. Řešením je použít jiný objekt, zástupce obrazu, který se chová jako náhradník za skutečný obraz. Pokud je to požadováno, tak se zástupce chová přesně stejně jako obraz a stará se o tvorbu jeho instance. Zástupce obrazu vytváří skutečný obraz, jen když ho textový editor požádá o jeho zobrazení tím, že vyvolá operaci Draw. Zástupce předá další žádosti přímo na obraz a proto musí udržovat odkaz na obraz po jeho vytvoření. Předpokládejme, že obrazy jsou uloženy ve zvláštních souborech. V tomto případě lze použít název souboru jako odkaz na skutečný objekt. Zástupce také ukládá jeho rozsah, který umožňuje zástupci reagovat na žádosti o jeho velikosti od formátovacího systému, aniž by musel vytvořit vlastní instanci obrazu. Následující třídní diagram ilustruje tento příklad podrobněji: 72 Textový editor přistupuje k zakomponovaným obrazům pomocí rozhraní definovaného abstraktní třídou Graphic. ImageProxy je třída pro obrazy, které jsou vytvářené na požádání (udržuje název souboru jako odkaz na obraz, který je na disku). Použití Proxy je použitelné, kdykoliv existuje potřeba univerzálnějšího či důmyslnějšího odkazu na objekt, než je jednoduchý ukazatel. Proxy: - vzdálený zástupce poskytuje místního představitele za objekt v jiném adresovém prostoru - virtuální zástupce vytváří nákladné objekty na požádání - ochranný zástupce řídí přístup k původnímu objektu (když mají mít objekty různá přístupová práva) - chytrý odkaz je náhradou za holý ukazatel, který vykonává další akce, když je k objektu přistupováno (evidence počtu odkazů na skutečný objekt, načtení trvalého objektu do paměti, kontrola uzamčení skutečného objektu před samým přístupem) Struktura Zde je možný objektový diagram struktury zástupce za běhu: Součásti Proxy (ImageProxy) - udržuje odkaz umožňující, aby zástupce přistupoval ke skutečnému subjektu - poskytuje rozhraní shodné s rozhraním Subject, aby bylo možné zástupce substituovat za skutečný objekt - řídí přístup ke skutečnému subjektu a může být odpovědný za jeho tvorbu a odstranění - vzdálení zástupci jsou odpovědní za zakódování žádosti a jejích argumentů a za poslání zakódované žádosti skutečnému subjektu v jiném adresovém prostoru - virtuální zástupci mohou dávat další informace o skutečném subjektu do mezipaměti, aby mohli přístup k objektu oddálit 73 - ochranní zástupci kontrolují, zda má volající přístupová práva, která jsou vyžadována k provedení žádosti Subject (Graphic) - definuje společné rozhraní pro RealSubject a Proxy, aby bylo možné Proxy použít, kdekoli se očekává RealSubject RealSubject (Image) - definuje skutečný objekt, který zástupce představuje Spolupráce Proxy předává žádosti objektu RealSubject, když je to vhodné, v závislosti na druhu zástupce. Důsledky Proxy zavádí úroveň nepřímosti pro přístup k objektu. Přídavná nepřímost má mnoho využití v závislosti na druhu zástupce: - vzdálený zástupce může skrýt skutečnost, že objekt přebývá v jiném adresovém prostoru - virtuální zástupce může vykonávat optimalizace (např. tvorba objektu na požádání) - ochranní zástupci a chytré odkazy umožňují další praktické úkoly, když se k objektu přistupuje Kopie při zápisu může podstatně snížit náklady na kopírování velkých a složitých objektů. Implementace Přetěžování operátoru členského přístupu v C++. Jazyk C++ podporuje přetěžování operator>, čili operátoru členského přístupu. Přetěžování tohoto operátoru umožňuje vykonávat další práci, kdykoli se zruší odkaz na objekt. Následující příklad znázorňuje, jak tento postup použít k implementaci virtuálního zástupce s názvem ImagePtr. class Image; extern Image* LoadAnImageFile(const char*); // vnejsi funkce class ImagePtr { public: ImagePtr(const char* imageFile); virtual ~ImagePtr(); virtual Image* operator->(); virtual Image& operator*(); private: Image* LoadImage(); private: Image* _image; const char* _imageFile; }; ImagePtr::ImagePtr (const char* theImageFile) { _imageFile = theImageFile; _image = 0; } Image* ImagePtr::LoadImage () { if (_image == 0) { _image = LoadAnImageFile(_imageFile); } return _image; 74 } Přetěžované operátory -> a * používají LoadImage ke vrácení _image volajícím, příp. načteným. Image* ImagePtr::operator-> () { return LoadImage(); } Image& ImagePtr::operator* () { return *LoadImage(); } Tento přístup umožňuje volat operace Image prostřednictvím objektů ImagePtr, aniž by bylo nutné se namáhat a přidávat opeace jako součásti rozhraní ImagePtr: ImagePtr image = ImagePtr("anImageFileName"); image->Draw(Point(50, 100)); // (image.operator->())->Draw(Point(50, 100)) Zástupce image se chová jako ukazatel, ale není deklarován jako ukazatel na Image (nelze jej jako skutečný ukazatel použít). Někdy není dobré přetěžování členského operátoru pro všechny druhy zástupců. Někteří musí přesně vědět, které operace jsou volány (přetěžování zde nefunguje). Použití doesNotUnderstand v jazyce Smalltalk. Smalltalk poskytuje háček, který lze použít pro podporu automatického předávání žádostí. Když klient posílá zprávu přijmi, který nemá vhodnou metodu, zavolá Smalltalk doesNotUnderstand: aMessage. Třídy Proxy může předefinovat doesNotUnderstand tak, aby se zpráva předala jeho subjektu. Aby se zajistilo, že je žádost předána subjektu a nikoli jen tiše pohlcena zástupcem, lze definovat třídu Proxy, která nerozumí žádným zprávám (definice Proxy jako třídy s absencí nadtřídy). Hlavní nevýhodou doesNotUnderstand: je, že většina systémů Smalltalk má tak málo speciálních zpráv (navíc byl doesNotUnderstand: vyvinut pro zpracování chyb a nikolik pro tvorbu zástupců) – při návrhu je nutné problém obejít. Zástupce nemusí vždy znát typ skutečného subjektu. Pokud může třída Proxy zacházet se svým subjektem výhradně pomocí abstraktního rozhraní, není nutné vytvářet třídu Proxy pro každou třídu RealSubject. Příklad Následující zdrojový text implementuje dva druhy zástupců: Virtuální zástupce Třída Graphic definuje rozhraní pro grafické objekty: class Graphic { public: virtual ~Graphic(); virtual void Draw(const Point& at) = 0; virtual void HandleMouse(Event& event) = 0; virtual const Point& GetExtent() = 0; virtual void Load(istream& from) = 0; virtual void Save(ostream& to) = 0; protected: Graphic(); }; Třída Image implementuje rozhraní Graphic k zobrazení obrazových souborů. class Image : public Graphic { 75 public: Image(const char* file); // nahrava obrazek ze souboru virtual ~Image(); virtual void Draw(const Point& at); virtual void HandleMouse(Event& event); virtual const Point& GetExtent(); virtual void Load(istream& from); virtual void Save(ostream& to); private: }; ImageProxy má stejné rozhraní jako Image: class ImageProxy : public Graphic { public: ImageProxy(const char* imageFile); virtual ~ImageProxy(); virtual void Draw(const Point& at); virtual void HandleMouse(Event& event); virtual const Point& GetExtent(); virtual void Load(istream& from); virtual void Save(ostream& to); protected: Image* GetImage(); private: Image* _image; Point _extent; char* _fileName; }; Konstruktor ukládá místní kopii názvu souboru, který obraz uchovává, a inicializuje _extent a _image: ImageProxy::ImageProxy (const char* fileName) _fileName = strdup(fileName); _extent = Point::Zero; _image = 0; } Image* ImageProxy::GetImage() { if (_image == 0) { _image = new Image(_fileName); } return _image; } { Implementace GetExtent vrací rozsah v mezipaměti, pokud to je možné; jinak se obraz načte ze souboru. Draw obraz načte a HandleMouse předá událost skutečnému obrazu. const Point& ImageProxy::GetExtent () { if (_extent == Point::Zero) { _extent = GetImage()->GetExtent(); } return _extent; } void ImageProxy::Draw (const Point& at) { GetImage()->Draw(at); } void ImageProxy::HandleMouse (Event& event) { GetImage()->HandleMouse(event); } 76 Operace Save uloží mezipaměťový rozsah obrazu a název obrazového souboru do proudu. Load tyto informace získá a zinicializuje odpovídající členy. void ImageProxy::Save (ostream& to) { to << _extent << _fileName; } void ImageProxy::Load (istream& from) { from >> _extent >> _fileName; } Konečně předpokládáme, že máme třídu TextDocument obsahující objekty Graphic: class TextDocument { public: TextDocument(); void Insert(Graphic*); }; ImageProxy můžeme vložit do textového dokumentu takto: TextDocument* text = new TextDocument; text->Insert(new ImageProxy("anImageFileName")); Zástupci používající doesNotUnderstand. V jazyce Smalltalk lze tvořit obecné zástupce pomocí definice tříd, jejichž nadtřída je nulová a definice metody doesNotUnderstand: ke zpracování zpráv. Následující metoda předpokládá, že má zástupce metodu realSubject vracející svůj skutečný subjekt. doesNotUnderstand: aMessage ^ self realSubject perform: aMessage selector withArguments: aMessage arguments Argument pro doesNotUnderstand: je instance Message představující zprávu, kterou zástupce nepochopil. Jednou z výhod doesNotUnderstand: je, že může provádět libovolné zpracování. Známá použití Použití zástupců jako představitelů objektů, které se mohou distribuovat. Přístup ke vzdáleným objektům. Příbuzné vzory Adapter poskytuje jiné rozhraní k objektu, který přizpůsobuje. Proxy poskytuje stejné rozhraní jako jeho subjekt. Decorator přidává jednu nebo více povinností objektu, Proxy řídí přístup k objektu. Proxy se liší stupněm, do něhož jsou implementováni jako Decorator. Ochranný zástupce může být implementován přesně jako Decorator Na druhé straně vzdálený zástupce neobsahuje přímý odkaz na svůj skutečný subjekt, ale jen nepřímý odkaz (např. ID hostitele a místní adresa u hostitele). Virtuální zástupce začne s nepřímým odkazem, jako je název souboru, ale nakonec přímý odkaz získá a použije. 77 Návrhové vzory Chování (behavioral patterns) Vzory chování se zabývají algoritmy rozdělení povinností mezi objekty. Nepopisují je vzory objektů nebo tříd, ale také vzory pro komunikaci mezi nimi. Charakterizují komplexní řídící proces, který je obtížné sledovat za běhu. Mezi návrhové vzory Chování patří následující:</p> - COMMAND (příkaz) – uveden v programové části - CHAIN OF RESPONSIBILITY (řetěz odpovědnosti) - ITERATOR (iterátor) - OBSERVER (pozorovatel) - STATE (stav) - STRATEGY (strategie) Chain of Responsibility – Řetěz odpovědnosti (objekt) Účel Vyhýbá se spojení odesílatele žádosti s příjemcem tím, že umožní více než jednomu objektu žádost zpracovat. Zřetězí objekty příjemců a předává prostřednictvím řetězu žádost, až ji nějaký objekt zpracuje. Motivace Součástí moderních softwarových systémů je poskytnutí pomocné informace (help), která je asociována s jednotlivými částmi uživatelského rozhraní. Poskytnutí nápovědy závisí na vybrané části tohoto rozhraní. Pokud není s tímto prvkem rozhraní žádná nápověda spojena, předává se tento požadavek dále ke zpracování prvku rozhraní, který obsahuje prvek původní. Tento postup se opakuje až do okamžiku, kdy je některý z prvků uživatelského rozhraní schopen tuto nápovědu poskytnout. Dochází tedy zřetězení zpracování požadavku. Podstata návrhového vzoru Chain of responsibility spočívá v tom, odesílatel požadavku není pevně spojen s příjemcem tohoto požadavku, ale jeho zpracování je řešeno průchodem přes zřetězené objekty až k tomu, který je schopen tento požadavek zpracovat (implicitní příjemce). Celou situaci lze demonstrovat na příkladu výše zmíněné nápovědy. Použití Chain of responsibility používáme v těchto situacích: - žádost může zpracovat více než jeden objekt a zpracovatel není předem znám. Zpracovatel má být zjištěn automaticky 78 - chceme vystavit žádost jednomu z několika objektů, aniž bychom explicitně určovali příjemce - sada objektů, které mohou žádost zpracovat, má být určena dynamicky Struktura Typická objektová struktura může vypadat takto: Součásti Handler (HelpHandler) - definuje rozhraní ke zpracování žádostí - (volitelné) implementuje propojení na následníka ConcreteHandler - zpracovává žádosti, za něž je odpovědný - může přistupovat ke svému následníkovi - pokud může ConcreteHandler žádost zpracovat, učiní tak; jinak žádost předá svému následníkovi Client - inicializuje žádost na objekt ConcreteHandler v řetězu Spolupráce Žádost vystavená klientem se předává po řetězu do doby, kdy objekt ConcreteHandler přijme odpovědnost za její zpracování. Důsledky Chain of responsibility má tyto důsledky: - snížené spojení (objekt musí pouze vědět, že žádost bude „vhodně“ zpracována; Chain of responsibility může zjednodušit síť objektových propojení) - přídavná tvárnost při přiřazování povinností objektům (odpovědnost za zpracování žádosti lze přidat či měnit za běhu přidáním objektu do řetězu) 79 - příjem není zaručen (protože žádost nemá explicitního příjemce, není zaručeno její zpracování) Implementace Implementace řetězu následníků. Existují dva způsoby, jak implementovat řetěz následníků: a) definovat nová propojení (obvykle ve třídě Handler nebo možné taky ConcreteHandler) b) použít existující spojení Spojování následníků. Pokud neexistují žádné předem dané odkazy pro definici řetězu, musíme je zavést sami. V tomto případě třída Handler nejen definuje rozhraní pro žádosti, ale také obvykle udržuje následníka. Zde je základní třída HelpHandler, která udržuje propojení na následníka: class HelpHandler { public: HelpHandler(HelpHandler* s) : _successor(s) { } virtual void HandleHelp(); private: HelpHandler* _successor; }; void HelpHandler::HandleHelp () { if (_successor) { _successor->HandleHelp(); } } Vyjádření žádostí. K vyjádření žádostí máme k dispozici různé možnosti. Nejjednodušší tvar pevně naprogramuje žádost pro vyvolání operace (HandleHelp). Tím lze předávat pouze pevnou sadu žádostí, jež definuje třída Handler. Alternativou může být použití jediné funkce zpracovatele, která má kód žádosti jako parametr (řetězec, celočíselná konstanta). Tento přístup vyžaduje podmíněné příkazy k odbavení žádosti na základě jejích kódu. Abychom vyřešili problém předání parametrů, lze použít zvláštní objekty, které přibalují zkoumající parametry. Třída Request může vyjadřovat žádosti explicitně a nové druhy žádostí můžeme definovat pomocí tvorby podtřídy. Podtřídy mohou definovat různé parametry. Zpracovatelé potřebují znát druh žádosti (tj. kterou podtřídu třídy Request používají) pro přístup k těmto parametrům. Aby bylo možné žádost identifikovat, může Request definovat přístupovou funkci, která vrací identifikátor třídy. Zde je nastíněna odbavovací funkce, která používá badatelské objekty k identifikaci žádosti. Operace GetKind definovaná v základní třídě Request identifikuje druh žádosti. void Handler::HandleRequest (Request* theRequest) { switch (theRequest->GetKind()) { case Help: // argument k vyhrazeni typu HandleHelp((HelpRequest*) theRequest); break; case Print: HandlePrint((PrintRequest*) theRequest); break; default: break; } } 80 Podtřídy mohou rozšířit odbavení překrytím HandleRequest. Zde je příklad, jak podtřída ExtendedRequest rozšiřuje verzi operace HandleRequest třídy Handler: class ExtendedHandler : public Handler { public: virtual void HandleRequest(Request* theRequest); }; void ExtendedHandler::HandleRequest (Request* theRequest) { switch (theRequest->GetKind()) { case Preview: // rizeni požadavek přehledu break; default: // Handler ridi jiné zadosti Handler::HandleRequest(theRequest); } } Automatické předávání v jazyce Smalltalk. Tady lze použít k předání žádostí mechanismus doesNotUnderstand. Zprávy, jež nemají odpovídající metody, se zachytí v implementaci doesNotUnderstand, kterou lze překrýt k předávání zprávy následníkovi objektu. Příklad Následující příklad ukazuje, jak může řetěz odpovědnosti zpracovávat žádosti pro systém nápovědy online. Žádost o nápovědu je explicitní operace. K šíření žádostí mezi pomůckami v řetězu použijeme existujících rodičovských odkazů. Třída HelpHandler definuje rozhraní ke zpracování žádostí o nápovědu. Udržuje téma nápovědy a odkaz na svého následníka v řetězu zpracovatelů nápovědy. Klíčovou operací je HandleHelp, kterou překrývají podtřídy. HasHelp kontroluje, zda existuje přidružené téma nápovědy. typedef int Topic; const Topic NO_HELP_TOPIC = -1; class HelpHandler { public: HelpHandler(HelpHandler* = 0, Topic = NO_HELP_TOPIC); virtual bool HasHelp(); virtual void SetHandler(HelpHandler*, Topic); virtual void HandleHelp(); private: HelpHandler* _successor; Topic _topic; }; HelpHandler::HelpHandler ( HelpHandler* h, Topic t ) : _successor(h), _topic(t) { } bool HelpHandler::HasHelp () { return _topic != NO_HELP_TOPIC; } void HelpHandler::HandleHelp () { if (_successor != 0) { _successor->HandleHelp(); } } Všechny pomůcky jsou podtřídami abstraktní třídy Widget, která je podtřídou třídy HelpHandler. class Widget : public HelpHandler { protected: Widget(Widget* parent, Topic t = NO_HELP_TOPIC); 81 private: Widget* _parent; }; Widget::Widget (Widget* w, Topic t) : HelpHandler(w, t) { _parent = w; } V našem příkladu je tlačítko prvním zpracovatelem v řetězu. Třída Button je podtřídou třídy Widget. Konstruktor Button má dva parametry: odkaz na svou uzavírající pomůcku a na téma nápovědy. class Button : public Widget { public: Button(Widget* d, Topic t = NO_HELP_TOPIC); virtual void HandleHelp(); // operace Widget, které prekryva Button... }; Pokud neexistuje žádné téma nápovědy pro tlačítka, předá se žádost následníkovi pomocí operace HandleHelp ve třídě Handler. Pokud téma nápovědy existuje, tlačítko jej zobrazí a hledání skončí. Button::Button (Widget* h, Topic t) : Widget(h, t) { } void Button::HandleHelp () { if (HasHelp()) { // nabidnout napovedu o tlacitko } else { HelpHandler::HandleHelp(); } } Dialog implementuje podobné schéma kromě toho, že jeho následník není pomůcka, ale jakýkoli zpracovatel nápovědy. V naší aplikaci je tento následník instancí Application. class Dialog : public Widget { public: Dialog(HelpHandler* h, Topic t = NO_HELP_TOPIC); virtual void HandleHelp(); // operace Widget ktere prekryva Dialog... }; Dialog::Dialog (HelpHandler* h, Topic t) : Widget(0) { SetHandler(h, t); } void Dialog::HandleHelp () { if (HasHelp()) { // nabidnout napovedu o dialogu } else { HelpHandler::HandleHelp(); } } Na konci řetězu je instance třídy Application. Když žádost o nápovědu dorazí na tuto úroveň, aplikace může dodat informace o aplikaci obecně, nebo může nabídnout seznam různých témat nápovědy: class Application : public HelpHandler { public: Application(Topic t) : HelpHandler(0, t) { } virtual void HandleHelp(); // aplikacne specificke operace... }; void Application::HandleHelp () { 82 // zobrazit seznam seznam temat napovedy } Následující kód tyto objekty vytváří a propojuje. Zde se dialog týká tisku, a tak mají objekty přiřazená tisková témata. const Topic PRINT_TOPIC = 1; const Topic PAPER_ORIENTATION_TOPIC = 2; const Topic APPLICATION_TOPIC = 3; Application* application = new Application(APPLICATION_TOPIC); Dialog* dialog = new Dialog(application, PRINT_TOPIC); Button* button = new Button(dialog, PAPER_ORIENTATION_TOPIC); Žádost o nápovědu lze vyvolat pomocí volání HandleHelp na jakýkoli objekt v řetězu. Aby se spustilo hledání u tlačítkového objektu, stačí na něm zavolat HandleHelp: button->HandleHelp(); V tomto případě zpracuje tlačítko žádost ihned. Známá použití Chain of Responsibility používá mnoho třídních knihoven ke zpracovávání uživatelských událostí. Zpracování aktualizace grafiky. Příbuzné vzory Tento vzor se často aplikuje ve spojení s Composite. Iterator - Iterátor Účel Poskytuje způsob sekvenčního přístupu k prvkům seskupeného objektu, aniž by se vystavilo jeho vnitřní vyjádření. Jiné názvy Cursor (ukazatel). Motivace Seskupený objekt (seznam aj.), má poskytovat způsob přístupu k jeho prvkům, aniž by se musela vystavit jeho vnitřní struktura. Navíc musíme seznam procházet různými způsoby v závislosti na tom, co chceme dosáhnout. Pravděpodobně ale nechceme nafouknout rozhraní List operacemi pro různá procházení. Také se nám může hodit, aby na stejném seznamu probíhalo více procházení najednou. Vzor Iterator to vše umožňuje. Klíčovou myšlenkou je odebrat odpovědnost za přístup a procházení z objektu seznamu a vložit ji do objektu iterátoru. Např. třída List zavolá ListIterator s tímto vztahem mezi nimi: Než můžeme vytvořit instanci ListIterator, musíme dodat List, který se má procházet. Jakmile máme instanci ListIterator, lze k prvkům seznamu přistupovat sekvenčně. Operace 83 CurrentItem vrací aktuální prvek seznamu, First aktuální prvek inicializuje na první prvek, Next aktuální prvek posouvá na další a IsDone testuje, zda jsme se neposunuli za poslední prvek. Oddělení procházecího mechanismu od objektu List umožňuje definovat iterátory pro různá procházecí pravidla, aniž bychom je museli vyjmenovat v rozhraní List. Klient musí vědět, že se jedná o procházený seznam a nikoli o nějakou jinou seskupenou strukturu. Jako příklad předpokládejme, že máme implementaci seznamu SkipList. Chceme napsat zdrojový text, který funguje pro objekty List i SkipList. Definujeme třídu AbstractList, která poskytuje společné iterační rozhraní. Podobně potřebujeme abstraktní třídu Iterator, jež definuje společné iterační rozhraní. Potom lze definovat konkrétní podtřídy třídy Iterator pro různé implementace seznamu. Výsledkem je, že se iterační mechanismus stává nezávislým na konkrétních seskupených třídách. Použití Vzor Iterator použijeme v těchto situacích: - přístup k obsahu seskupeného objektu, aniž se vystaví jeho vnitřní vyjádření - podpora více způsobů procházení seskupenými objekty - poskytnutí jednotného rozhraní k procházení různých seskupených struktur (podpora polymorfní iterace) Struktura 84 Součásti Iterator - definuje rozhraní pro přístup k prvkům a jejich procházení ConcreteIterator - implementuje rozhraní Iterator - sleduje aktuální pozici při procházení seskupení Aggregate - definuje rozhraní k tvorbě objektu Iterator ConcreteAggregate - implementuje rozhraní k tvorbě objektu Iterator a vrací instanci třídy ConcreteIterator Spolupráce ConcreteIterator sleduje aktuální objekt v seskupení a může vypočítat následující objekt procházení. Důsledky Vzor Iterator má tyto důsledky: - podporuje variace při procházení seskupení - iterátory zjednodušují rozhraní Aggregate (procházecí rozhraní iterátoru se zbavuje potřeby podobného rozhraní ve třídě Aggregate) - v seskupení může zároveň probíhat více procházení najednou (protože Iterátor sleduje vlastní stav procházení) Implementace Kdo iteraci řídí? Je-li iterace řízena klientem, nazývá se iterátor externí (klienti si musí požádat o další prvek a posun při procházení). Je-li iterace řízena iterátorem, jedná se o interní iterátor (klient předá iterátoru operaci a iterátor ji aplikuje na každý prvek seskupení). Kdo definuje procházecí algoritmus? Může být definováno v iterátoru nebo v seskupení (iterátor se pak využije k uložení stavu iterace). Jak je iterátor robustní? Může být nebezpečné seskupení při procházení modifikovat. Jednoduché řešení je procházet kopii seskupení. Lepším způsobem je robustní iterátor, který zajišťuje, že vkládání a odstraňování nenaruší procházení bez kopírování seskupení. Další operace Iterátora. Minimální Iterátor se skládá z operací First, Next, IsDone a CurrentItem. Další můžou být Previous a SkipTo. Použití polymorfních iterátorů v jazyce C++. Ty vyžadují, aby byl objekt iterátora dynamicky alokován pomocí tovární metody – je vhodné je použít, jestliže existuje polymorfie. Další nevýhodou je to, že klient si musí polymorfní iterátory odstranit sám. Iterátory mohou mít privilegovaný přístup. Ten však může znesnadnit definice nových procházení, protože vyžaduje změnu rozhraní seskupení v podobě přidání dalšího „přítele“ (těsné spojení iterátoru a seskupení). Iterátory pro Composite. Externí iterátory mohou být nesnadno implementovatelné na rekurzivně seskupených strukturách, které se podobají Composite, neboť pozice ve struktuře může překlenout mnoho úrovní vnořených seskupení. Proto musí externí iterátor ukládat cestu Skladbou, aby aktuální objekt sledoval. 85 Prázdné iterátory. NullIterator je degenerovaný iterátor, který pomáhá při zvládnutí mezních podmínek. Operace IsDone je vždy pravdivá. Příklad Rozhraní tříd List a Iterator. Nejdříve se podíváme na část rozhraní List. template <class Item> class List { public: List(long size = DEFAULT_LIST_CAPACITY); long Count() const; Item& Get(long index) const; }; Abychom aktivovali transparentní použití různých procházení, definujeme abstraktní třídu Iterator, která definuje rozhraní iterátorů. template <class Item> class Iterator { public: virtual void First() = 0; virtual void Next() = 0; virtual bool IsDone() const = 0; virtual Item CurrentItem() const = 0; protected: Iterator(); }; Implementace třídy Iterator. ListIterator je podtřídou třídy Iterator. template <class Item> class ListIterator : public Iterator<Item> { public: ListIterator(const List<Item>* aList); virtual void First(); virtual void Next(); virtual bool IsDone() const; virtual Item CurrentItem() const; private: const List<Item>* _list; long _current; }; Implementace ListIterator je přímočará. Do seznamu ukládá List včetně indexu _current: template <class Item> ListIterator<Item>::ListIterator ( const List<Item>* aList ) : _list(aList), _current(0) { } First umisťuje iterátor na první prvek: template <class Item> void ListIterator<Item>::First () { _current = 0; } 86 Next posune aktuální prvek: template <class Item> void ListIterator<Item>::Next () { _current++; } IsDone kontroluje, zda se index odkazuje na prvek v rámci seznamu: template <class Item> bool ListIterator<Item>::IsDone () const { return _current >= _list->Count(); } Nakonec CurrentItem vrací položku na aktuálním indexu: template <class Item> Item ListIterator<Item>::CurrentItem () const { if (IsDone()) { throw IteratorOutOfBounds; } return _list->Get(_current); } Použití iterátorů. Máme seznam List s objekty Employee a chceme vytisknout všechny zaměstnance v seznamu (pomocí operace Print). Operace použije iterátor k procházení a vytištění seznamu. void PrintEmployees (Iterator<Employee*>& i) { for (i.First(); !i.IsDone(); i.Next()) { i.CurrentItem()->Print(); } } Operaci lze použít k vytištění zaměstnanců v obou pořadích. List<Employee*>* employees; ListIterator<Employee*> forward(employees); ReverseListIterator<Employee*> backward(employees); PrintEmployees(forward); PrintEmployees(backward); Zajištění, že se iterátory odstraní. CreateIterator vrací nově alokovaný objekt iterátora. Jsme odpovědni za jeho odstranění. Pokud zapomeneme, unikne nám úložný prostor. Abychom usnadnili život klientům, poskytneme IteratorPtr, který jedná jako zástupce za iterátora. Bere si na starost odstranění objektu Iterator, když se dostaneme mimo dosah. template <class Item> class IteratorPtr { public: IteratorPtr(Iterator<Item>* i): _i(i) { } ~IteratorPtr() { delete _i; } Iterator<Item>* operator->() { return _i; } Iterator<Item>& operator*() { return *_i; } private: // znemozni kopii a prirazeni, abychom se // vyhnuli vicenasobnym odstraněním _i IteratorPtr(const IteratorPtr&); IteratorPtr& operator=(const IteratorPtr&); private: Iterator<Item>* _i; 87 }; IteratorPtr nám umožňuje zjednodušit kód pro tisk: AbstractList<Employee*>* employees; IteratorPtr<Employee*> iterator(employees->CreateIterator()); PrintEmployees(*iterator); Implementace interní – pasivní třídy ListIterator. C++ nepodporuje anonymní funkce ani ukončování. Existují dvě možnosti: buď předat ukazatel funkci nebo spoléhat na tvorbu podtříd. Zde je náznak druhé možnost, která používá tvorbu podtříd. Interní iterátor nazýváme ListTraverser. template <class Item> class ListTraverser { public: ListTraverser(List<Item>* aList); bool Traverse(); protected: virtual bool ProcessItem(const Item&) = 0; private: ListIterator<Item> _iterator; }; Traverse spustí procházení a volá ProcessItem pro každou položku. Interní iterátor může zvolit ukončení procházení tím, že vrátí hodnotu false z operace ProcessItem. Traverse vrací údaj o tom, zda se procházení neukončilo předčasně. template <class Item> ListTraverser<Item>::ListTraverser ( List<Item>* aList ) : _iterator(aList) { } template <class Item> bool ListTraverser<Item>::Traverse () { bool result = false; for ( _iterator.First(); !_iterator.IsDone(); _iterator.Next() ) { result = ProcessItem(_iterator.CurrentItem()); if (result == false) { break; } } return result; } Použijeme ListTraverser k vytištění prvních deseti zaměstnanců ze seznamu. Počet vytištěných zaměstnanců počítáme v instanční proměnné _count. class PrintNEmployees : public ListTraverser<Employee*> { public: PrintNEmployees(List<Employee*>* aList, int n) : ListTraverser<Employee*>(aList), _total(n), _count(0) { } protected: bool ProcessItem(Employee* const&); private: 88 int _total; int _count; }; bool PrintNEmployees::ProcessItem (Employee* const& e) { _count++; e->Print(); return _count < _total; } Zde vidím, jak PrintNEmployees vytiskne prvních deset zaměstnancův seznamu: List<Employee*>* employees; PrintNEmployees pa(employees, 10); pa.Traverse(); Srovnání s použitím externího iterátoru: ListIterator<Employee*> i(employees); int count = 0; for (i.First(); !i.IsDone(); i.Next()) { count++; i.CurrentItem()->Print(); if (count >= 10) { break; } } Interní iterátory mohou zapouzdřit různé druhy iterací. Např. FilteringListTraverser zapouzdřuje iteraci, která zpracovává pouze položky splňující test. template class FilteringListTraverser { public: FilteringListTraverser(List* aList); bool Traverse(); protected: virtual bool ProcessItem(const Item&) = 0; virtual bool TestItem(const Item&) = 0; private: ListIterator _iterator; }; Traverse rozhoduje o pokračování procházení na základě výsledku testu: template <class Item> void FilteringListTraverser<Item>::Traverse () { bool result = false; for ( _iterator.First(); !_iterator.IsDone(); _iterator.Next() ) { if (TestItem(_iterator.CurrentItem())) { result = ProcessItem(_iterator.CurrentItem()); if (result == false) { break; } } } return result; } 89 Varianta této třídy může definovat návrat Traverse v případě, že testu vyhovuje minimálně jedna položka. Známá použití Iterátory jsou v objektově orientovaných systémech běžné. Např. u většiny knihoven korekčních tříd. Příbuzné vzory Iterator se často aplikuje na rekurzivní struktury jako jsou Composite. Polymorfní iterátory spoléhají na Factory Method při tvorbě instance příslušné podtřídy třídy Iterator. Iterator může použít Memento k zachycení stavu iterace. Observer – Pozorovatel (objekt) Účel Definuje meziobjektovou závislost „jedna ku n“. Změní-li jeden objekt svůj stav, všechny závislé objekty jsou automaticky upozorněny a zaktualizovány. Jiné názvy Dependents (závislé objekty), Publish – Subscribe (vydávat – odebírat). Motivace Běžným vedlejším účinkem rozdělení systém na kolekci spolupracujících tříd je potřeba udržování důslednosti mezi příbuznými objekty. Důslednost nechceme dosahovat pevným spojením tříd, protože to snižuje schopnost jejich znovupoužití. Mnoho souprav nástrojů pro grafická uživatelská rozhraní např. odděluje prezentační aspekty uživatelského rozhraní od vlastních dat aplikací. Tabulkový objekt i objekt sloupcového grafu mohou popisovat informace stejného objektu aplikačních dat pomocí různých prezentací. Tabulka ani sloupcový graf o sobě nevědí, a tím umožňují znovupoužívat jen potřebný objekt. Pokud uživatel změní informace v tabulce, odrazí se to ihned ve sloupcovém grafu a naopak. Z tohoto chování vyplývá, že tabulka i sloupcový graf závisejí na datovém obejktu, a proto musí být upozorněny na jakoukoli změnu jeho stavu. Vzor Observer popisuje, jak tyto vztahy zřídit. Klíčovými objekty tohoto vzoru jsou subjekt a pozorovatel. Subjekt může mít libovolný počet pozorovatelů. Všichni pozorovatelé jsou upozorněni, 90 kdykoli subjekt absolvuje změnu stavu. Reakcí všech pozorovatelů jež, že dotazují subjekt a synchronizujíc své stavy podle jeho stavu. Použití Vzor Observer používáme v těchto situacích: - abstrakce má dva aspekty a jeden závisí na druhém (jejich zapouzdření do zvláštních objektů je umožňuje nezávisle měnit a znovupoužívat) - změna jednoho objektu vyžaduje změnu jiných a jejich počet neznáme - objekt má upozorňovat ostatní objekty, aniž by o nich cokoli předpokládal Struktura Součásti Subject - zná své pozorovatele (může pozorovat libovolný počet objektů Observer) - poskytuje rozhraní pro připojování a odpojování objektů Observer Observer - definuje aktualizační rozhraní objektů, jež mají být upozorněny na změny subjektu ConcreteSubject - ukládá stav vlivu do objektů ConcreteObserver - posílá upozornění svm pozorovatelům v případě změny svého stavu ConcreteObserver - udržuje odkaz na objekt ConcreteSubject - ukládá stav, který má zůstat konzistentní se stavem subjektu - implementuje aktualizační rozhraní objektu Observer, které udržuje jeho stav konzistentní se stavem subjektu Spolupráce ConcreteSubject upozorňuje své pozorovatele v případě výskytu jakékoli změny, která může zapříčinit nekonzistenci mezi stavem jeho pozorovatelů a jeho vlastním. Po obdržení informace o změně konkrétního subjektu může objekt ConcreteObserver požádat subjekt o další informace. ConcreteObserver tyto informace používá ke sladění svého stavu se stavem subjektu. 91 Důsledky Observer umožňuje měnit nezávisle subjekty a pozorovatele. Subjekty lze znovupoužívat bez znovupoužití jejich pozorovatelů a naopak. Umožňuje přidávat pozorovatele bez úprav subjektu či ostatních pozorovatelů. Dále je to: - abstrakční spojení mezi třídami Subjekt a Observer (subjekt pouze ví to, že má seznam pozorovatelů, kde každý z nich splňuje jednoduché rozhraní abstraktní třídy Observer) - podpora pro vysílací komunikaci (na rozdíl od obyčejné žádosti nemusí upozornění, které subjekt posílá, specifikovat svého příjemce) - neočekávané aktualizace (pozorovatelé se nemusí starat o práci potřebnou pro změnu objektu) Implementace Mapování subjektů a jejich pozorovatelů. Pro subjekt nejjednodušší způsobe pro sledování pozorovatelů, které má upozorňovat, je explicitně si ukládat jejich odkazy. Pozorování více subjektů. Aby pozorovatel závisel na více subjektech, je nutné rozšířit rozhraní Update tak, aby pozorovatel poznal subjekt posílající upozornění. Kdo spustí aktualizaci? Buď se klienti učiní zodpovědní za volání funkce Notify ve správnou dobu, nebo mít operace nastavující stav objektu Subjekt, které zavolají Notify poté, co změní stav subjektu. Uvolněné odkazy na odstraněné subjekty. Je možné, aby subjekt informoval o svém odstranění. Zajištění, aby byl stav objektu Subjekt konzistentní před upozorněním. Je důležité, aby byl stav objektu Subjekt konzistentní před voláním funkce Notify, neboť pozorovatelé při aktualizaci vlastního stavu dotazují subjekt na jeho aktuální stav. Prevence aktualizačních protokolů pro určité pozorovatele: modely poslání a stažení. Model poslání (subjekt posílá pozorovatelům podrobné informace o změně bez ohledu na to, zda je chtějí nebo ne), model stažení (subjekt posílá jen minimální upozornění a pozorovatelé pak explicitně žádají o podrobnosti). Explicitní určování zájmových modifikací. Aktualizační účinnost lze zlepšit rozšířením registračního rozhraní objektu, které umožní registraci pozorovatelů jen pro určité zájmové události. Zapouzdření komplexní aktualizační sémantiky. Je-li vztah závislosti mezi subjekty a pozorovateli zvláště komplexní, může být nezbytné vytvořit objekt, který tyto vztahy udržuje – ChangeManager (minimalizuje práci, které je zapotřebí k tomu, aby pozorovatelé reflektovali změnu jejich subjektu). ChangeManager má tři povinnosti: 92 - mapovat subjekt na své pozorovatele a poskytovat rozhraní k udržování tohoto mapování (tím se eliminuje potřeba subjektů, aby udržovaly odkazy na své pozorovatele a naopak) - definovat určitou aktualizační strategii - na žádost subjektu aktualizovat všechny závislé pozorovatele Následující diagram popisuje jednoduchou implementaci vzoru Observer na základě objektu ChangeManager. Objekt SimpleChangeManager je naivní v tom, že vždy aktualizuje všechny pozorovatele pro každý subjekt. DAGChangeManager zpracovává řízené acyklické grafy závislostí mezi subjekty a jejich pozorovateli (je vhodnější, pokud pozorovatel pozoruje více subjektů). Zkombinování tříd Subjekt a Observer. To dovoluje definovat objekt, který zároveň jedná jako subjekt i pozorovatel bez několikanásobné dědičnosti. Příklad Abstraktní třída definuje rozhraní Observer: class Subject; class Observer { public: virtual ~ Observer(); virtual void Update(Subject* theChangedSubject) = 0; protected: Observer(); }; Tato implementace podporuje několik subjektů na každého pozorovatele. Subject předaný operaci Update umožňuj pozorovateli zjistit, který subjekt se změnil, pozoruje-li jich více. Podobně definuje abstraktní třída rozhraní Subject: class Subject { public: virtual ~Subject(); virtual void Attach(Observer*); virtual void Detach(Observer*); virtual void Notify(); protected: Subject(); 93 private: List *_observers; }; void Subject::Attach (Observer* o) { _observers->Append(o); } void Subject::Detach (Observer* o) { _observers->Remove(o); } void Subject::Notify () { ListIterator i(_observers); for (i.First(); !i.IsDone(); i.Next()) { i.CurrentItem()->Update(this); } } ClockTimer je konkrétní subjekt pro ukládání a udržování denního času. ClockTimer poskytuje rozhraní pro získání jednotlivých časových jednotek. class ClockTimer : public Subject { public: ClockTimer(); virtual int GetHour(); virtual int GetMinute(); virtual int GetSecond(); void Tick(); }; Operace Tick je volána vnitřní časomírou v pravidelných intervalech a poskytuje přesnou časovou bázi. Tick aktualizuje vnitřní stav subjektu ClockTimer a volá operaci Notify, aby informoval pozorovatele o změně: void ClockTimer::Tick () { // aktualizovat vnitrni stav udrzujici cas Notify(); } Nyní lze definovat třídu DigitalClock, která zobrazuje čas. class DigitalClock: public Widget, public Observer { public: DigitalClock(ClockTimer*); virtual ~DigitalClock(); virtual void Update(Subject*); // prekryva operaci tridy observer virtual void Draw(); // prekryva operaci tridy widget; // definuje, jak vykreslit digitalni hodiny private: ClockTimer* _subject; }; DigitalClock::DigitalClock (ClockTimer* s) { _subject = s; _subject->Attach(this); } DigitalClock:: DigitalClock () { _subject->Detach(this); } Před vykreslení podoby hodin operace Update zkontroluje a zajistí, že je upozorňovaný subjekt skutečně subjektem hodin: 94 void DigitalClock::Update (Subject* theChangedSubject) { if (theChangedSubject == _subject) { Draw(); } } void DigitalClock::Draw () { // ziskat od subjektu nove hodnoty int hour = _subject->GetHour(); int minute = _subject->GetMinute(); // atd. // vykreslit digitalni hodiny } Třídu AnalogClock lze definovat stejným způsobem. class AnalogClock : public Widget, public Observer { public: AnalogClock(ClockTimer*); virtual void Update(Subject*); virtual void Draw(); }; Následující text vytváří AnalogClock a DigitalClock, vždy zobrazující shodný čas: ClockTimer* timer = new ClockTimer; AnalogClock* analogClock = new AnalogClock(timer); DigitalClock* digitalClock = new DigitalClock(timer); Kdykoliv timer tikne, oboje hodiny se zaktualizují a příslušně se samy vykreslí. Příbuzné vzory ChangeManager se chová jako Mediator mezi subjektem a pozorovateli tím, že zapouzdřuje komplexní aktualizační sémantiku. ChangeManager může použít vzor Singleton, aby zajistil jedinečnost a globální přístupnost. Strategy – Strategie (objekt) Účel Definuje řadu algoritmů, každý zapouzdřuje a umožňuje jejich zaměnitelnost. Strategy dovoluje algoritmu, aby se měnil nezávisle na klientech, kteří jej používají. Jiné názvy Policy (zásady). Motivace Pro zlom souvislého textu do řádků existuje mnoho různých algoritmů. Není vhodné pevné programování všech těchto algoritmů do tříd, jež je potřebují, a to z několika důvodů: - klienti s potřebou zlomu textu do řádků jsou mnohem komplikovanější, obsahují-li také příslušný zdrojový text - pro různé situace se hodí různé algoritmy. Nechceme však podporovat více zlamovacích algoritmů, než kolik skutečně potřebujeme - pokud zlom představuje integrální součást klienta, je přidávání nových algoritmů a upravování těch existujících obtížné Těmto problémům se můžeme vyhnout definováním tříd, které zapouzdřují různé zalamovací algoritmy. Algoritmus, který je takto zapouzdřen, se nazývá strategií. 95 Předpokládejme, že je třída Composition odpovědná za údržbu a aktualizaci řádkových zlomů v textu, který se zobrazuje v prohlížeči. Strategie pro zalamování řádků nejsou implementovány třídou Composition. Místo toho se implementují odděleně pomocí podtříd abstraktní třídy Compositor. Podtřídy třídy Compositor implementují různé strategie: - SimpleCompositor implementuje jednoduchou strategii, která určuje zlomy jednotlivě - TeXCompositor implementuje k vyhledávání algoritmus TeX - ArrayCompositor implementuje strategii, která vybírá zlomy tak, aby měl každý řádek pevný počet položek Composition udržuje odkaz na objekt Compositor. Kdykoliv Composition přeformátuje text, předá tuto odpovědnost objektu Compositor. Klient třídy Composition specifikuje, který Compositor se má použít, nainstalováním požadovaného Compositor do Composition. Použití Vzor Strategy použijeme v těchto případech: - mnoho příbuzných tříd se odlišuje pouze svým chováním (strategie poskytují způsob, jak třídu nakonfigurovat jedním z mnoha druhů chování) - potřebujeme různé varianty algoritmu - algoritmus používá data, která by klienti neměli znát - třída definuje mnoho druhů chování, jež se v operacích jeví jako vícenásobné podmíněné příkazy Struktura Součásti Strategy (Compositor) - deklaruje rozhraní společné všem podporovaným algoritmům ConcreteStrategy (SimpleCompositor, TeXCompositor, ArrayCompositor) - implementuje algoritmus pomocí rozhraní Strategy 96 Context (Composition) - je konfigurován pomocí objektu ConcreteStrategy - udržuje odkaz na objekt Strategy - může definovat rozhraní, pomocí kterého Strategy získá přístup ke svým datům Spolupráce Aby implementovaly vybraný algoritmus, Strategy a Context spolu interagují. Jestliže se algoritmus zavolá, kontext může předat strategii všechna data, která jsou pro algoritmus potřebná. Další možnost je, že se kontext sám předá operacím Strategy jako argument. To dovoluje strategii, aby se vracela ke kontextu podle potřeby. Context předává žádosti klientů své strategii. Klienti zpravidla vytvoří a předají objekt ConcreteStrategy kontextu. Posléze klienti interagují výhradně s kontextem. Klient si často může vybrat z celé řady tříd ConcreteStrategy. Důsledky Vzor Strategy má tyto výhody a nevýhody: - řady příbuzných algoritmů (dědičnost pomáhá vyčlenit společné funkce z algoritmů) - alternativa tvorby podtříd (by se třída Context mohla různě chovat, můžeme přímo vytvořit její podtřídy) - strategie eliminují podmíněné příkazy - volba implementací (mohou dodávat různé implementace stejného chování) - klienti musí vědět o různých třídách Strategy (musí chápat jejich rozdíly, než si vybere tu nejlepší) - komunikační režie mezi třídami Strategy a Context - zvýšený počet objektů (to lze řešit někdy redukcí implementování strategie jako bezstavových objektů) Implementace Definice rozhraní Strategy a Context. Rozhraní Strategy a Context musí dávat třídě ConcreteStrategy účinný přístup ke všem údajům, jež od kontextu potřebuje a naopak. Buď Context může předávat data operacím Strategy pomocí parametrů, nebo se kontext sám předá jako argument a strategie si data explicitně vyžádá od kontextu. Strategie jako parametry šablon. V jazyce C++ lze používat šablony ke konfiguraci třídy pomocí strategie. Tuto techniku lze používat, pokud jsou splněny dvě podmínky: třídu Strategy lze vybrat při kompilaci a za běhu se nemusí měnit. V tomto případě se konfigurovaná třída (např. Context) definuje jako šablonová třída, která má za parametr třídu Strategy: template class Context { void Operation() { theStrategy.DoAlgorithm(); } private: AStrategy theStrategy; }; Třída se pak zkonfiguruje pomocí třídy Strategy, když se vytvoří její instance: class MyStrategy { 97 public: void DoAlgorithm(); }; Se šablonami není nutné definovat abstraktní třídu, která definuje rozhraní Strategy. Použití Strategy jako parametru šablony nám také dovoluje staticky svázat Strategy a odpovídající Context, což může vést ke zvýšení efektivity. Objekty Strategy lze učinit volitelnými. Třídu Context lze zjednodušit, dává-li smysl mít objekt Strategy. Context před vlastním přístupem zjišťuje, zda má objekt Strategy. Jestliže existuje, Context jej použije běžným způsobem. V opačném případě Context přejde na výchozí chování. Klienti se pak nemusí zabývat objekty Strategy kromě případu, kdy se jim výchozí chování nelíbí. Příklad Uvedeme si kód pro příklad z oddílu motivace, který je založen na implementaci tříd Composition a Compositor. Třída Composition udržuje kolekci instancí Component, které v dokumentu reprezentují textové a grafické prvky. class Composition { public: Composition(Compositor*); void Repair(); private: Compositor* _compositor; Component* _components; int _componentCount; int _lineWidth; int* _lineBreaks; }; int _lineCount; // // // // // // seznam komponent počet komponent sirka radku pro Composition pozice radkovych zlomu v komponentach počet radku Rozhraní Compositor umožňuje skladbě, aby předala sazeči všechny nutné informace. Např. jak vzít data a dát je strategii: class Compositor { public: virtual int Compose( Coord natural[], Coord stretch[], Coord shrink[], int componentCount, int lineWidth, int breaks[] ) = 0; protected: Compositor(); }; Skladba volá sazeče pomocí operace Repair. Repair nejprve zinicializuje pole pomocí přirozené velikosti, roztažitelnosti a smrštitelnosti všech komponent. Potom zavolá sazeče za účelem získání řádkových zlomů a nakonec podle nich rozvrhne komponenty: void Composition::Repair () { Coord* natural; Coord* stretchability; Coord* shrinkability; int componentCount; int* breaks; // priprava poli s požadovanými velikostmi komponent // ... 98 // urceni poloh radkovych zlomu: int breakCount; breakCount = _compositor->Compose( natural, stretchability, shrinkability, componentCount, _lineWidth, breaks ); // rozvrzeni komponent podle zlomu // ... } SimpleCompositor prohlíží komponenty po jednom řádku a zjišťuje vhodné zlomy: class SimpleCompositor : public Compositor { public: SimpleCompositor(); virtual int Compose( Coord natural[], Coord stretch[], Coord shrink[], int componentCount, int lineWidth, int breaks[] ); // ... }; TeXCompositor používá globálnější strategii. Prohlíží komponenty po odstavci a bere v úvahu velikost a roztažitelnost komponent. Taky se pokouší dát odstavci rovnoměrné „zaplnění“ tím, že minimalizuje bílé znaky mezi komponentami. class TeXCompositor : public Compositor { public: TeXCompositor(); virtual int Compose( Coord natural[], Coord stretch[], Coord shrink[], int componentCount, int lineWidth, int breaks[] ); // ... }; ArrayCompositor zalomí komponenty do řádků v pravidelných intervalech. class ArrayCompositor : public Compositor { public: ArrayCompositor(int interval); virtual int Compose( Coord natural[], Coord stretch[], Coord shrink[], int componentCount, int lineWidth, int breaks[] ); // ... }; Instanci třídy Composition vytvoříme tak, že ji předáme požadovanému sazeči: Composition* quick = new Composition(new SimpleCompositor); Composition* slick = new Composition(new TeXCompositor); Composition* iconic = new Composition(new ArrayCompositor(100)); Rozhraní Compositor je pečlivě navržené, aby podporovalo všechny algoritmy rozvržení, které by mohly podtřídy implementovat. Známá použití Strategie k zapouzdření různých algoritmů ke zlomu řádků. Různá schémata pro alokaci registrů a pravidel plánování v sadě instrukcí. Argumenty šablon. 99 Systémy pro návrh integrovaných obvodů. ObjectWindows společnosti Borland používají strategie v dialogových oknech k zajištění zadání planých dat uživatelem. Příbuzné vzory Objekty Strategy jsou často dobrými Flyweights. 100 ZÁVĚR Cílem této bakalářské práce bylo vytvoření studijních podpor pro objektově orientované metody s katalogem návrhových vzorů a s přiloženou multimediální prezentací, která by obohatila zpracovanou bakalářkou práci. U vlastní multimediální aplikace pro studijní podporu byly popsány nejdůležitější ovládací prvky a programové struktury této aplikace. Aplikace nabízí možnosti pro aktualizaci textů i obrázků. Celé téma standardů UML a objektově orientovaných metod bylo shrnuto do nejdůležitějších pojmů s jejich vysvětlením, u některých s názornou grafikou. Shrnutí pojmů umožňuje lepší porozumění obsáhlejší kapitoly s katalogem návrhových vzorů. Pro tuto práci byly vybrány z oblasti návrhových vzorů tvořivých, strukturálních a chování jenom ty nejpoužívanější a nejznámější, které byly rozebrány nejen jako samostatné celky, ale také jako vzájemně se doplňující entity. U každého návrhového vzoru byl zmíněn nejen jeho záměr a struktura, ale také doporučení, které se týkají implementace a příklad konkrétněji rozepisující možný zdrojový kód návrhového vzoru použitého v aplikaci. 101 PŘÍLOHY Přílohou je adresářová struktura na disku CD, která má následující strukturu a obsah: dokumenty –pdf dokumenty s touto prací a obsahem multimediální studijní podpory spustitelný program flash player - multimediální podpora spustitelná souborem main.exe web stránky - multimediální podpora spustitelná souborem index.html zdrojové soubory – všechny zdrojové soubory upravitelné v programu Macromedia Flash LITERATURA Prof. Ing. Ivo Vondrák, CSc.: Objektově orientované metody pro kombinované studium, 1994, 65 stran. Adison Wesley Longman / Jim Arlow, Ila Neustat: UML a unifikovaný proces vývoje aplikací, Computer Press 2003, první vydání. Adison Wesley Longman /Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Návrh programů pomocí vzorů - stavební kameny objektově orientovaných programů, Grada Publishing a.s. 2003, 388 stran, první vydání. Adison Wesley Longman / Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Design Patterns CD - Elements of Reusable Object-Oriented Software, 1994.
Podobné dokumenty
dip.felk.cvut.cz - DCGI - České vysoké učení technické v Praze
Na tomto místě bych chtěla poděkovat všem, kteří mě podporovali během vzniku bakalářské práce. Především bych chtěla poděkovat panu Mgr. Jakubovi Francovi za vedení bakalářské práce, panu Ing. Zdeň...
Elektronické publikování
Rozvrhněte si adresářovou strukturu webu pro přehlednost. Třiďte grafiku a informace do odlišných adresářů.
Nestahujte grafiku z cizích stránek a nepoužívejte je na svých. Snažte si vytvářet vlastn...
ladění player6
Zde narážíme na první omezení - export do SWF animace je nevratný. Proto pokud budete chtít animaci do budoucna
editovat, zachovejte si pracovní dokument (formát FLA)
Pokud jde o přehrávání animací...
Práce s kolekcemi – algoritmy a implementace ve zvoleném
Množiny (rozhraní Set) - každý prvek může být v kontejneru pouze jednou (jinak kontejner
zabrání vložení). Není zde obdoba kontejneru multiset (možnost vícenásobné přítomnosti téhož...
zde - PrPom
pravidlem, že ve větších městech či aglomeracích, je mnoho zařízení na malé ploše, avšak
stačí vyjet za hranice města a narazíte stěží na polikliniku.
Takový stav by nezaručoval stejnou rychlost do...
Panel - GI (pan european link for geographical information)
a kvalitou ivota populace v dané oblasti. Dalím pøíkladem mùe být ohodnocení pøístupnosti rekreaèních
oblastí pro obyvatele urèité oblasti pøed a po výstavbì nové dálnice.