Decorator (dekorátor)
Niekedy potrebujeme triede alebo skupine tried pridať ďalšiu funkcionalitu, ale zdedenie nie je vhodné riešenie. Takéto situácie môžu nastať napríklad pri použití knižníc tretích strán, ktoré používajú zapečatené triedy. Zároveň nemusíme poznať vnútorné implementáciu triedy, od ktorej odvodzujeme. Posledným príkladom môže byť situácia, keď nechceme použiť dedičnosť - dedičnosť je považovaná za veľmi tesné zviazanie tried. Nie vždy je to správanie, ktoré požadujeme. Pripomeňme si pravidlo dedičnosti - odvodená trieda musí ísť použiť vo všetkých prípadoch, keď je použitá trieda základnej. Ak nevhodnou dedičnosťou toto pravidlo porušíme, porušujeme tiež OOP princípy. Použitie dekorátoru je v takýchto situáciách vhodné riešenie.
Praktické použitie
Je dosť možné, že ste sa už s návrhom vzorom Dekorátor stretli, ale nevedeli ste o tom. Tradične sa používa v GUI aplikáciách - napríklad posuvníky na krajoch obrazovky. Posúvať môžeme čokoľvek - obrázok, text alebo webovú stránku. Keby sme mali túto funkcionalitu implementovať každému ovládaciemu prvku, prakticky by sme tým porušovali princíp objektovo orientovaného programovania, pretože by bol úplne rovnaký kód na rôznych miestach aplikácie. Namiesto toho vytvoríme dekorátor, ktorý ovládací prvok obalí, vykreslí posuvníky a priradí im funkcionalitu. Zvyšok akcií delegujú na pôvodnú triedu. Podobne môžeme k ľubovoľnému elementu pridať rámček. Dekorátor sa postará iba o vykreslenie rámčeku a zvyšok funkcionality deleguje na pôvodnú triedu.
Druhý prípad môže byť pre práce so vstupom a výstupom. Pri ukladaní do súboru budeme chcieť zrejme použiť nejaký cachovací systém, pre odosielanie dát cez internet budeme možno musieť text rozložiť na jednotlivé segmenty, pre ukladanie dát do databázy si budeme musieť vytvoriť pripojenie. Implementovať každú z týchto funkcionalít do samostatnej triedy by nebolo optimálne. Opäť si pomôžeme dekorátorem, ktorý na seba prevezme zodpovednosť iba za špecifický úlohu (pripojenie k databáze, cachovanie), ao zvyšok sa postará pôvodnej trieda.
Implementácia
Ako som už spomenul, dekorátor veľkú časť funkcionality deleguje na pôvodnú triedu. To za prvé znamená, že jej inštanciu musí niekde získať (najčastejšie v konstruktoru), a za druhé musí dodržať rovnaké rozhranie, aké má pôvodný trieda. To dovoľuje rozšíriť funkcionalitu, bez toho aby sa v programe čokoľvek menilo. Táto myšlienka je jedným z pilierov objektovo orientovaného programovania a hovorí, že by sme mali programovať proti rozhrania, nie proti implementáciu. Tento prístup zároveň vyžaduje, aby pôvodný trieda (tá, ktorú dekorujeme) implementovala rozhranie. Pre ukážku si povieme, ako by vyzerala implementácie pre vykreslenie rámčeku pre obrázok a text v GUI aplikácii.
Trieda Rámeček
je dekorátor pre rozhranie
IVykreslitelný
. Objekt príjme v konstruktoru a obalí jeho
metódu Vykresli()
. Parameter místo
je len
informácia o tom, kam sa má obrázok alebo text vykresliť. Všimnime si, že
sám Rámeček
implementuje rozhranie
IVykreslitelný
.
Predstavme si, že máme ďalšiu dekorátor, ktorý pridáva posuvníky na okraj obrazovky. Pri súčasnom návrhu nám nič nebráni vytvoriť rámček, v ktorom budú posuvníky, ktoré budú posúvať text alebo obrázok. Ide len o správnej zanorenia. V nasledujúcom kóde takú situáciu prakticky implementujeme. Najprv si vytvoríme rozhrania a základné triedy:
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(); } }
Metóda Vykresli()
sa zavolá pri zobrazovaní prvku na
obrazovku. Metóda Kliknutí()
po kliknutí myšou na prvku. Teraz
sa pozrieme 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í(); } }
Všimnime si, že dekorátor vždy volá metódu pôvodného objektu. To nie
je pravidlom, ale často sa to využíva. Ďalej si všimnime metódy
Kliknutí()
v triede Rámeček
. Rámeček
slúži iba na vykreslenie, nie je určený k interakcii, preto iba "hlúpo"
odovzdá riadenie pôvodnému objektu.
Teraz sa ešte pozrieme, ako by sa dekorátor použil v kóde:
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áver
Určite ste si všimli hlavné nevýhody tohto návrhového vzoru - dekorátor musia reimplementovať úplne všetky metódy. Vo výsledku ale robí iba jednu činnosť - odovzdá riadenie vnútornému objektu. Kvôli jednoduché funkcionalite je to veľa programovania navyše. Pre typickú aplikáciu sa teda spravidla vytvorí abstraktné trieda, ktorá iba deleguje volanie rozhrania na vnútornej objekt. Dekorátor potom zdedí z tejto abstraktné triedy a prepíše iba adekvátny metódy:
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šimnú isté podoby s návrhovým vzorom Adapter. Rozdiel je predovšetkým v tom, že Adapter mení rozhrania pôvodnej triedy a odtieňuje tak program od samotného použitia tejto triedy (napríklad pretože má nekompatibilný rozhranie). Dekorátor naproti tomu zachováva pôvodné rozhranie a navyše ho rozširuje.
Tiež je otázka, prečo nepoužiť radšej dedičnosť a nevytvoriť triedu novú, ktorá bude mať ďalšiu funkcionalitu. Správny programátor by mal vycítiť, kedy je vhodné použiť dedičnosť a kedy inú jazykovú konštrukciu. Dedičnosť je zo všetkých možností najtesnejší spojenie medzi bázovú a odvodenú triedou. Pokiaľ s dedičnosťou začneme pracovať a zistíme, že nám nevyhovuje, budeme program ťažko prepisovať.