Studijní opora pro OOM

Transkript

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

Podobné dokumenty

dip.felk.cvut.cz - DCGI - České vysoké učení technické v Praze

dip.felk.cvut.cz - DCGI - České vysoké učení technické v Praze Na tomto místě bych chtěla poděkovat všem, kteří mě podporovali během vzniku bakalářské práce. Především bych chtěla poděkovat panu Mgr. Jakubovi Francovi za vedení bakalářské práce, panu Ing. Zdeň...

Více

Google Android

Google Android Tlačítko BACK – zrušení aktivity Tlačítko HOME – přesun aktivity do pozadí

Více

Elektronické publikování

Elektronické publikování Rozvrhněte si adresářovou strukturu webu pro přehlednost. Třiďte grafiku a informace do odlišných adresářů. Nestahujte grafiku z cizích stránek a nepoužívejte je na svých. Snažte si vytvářet vlastn...

Více

ladění player6

ladění player6 Zde narážíme na první omezení - export do SWF animace je nevratný. Proto pokud budete chtít animaci do budoucna editovat, zachovejte si pracovní dokument (formát FLA) Pokud jde o přehrávání animací...

Více

Práce s kolekcemi – algoritmy a implementace ve zvoleném

Práce s kolekcemi – algoritmy a implementace ve zvoleném Množiny  (rozhraní  Set) - každý  prvek  může  být  v  kontejneru pouze jednou (jinak kontejner zabrání  vložení).  Není  zde  obdoba  kontejneru  multiset (možnost  vícenásobné  přítomnosti  téhož...

Více

zde - PrPom

zde - PrPom pravidlem, že ve větších městech či aglomeracích, je mnoho zařízení na malé ploše, avšak stačí vyjet za hranice města a narazíte stěží na polikliniku. Takový stav by nezaručoval stejnou rychlost do...

Více

Panel - GI (pan european link for geographical information)

Panel - GI (pan european link for geographical information) a kvalitou života populace v dané oblasti. Dalším pøíkladem mùže být ohodnocení pøístupnosti rekreaèních oblastí pro obyvatele urèité oblasti pøed a po výstavbì nové dálnice.

Více