Decorator (dekorátor)

Návrhové vzory Decorator (dekorátor)

Někdy potřebujeme třídě nebo skupině tříd přidat další funkcionalitu, ale odvození není vhodné řešení. Takové situace můžou 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 kritériem může být situace, kdy nechceme použít dědičnost - dědičnost je považována za velmi blízké svázání tříd. Například při přidání určité funkcionality by tuto funkcionalitu zdědily všechny třídy, které by byly od naší třídy odvozené. Ne vždy je to chování, které chceme. Připomeňme si pravidlo dědění - odvozená třída musí jít použít ve všech případech, kdy je použita třída základní. To nemusí pro náš případ platit. Použití dekorátoru se v takových situacích zdá jako vhodné řešení.

Praktické použití

Je dost možné, že jste se už s návrhovým vzorem Dekorátor setkali, jen o tom nevíte. 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é kontrolce, 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ý 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 předává původní třídě. To za prvé znamená, že ji 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 měla původní třída rozhraní, ze kterého je odvozena. Pro ukázku si řekneme, jak by vypadala implementace pro vykreslení rámečku pro obrázek a text v GUI aplikaci.

UML navrh

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 obrazovce. 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 a jediné, co často udělá je, že předá řízení původnímu objektu. To je spousta programování. Proto se většinou vytvoří abstraktní třída, která má pouze konstruktor přijímající objekt, a implementované všechny metody, které má i původní třída. Implementace je taková, že se pouze zavolají metody původního objektu. Dekorátory se poté odvodí od této abstraktní třídy a přepíší pouze pro ně adekvátní metody.

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 pokud 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 neodvodit třídu novou. Správný programátor by měl vycítit, kdy je vhodné použít dědičnost a kdy jinou kompozici. Dědičnost je ze všech možností nejtěsnější spojení mezi bázovou a odvozenou třídou. Pokud již s dědičností začneme pracovat a zjistíme, že nám nevyhovuje, budeme už těžko program přepisovat. Hlavním pravidlo zní: všechny odvozené třídy musí být možné bez výjimky použít místo jejich bázových tříd. Pokud toto pravidlo nelze splnit, dědičnost by neměla být použita.


 

  Aktivity (2)

Článek pro vás napsal patrik.valkovic
Avatar
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu.

Jak se ti líbí článek?
Celkem (1 hlasů) :
55555


 


Miniatura
Všechny články v sekci
Návrhové vzory
Miniatura
Následující článek
Flyweight (muší váha)

 

 

Komentáře

Avatar
vodacek
Redaktor
Avatar
vodacek:

ale notáák co ta diakritika...

 
Odpovědět  +6 13. února 14:28
Avatar
patrik.valkovic
Šéfredaktor
Avatar
Odpovídá na vodacek
patrik.valkovic:

Být pomém, tak je to anglicky, ale s tím by zase měla spousta lidí problém. Nevidím tedy na diakritice nic špatného.

Odpovědět  +1 13. února 15:03
Nikdy neumíme dost na to, abychom se nemohli něco nového naučit.
Avatar
David Novák
Tým ITnetwork
Avatar
Odpovědět  +2 13. února 15:12
Chyba je mezi klávesnicí a židlí.
Avatar
vodacek
Redaktor
Avatar
Odpovídá na patrik.valkovic
vodacek:

proč anglicky? může to zůstat česky avšak bez diakritiky

 
Odpovědět 13. února 16:27
Avatar
vodacek
Redaktor
Avatar
 
Odpovědět 13. února 16:27
Avatar
Taskkill
Redaktor
Avatar
 
Odpovědět 13. února 16:42
Avatar
tomisoka
Redaktor
Avatar
Odpovídá na Taskkill
tomisoka:

Pravděpodobně používání diakritiky v kódu...

 
Odpovědět 13. února 16:43
Avatar
Taskkill
Redaktor
Avatar
Odpovídá na tomisoka
Taskkill:

:-) dík, teď vlastně ani nevím co si o tom myslím... Nechám to asi tak ...

 
Odpovědět 13. února 16:55
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 8 zpráv z 8.