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:

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.
V následujícím kvízu, Kvíz - Adapter, Facade, Proxy, Decorator ve Vzory GOF, si vyzkoušíme nabyté zkušenosti z předchozích lekcí.