IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

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.

V tutoriálu Návrhové vzory GoF si představíme návrhový vzor Decorator. Vzor dovoluje přidat funkcionalitu třídě za běhu programu a přitom od ní nedědit.

Motivace pro použití vzoru Decorator

Někdy nechceme použít dědičnost, protože dědičnost představuje velmi těsné svázání tříd. Odvozená třída musí být použitelná 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.

Nebo se můžeme dostat do situace, kdy neznáme vnitřní implementaci třídy, od které odvozujeme. Taková situace může nastat například při použití knihoven třetích stran, které používají zapečetěné třídy. Zapečetěné třídy jsme probírali v lekci Nullovatelné typy, klonování, atributy, destruktor a další.

Použití dekorátoru je v těchto případech vhodné řešení.

Praktické použití vzoru Decorator

Návrhový vzor Dekorátor se tradičně používá v GUI aplikacích. Například pro 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, porušovali bychom tím princip objektově orientovaného programování. Naprosto stejný kód by byl na různých místech aplikace. Místo toho vytvoříme dekorátor, který ovládací prvek obalí. Poté 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.

Použití dekorátoru je také vhodné pro práce se vstupem a výstupem. Při ukládání do souboru chceme 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í). O zbytek se postará původní třída.

Implementace vzoru Decorator

Připomeňme si, že dekorátor velkou část funkcionality deleguje na původní třídu. To za prvé znamená, že dekorátor musí:

  • získat instanci původní třídy (nejčastěji v konstruktoru),
  • dodržet stejné rozhraní, jaké má původní třída.

Dodržení stejného rozhraní 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í.

Praktický příklad

V našem praktickém příkladu si ukážeme implementaci pro vykreslení rámečku pro obrázek a text v GUI aplikaci:

UML návrh implementace vzoru dekorátor - Návrhové vzory GoF

Třída Ramecek je dekorátor pro rozhraní IVykreslitelny. Objekt příjme v konstruktoru a obalí jeho metodu Vykresli(). Parametr misto je pouze informace o tom, kam se má obrázek nebo text vykreslit. Všimněme si, že sám Ramecek implementuje rozhraní IVykreslitelny.

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 můžeme 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.

Tvorba rozhraní a základních tříd

Nejprve si napíšeme rozhraní a základní třídy:

  • interface IVykreslitelny
    {
        void Vykresli(Misto kam);
        void Kliknuti();
    }
    
    class Obrazek : IVykreslitelny
    {
        private byte[] zdrojObrazku;
    
        public Obrazek(byte[] zdroj)
        {
            this.zdrojObrazku = zdroj;
        }
    
        public void Vykresli(Misto kam)
        {
            // vykreslení obrázku
        }
    
        public void Kliknuti()
        {
            PriblizeniObrazku();
        }
    
        private void PriblizeniObrazku()
        {
            // implementace přiblížení obrázku
        }
    }
    
    class Text : IVykreslitelny
    {
        private string textKVykresleni;
    
        public Text(string text)
        {
            this.textKVykresleni = text;
        }
    
        public void Vykresli(Misto kam)
        {
            // vykreslení textu
        }
    
        public void Kliknuti()
        {
            OznaceniTextu();
        }
    
        private void OznaceniTextu()
        {
            // implementace označení textu
        }
    }
  • interface IVykreslitelny {
        void vykresli(Misto kam);
        void kliknuti();
    }
    
    class Obrazek : IVykreslitelny {
        private byte[] zdrojObrazku;
    
        public Obrazek(byte[] zdroj) {
            this.zdrojObrazku = zdroj;
        }
    
        public void vykresli(Misto kam) {
            // vykreslení obrázku
        }
    
        public void kliknuti() {
            priblizeniObrazku();
        }
    }
    
    class Text : IVykreslitelny {
        private String textKVykresleni;
    
        public Text(String text) {
            this.textKVykresleni = text;
        }
    
        public void vykresli(Misto kam) {
            // vykreslení textu
        }
    
        public void kliknuti() {
            oznaceniTextu();
        }
    }
  • interface IVykreslitelny {
        public function vykresli($kam);
        public function kliknuti();
    }
    
    class Obrazek implements IVykreslitelny {
        private $zdrojObrazku;
    
        public function __construct($zdroj) {
            $this->zdrojObrazku = $zdroj;
        }
    
        public function vykresli($kam) {
            // vykreslení obrázku
        }
    
        public function kliknuti() {
            $this->priblizeniObrazku();
        }
    
        private function priblizeniObrazku() {
            // implementace přiblížení obrázku
        }
    }
    
    class Text implements IVykreslitelny {
        private $textKVykresleni;
    
        public function __construct($text) {
            $this->textKVykresleni = $text;
        }
    
        public function vykresli($kam) {
            // vykreslení textu
        }
    
        public function kliknuti() {
            $this->oznaceniTextu();
        }
    
        private function oznaceniTextu() {
            // implementace označení textu
        }
    }
  • class IVykreslitelny {
        vykresli(kam) {}
        kliknuti() {}
    }
    
    class Obrazek extends IVykreslitelny {
        constructor(zdroj) {
            super();
            this.zdrojObrazku = zdroj;
        }
    
        vykresli(kam) {
            // vykreslení obrázku
        }
    
        kliknuti() {
            this.priblizeniObrazku();
        }
    
        priblizeniObrazku() {
            // implementace přiblížení obrázku
        }
    }
    
    class Text extends IVykreslitelny {
        constructor(text) {
            super();
            this.textKVykresleni = text;
        }
    
        vykresli(kam) {
            // vykreslení textu
        }
    
        kliknuti() {
            this.oznaceniTextu();
        }
    
        oznaceniTextu() {
            // implementace označení textu
        }
    }
  • class IVykreslitelny:
        def vykresli(self, kam):
            pass
    
        def kliknuti(self):
            pass
    
    class Obrazek(IVykreslitelny):
        def __init__(self, zdroj):
            super().__init__()
            self.zdrojObrazku = zdroj
    
        def vykresli(self, kam):
            # vykreslení obrázku
            pass
    
        def kliknuti(self):
            self.priblizeni_obrazku()
    
        def priblizeni_obrazku(self):
            # implementace přiblížení obrázku
            pass
    
    class Text(IVykreslitelny):
        def __init__(self, text):
            super().__init__()
            self.textKVykresleni = text
    
        def vykresli(self, kam):
            # vykreslení textu
            pass
    
        def kliknuti(self):
            self.oznaceni_textu()
    
        def oznaceni_textu(self):
            # implementace označení textu
            pass

Metoda Vykresli() se zavolá při zobrazování prvku na obrazovku. Metoda Kliknuti() po kliknutí myší na prvku.

Tvorba dekorátorů

Nyní si vytvoříme dekorátory:

  • class Ramecek : IVykreslitelny
    {
        private IVykreslitelny obalovanyObjekt;
    
        public Rameček(IVykreslitelny objekt)
        {
            this.obalovanyObjekt = objekt;
        }
    
        public void Vykresli(Misto kam)
        {
            // vykreslení rámečku
            kam.ZmensiMisto(); // odpočítá místo, které zabere rámeček
            this.obalovanyObjekt.Vykresli(kam);
        }
    
        public void Kliknuti()
        {
            this.obalovanyObjekt.Kliknuti();
        }
    }
    
    class Posuvnik : IVykreslitelny
    {
        private IVykreslitelny obalovanyObjekt;
        private bool byloKliknutoNaPosuvnik;
    
        public Posuvnik(IVykreslitelny objekt)
        {
            this.obalovanyObjekt = objekt;
        }
    
        public void Vykresli(Misto kam)
        {
            // vykreslení posuvníku
            kam.OdeberMistoProPosuvnik();
            this.obalovanyObjekt.Vykresli(kam);
        }
    
        public void Kliknuti()
        {
            if (byloKliknutoNaPosuvnik)
                this.PosunoutObjekt();
            else
                this.obalovanyObjekt.Kliknuti();
        }
    
        private void PosunoutObjekt()
        {
            // implementace posunu objektu
        }
    }
  • class Ramecek : IVykreslitelny {
        private IVykreslitelny obalovanyObjekt;
    
        public Ramecek(IVykreslitelny objekt) {
            this.obalovanyObjekt = objekt;
        }
    
        public void vykresli(Misto kam) {
            // vykreslení rámečku
            kam.zmensiMisto(); // odpočítá místo, které zabere rámeček
            this.obalovanyObjekt.vykresli(kam);
        }
    
        public void kliknuti() {
            this.obalovanyObjekt.kliknuti();
        }
    }
    
    class Posuvnik : IVykreslitelny {
        private IVykreslitelny obalovanyObjekt;
    
        public Posuvnik(IVykreslitelny objekt) {
            this.obalovanyObjekt = objekt;
            this.byloKliknutoNaPosuvnik = false;
        }
    
        public void vykresli(Misto kam) {
            //vykreslení posuvníku
            kam.odeberMistoProPosuvnik();
            this.obalovanyObjekt.vykresli(kam);
        }
    
        public void kliknuti() {
            if(byloKliknutoNaPosuvnik)
                this.posunoutObjekt();
            else
                this.obalovanyObjekt.kliknuti();
        }
    }
  • class Ramecek implements IVykreslitelny {
        private $obalovanyObjekt;
    
        public function __construct($objekt) {
            $this->obalovanyObjekt = $objekt;
        }
    
        public function vykresli($kam) {
            // vykreslení rámečku
            $kam->zmensiMisto(); // odpočítá místo, které zabere rámeček
            $this->obalovanyObjekt->vykresli($kam);
        }
    
        public function kliknuti() {
            $this->obalovanyObjekt->kliknuti();
        }
    }
    
    class Posuvnik implements IVykreslitelny {
        private $obalovanyObjekt;
        private $byloKliknutoNaPosuvnik;
    
        public function __construct($objekt) {
            $this->obalovanyObjekt = $objekt;
        }
    
        public function vykresli($kam) {
            // vykreslení posuvníku
            $kam->odeberMistoProPosuvnik();
            $this->obalovanyObjekt->vykresli($kam);
        }
    
        public function kliknuti() {
            if ($this->byloKliknutoNaPosuvnik) {
                $this->posunoutObjekt();
            } else {
                $this->obalovanyObjekt->kliknuti();
            }
        }
    
        private function posunoutObjekt() {
            // implementace posunu objektu
        }
    }
  • class Ramecek {
        constructor(objekt) {
            this.obalovanyObjekt = objekt;
        }
    
        vykresli(kam) {
            // vykreslení rámečku
            kam.zmensiMisto(); // odpočítá místo, které zabere rámeček
            this.obalovanyObjekt.vykresli(kam);
        }
    
        kliknuti() {
            this.obalovanyObjekt.kliknuti();
        }
    }
    
    class Posuvnik {
        constructor(objekt) {
            this.obalovanyObjekt = objekt;
            this.byloKliknutoNaPosuvnik = false;
        }
    
        vykresli(kam) {
            // vykreslení posuvníku
            kam.odeberMistoProPosuvnik();
            this.obalovanyObjekt.vykresli(kam);
        }
    
        kliknuti() {
            if (this.byloKliknutoNaPosuvnik) {
                this.posunoutObjekt();
            } else {
                this.obalovanyObjekt.kliknuti();
            }
        }
    
        posunoutObjekt() {
            // implementace posunu objektu
        }
    }
  • class Ramecek:
        def __init__(self, objekt):
            self.obalovanyObjekt = objekt
    
        def vykresli(self, kam):
            # vykreslení rámečku
            kam.zmensiMisto()  # odpočítá místo, které zabere rámeček
            self.obalovanyObjekt.vykresli(kam)
    
        def kliknuti(self):
            self.obalovanyObjekt.kliknuti()
    
    class Posuvnik:
        def __init__(self, objekt):
            self.obalovanyObjekt = objekt
            self.byloKliknutoNaPosuvnik = False
    
        def vykresli(self, kam):
            # vykreslení posuvníku
            kam.odeberMistoProPosuvnik()
            self.obalovanyObjekt.vykresli(kam)
    
        def kliknuti(self):
            if self.byloKliknutoNaPosuvnik:
                self.posunoutObjekt()
            else:
                self.obalovanyObjekt.kliknuti()
    
        def posunout_objekt(self):
            # implementace posunu objektu
            pass

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 Kliknuti() ve třídě Ramecek. Třída Ramecek slouží pouze k vykreslení, není určena k interakci, proto pouze předá řízení původnímu objektu.

Použití Dekorátoru v kódu

Nakonec si ukážeme, jak použijeme dekorátor:

  • IVykreslitelny obrazekSRameckem = new Ramecek(new Obrazek(data));
    IVykreslitelny textSPosunovatelnymRameckem = new Posuvnik(new Ramecek(new Text("Text k vykreslení")));
    IVykreslitelny posunovatelnyText = new Posuvnik(new Text("Text k vykreslení"));
  • IVykreslitelny obrazekSRameckem = new Ramecek(new Obrazek(data));
    IVykreslitelny textSPosunovatelnymRameckem = new Posuvnik(new Ramecek(new Text("Text k vykreslení")));
    IVykreslitelny posunovatelnyText = new Posuvnik(new Text("Text k vykreslení"));
  • $obrazekSRameckem = new Ramecek(new Obrazek($data));
    $textSPosunovatelnymRameckem = new Posuvnik(new Ramecek(new Text("Text k vykreslení")));
    $posunovatelnyText = new Posuvnik(new Text("Text k vykresleni"));
  • const obrazekSRameckem = new Ramecek(new Obrazek(data));
    const textSPosunovatelnymRameckem = new Posuvnik(new Ramecek(new Text("Text k vykreslení")));
    const posunovatelnyText = new Posuvnik(new Text("Text k vykresleni"));
  • obrazekSRameckem = Ramecek(Obrazek(data))
    textSPosunovatelnymRameckem = Posuvnik(Ramecek(Text("Text k vykreslení")))
    posunovatelnyText = Posuvnik(Text("Text k vykreslení"))

Vylepšení použitím abstraktní třídy

Hlavní nevýhodou tohoto návrhového vzoru je nutnost reimplementace všech metod. 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 Dekorator : IVykreslitelny
    {
        protected IVykreslitelny obalovanyObjekt;
    
        public Dekorator(IVykreslitelny objekt)
        {
            this.obalovanyObjekt = objekt;
        }
    
        public void Vykresli(Misto kam)
        {
            obalovanyObjekt.Vykresli(kam);
        }
    
        public void Kliknuti()
        {
            obalovanyObjekt.Kliknuti();
        }
    }
    
    class Ramecek : Dekorator
    {
        public void Vykresli(Misto kam)
        {
            // vykreslení rámečku
            kam.ZmensiMisto(); //odpočítá místo, které zabere rámeček
            obalovanyObjekt.Vykresli(kam);
        }
    }
  • abstract class Dekorator implements IVykreslitelny {
        protected IVykreslitelny obalovanyObjekt;
    
        public Dekorator(IVykreslitelny objekt) {
            this.obalovanyObjekt = objekt;
        }
    
        @Override
        public void vykresli(Misto kam) {
            obalovanyObjekt.vykresli(kam);
        }
    
        @Override
        public void kliknuti() {
            obalovanyObjekt.kliknuti();
        }
    }
    
    class Ramecek extends Dekorator {
        public void vykresli(Misto kam) {
            // vykreslení rámečku
            kam.zmensiMisto(); //odpočítá místo, které zabere rámeček
            obalovanyObjekt.vykresli(kam);
        }
    }
  • abstract class Dekorator implements IVykreslitelny {
        protected $obalovanyObjekt;
    
        public function __construct($objekt) {
            $this->obalovanyObjekt = $objekt;
        }
    
        public function vykresli($kam) {
            $this->obalovanyObjekt->vykresli($kam);
        }
    
        public function kliknuti() {
            $this->obalovanyObjekt->kliknuti();
        }
    }
    
    class Ramecek extends Dekorator {
        public function vykresli($kam) {
            // vykreslení rámečku
            $kam->zmensiMisto(); // odpočítá místo, které zabere rámeček
            $this->obalovanyObjekt->vykresli($kam);
        }
    }
  • class Dekorator {
        constructor(objekt) {
            this.obalovanyObjekt = objekt;
        }
    
        vykresli(kam) {
            this.obalovanyObjekt.vykresli(kam);
        }
    
        kliknuti() {
            this.obalovanyObjekt.kliknuti();
        }
    }
    
    class Ramecek extends Dekorator {
        vykresli(kam) {
            // vykreslení rámečku
            kam.zmensiMisto(); // odpočítá místo, které zabere rámeček
            this.obalovanyObjekt.vykresli(kam);
        }
    }
  • class Dekorator(IVykreslitelny):
        def __init__(self, objekt):
            self.obalovanyObjekt = objekt
    
        def vykresli(self, kam):
            self.obalovanyObjekt.vykresli(kam)
    
        def kliknuti(self):
            self.obalovanyObjekt.kliknuti()
    
    class Ramecek(Dekorator):
        def vykresli(self, kam):
            # vykreslení rámečku
            kam.zmensiMisto()  # odpočítá místo, které zabere rámeček
            self.obalovanyObjekt.vykresli(kam)

Můžeme si všimnout 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 dále tak odstiňuje 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.

Závěr

Závěrem si můžeme položit otázku, proč raději nepoužít dědičnost. A nevytvořit novou třídu, 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, bude se nám program těžko přepisovat.


 

Předchozí článek
Proxy (zástupce)
Všechny články v sekci
Návrhové vzory GoF
Přeskočit článek
(nedoporučujeme)
Kvíz - Adapter, Facade, Proxy, Decorator ve Vzory GOF
Článek pro vás napsal Patrik Valkovič
Avatar
Uživatelské hodnocení:
30 hlasů
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Aktivity