Lekce 8 - Decorator (dekorátor)
V minulé lekci, Proxy (zástupce), jsme si ukázali návrhový vzor
Proxy
(nebo také zástupce), který se používá pro řízení
přístupu uživatelů k objektu, popřípadě k rozšíření nebo k
zefektivnění práce s tímto objektem.
Někdy potřebujeme třídě nebo skupině tříd přidat další funkcionalitu, ale zdědění není vhodné řešení. Takové situace mohou nastat například při použití knihoven třetích stran, které používají zapečetěné třídy. Zároveň nemusíme znát vnitřní implementaci třídy, od které odvozujeme. Posledním příkladem může být situace, kdy nechceme použít dědičnost - dědičnost je považována za velmi těsné svázání tříd. Ne vždy je to chování, které požadujeme. Připomeňme si pravidlo dědičnosti - odvozená třída musí jít použít ve všech případech, kdy je použita třída základní. Pokud nevhodnou dědičností toto pravidlo porušíme, porušujeme také OOP principy. Použití dekorátoru je v takových situacích vhodné řešení.
Praktické použití
Je dost možné, že jste se už s návrhovým vzorem Dekorátor setkali, ale nevěděli jste o tom. Tradičně se používá v GUI aplikacích - například posuvníky na krajích obrazovky. Posouvat můžeme cokoliv - obrázek, text nebo webovou stránku. Kdybychom měli tuto funkcionalitu implementovat každému ovládacímu prvku, prakticky bychom tím porušovali princip objektově orientovaného programování, protože by byl naprosto stejný kód na různých místech aplikace. Místo toho vytvoříme dekorátor, který ovládací prvek obalí, vykreslí posuvníky a přiřadí jim funkcionalitu. Zbytek akcí delegujeme na původní třídu. Podobně můžeme k libovolnému elementu přidat rámeček. Dekorátor se postará pouze o vykreslení rámečku a zbytek funkcionality deleguje na původní třídu.
Druhý případ může být pro práce se vstupem a výstupem. Při ukládání do souboru budeme chtít zřejmě použít nějaký cachovací systém, pro odesílání dat přes internet budeme možná muset text rozložit na jednotlivé segmenty, pro ukládání dat do databáze si budeme muset vytvořit připojení. Implementovat každou z těchto funkcionalit do samostatné třídy by nebylo optimální. Opět si pomůžeme dekorátorem, který na sebe převezme zodpovědnost pouze za specifický úkol (připojení k databázi, cachování), a o zbytek se postará původní třída.
Implementace
Jak jsem již zmínil, dekorátor velkou část funkcionality deleguje na původní třídu. To za prvé znamená, že její instanci musí někde získat (nejčastěji v konstruktoru), a za druhé musí dodržet stejné rozhraní, jaké má původní třída. To dovoluje rozšířit funkcionalitu, aniž by se v programu cokoliv měnilo. Tato myšlenka je jedním z pilířů objektově orientovaného programování a říká, že bychom měli programovat proti rozhraní, ne proti implementaci. Tento přístup zároveň vyžaduje, aby původní třída (ta, kterou dekorujeme) implementovala rozhraní. Pro ukázku si řekneme, jak by vypadala implementace pro vykreslení rámečku pro obrázek a text v GUI aplikaci.

Třída Rámeček
je dekorátor pro rozhraní
IVykreslitelný
. Objekt příjme v konstruktoru a obalí jeho
metodu Vykresli()
. Parametr místo
je pouze informace
o tom, kam se má obrázek nebo text vykreslit. Všimněme si, že sám
Rámeček
implementuje rozhraní IVykreslitelný
.
Představme si, že máme další dekorátor, který přidává posuvníky na okraj obrazovky. Při současném návrhu nám nic nebrání vytvořit rámeček, ve kterém budou posuvníky, které budou posouvat text nebo obrázek. Jde jen o správné zanoření. V následujícím kódu takovou situaci prakticky implementujeme. Nejprve si vytvoříme rozhraní a základní třídy:
interface IVykreslitelný { void Vykresli(Místo kam); void Kliknutí(); } class Obrázek : IVykreslitelný { private byte[] ZdrojObrázku; public Obrázek(byte[] Zdroj) { this.ZdrojObrázku = Zdroj; } public void Vykresli(Místo kam) { // vykreslení obrázku } public void Kliknutí() { PřiblíženíObrázku(); } } class Text : IVykreslitelný { private string TextKVykreslení; public Text(string Text) { this.TextKVykreslení = Text; } public void Vykresli(Místo kam) { // vykreslení textu } public void Kliknutí() { OznačeníTextu(); } }
Metoda Vykresli()
se zavolá při zobrazování prvku na
obrazovku. Metoda Kliknutí()
po kliknutí myší na prvku. Nyní
se podíváme na naše dekorátory:
class Rámeček : IVykreslitelný { private IVykreslitelný ObalovanýObjekt; public Rámeček(IVykreslitelný Objekt) { this.ObalovanýObjekt = Objekt; } public void Vykresli(Místo kam) { // vykreslení rámečku kam.ZmenšiMísto(); // odpočítá místo, které zabere rámeček this.ObalovanýObjekt.Vykresli(kam); } public void Kliknutí() { this.ObalovanýObjekt.Kliknutí(); } } class Posuvník : IVykreslitelný { private IVykreslitelný ObalovanýObjekt; public Posuvník(IVykreslitelný Objekt) { this.ObalovanýObjekt = Objekt; } public void Vykresli(Místo kam) { //vykreslení posuvníku kam.OdeberMístoProPosuvník(); this.ObalovanýObjekt.Vykresli(kam); } public void Kliknutí() { if(ByloKliknutoNaPosuvník) this.PosunoutObjekt(); else this.ObalovanýObjekt.Kliknutí(); } }
Povšimněme si, že dekorátor vždy volá metodu původního objektu. To
není pravidlem, ale často se toho využívá. Dále si povšimněme metody
Kliknutí()
ve třídě Rámeček
.
Rámeček
slouží pouze k vykreslení, není určen k interakci,
proto pouze "hloupě" předá řízení původnímu objektu.
Nyní se ještě podíváme, jak by se dekorátor použil v kódu:
IVykreslitelný ObrázekSRámečkem = new Rámeček(new Obrázek(data)); IVykreslitelný TextSPosunovatelnýmRámečkem = new Posuvník(new Rámeček(new Text("Text k vykreslení"))); IVykreslitelný PosunovatelnýText = new Posuvník(new Text("Text k vykreslení"));
Závěr
Určitě jste si všimli hlavní nevýhody tohoto návrhového vzoru - dekorátor musí reimplementovat naprosto všechny metody. Ve výsledku ale dělá pouze jednu činnost - předá řízení vnitřnímu objektu. Kvůli jednoduché funkcionalitě je to spousta programování navíc. Pro typickou aplikaci se tedy zpravidla vytvoří abstraktní třída, která pouze deleguje volání rozhraní na vnitřní objekt. Dekorátor poté podědí z této abstraktní třídy a přepíše pouze adekvátní metody:
abstract class Dekorátor : IVykreslitelný { protected IVykreslitelny ObalovanýObjekt; public Dekorátor(IVykreslitelný Objekt) { this.ObalovanýObjekt = Objekt; } public void Vykresli(Místo kam) { ObalovanýObjekt.Vykresli(kam); } public void Kliknuti() { ObalovanýObjekt.Kliknuti(); } } class Rámeček : Dekorátor { public void Vykresli(Místo kam) { // vykreslení rámečku kam.ZmenšiMísto(); //odpočítá místo, které zabere rámeček ObalovanýObjekt.Vykresli(kam); } }
Můžeme si všimnou jisté podoby s návrhovým vzorem Adapter. Rozdíl je především v tom, že Adapter mění rozhraní původní třídy a odstiňuje tak program od samotného použití této třídy (například protože má nekompatibilní rozhraní). Dekorátor naproti tomu zachovává původní rozhraní a navíc ho rozšiřuje.
Také je otázka, proč nepoužít raději dědičnost a nevytvořit třídu novou, která bude mít další funkcionalitu. Správný programátor by měl vycítit, kdy je vhodné použít dědičnost a kdy jinou jazykovou konstrukci. Dědičnost je ze všech možností nejtěsnější spojení mezi bázovou a odvozenou třídou. Pokud s dědičností začneme pracovat a zjistíme, že nám nevyhovuje, budeme program těžko přepisovat.
V další lekci, Flyweight (muší váha), si ukážeme návrhový vzor Flyweight
(neboli muší váha), který slouží k šetření zdrojů v případech, kdy
potřebujeme vytvořit velký počet instancí jednoho typu.
Komentáře


Zobrazeno 9 zpráv z 9.