Účel
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ř.
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 <
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 <
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 <
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 <
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 <
-
stereotyp <
-
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 <
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 <
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í: