/ janturon.cz / Od kodéra k analytikovi / Adaptér a Dekorátor

Adaptér a Dekorátor

Adaptér

přizpůsobuje hotové nebo nekompatibilní třídy libovolnému rozhraní


Představme si, že by nám někdo dodal následující kód, který by nebylo možné přepsat (třeba proto, že je použita na mnoha jiných místech nebo dodaná v DLL).

// C++ class Code { string content; public: Code() : content() { } void AddLine(string line) { content += line + "\n"; } string Display() { return content; } }; // Javascript function Code() { var content = ""; this.AddLine = function(line) { content+= line + "\n"; } this.Display() { return content; } }

Představme si dále, že máme několik dalších tříd, které jsou systémem využívané přes následující rozhraní

// C++ struct IShowAble { virtual string Show()=0; }; // Javascript function IShowAble() { this.Show = function() { throw "not implemented"; } }

Bylo by fajn, kdybychom mohli dodanou třídu Code využívat stejným způsobem, ale díky tomu, že ji nemůžeme přepsat, to má dvě drobné vady

  1. Neimplementuje naše rozhraní IShowAble
  2. Metoda Show(), kterou toto rozhraní vnucuje, se v Code jmenuje Display()

Tuto nekompatibilitu umožňuje vyřešit návrhový vzor Adaptér. Dělá totéž, co známe z elektronických adaptérů: převádí jeden typ konektoru na jiný, kompatibilní. Postup návrhu adaptéru je následující.

  1. Neveřejně zdědíme nebo agregujeme novou třídu implementující požadované rozhraní
  2. Vytvoříme metody rozhraní
  3. Zveřejníme metody rodiče kromě metod nahrazených rozhraním

Tímto postupem navrhneme třídu CodeAdapted, která bude adaptovat třídu Code do rozhraní IShowAble

// C++ class CodeAdapted : public Code, public IShowAble { using Code::Display; public: string Show() { return Display(); } };

Zde má jazyk C++ větší vyjadřovací schopnosti: CodeAdapted je (už podle jména) specializací rodiče Code, proto je vhodnější než skládání (agregaci) použít dědičnost. Zároveň je vhodné tvořit co nejjednodušší objekty, tedy skrýt metodu Display(), protože je nahrazena metodou rozhraní Show(). Javascript musí použít agregaci:

// Javascript function CodeAdapted(code) { IShowAble.call(this); if(!(code instanceof Code)) throw "code is not Code"; this.Show = function() { return code.Display(); } this.AddLine = function(line) { code.AddLine(line); } };

Ať už dědíme nebo skládáme, metoda Show v rodiči nemusí jen volat jen metodu rodiče: pokud mění její název, parametry nebo návratovou hodnotu, je to Adaptér. Pokud přidává i kód mimo rodiče, je to Dekorátor.

A použití v kódu:

// C++ CodeAdapted* ca = new CodeAdapted(); ca->AddLine("Hello"); ca->AddLine("world!"); printf(ca->Show()); // volá Code::Display() // CodeAdapted je kompatibilní s IShowAble // Javascript var c = new Code(); var ca = new CodeAdapted(c); ca.AddLine("Hello"); ca.AddLine("World!"); alert(ca.Show());

Existují i vícevstupé adaptéry: zde například firma Atlona prodává adaptér rozhraní HDMI, které je poskládáno ze vstupů VGA, 3.5mm audio Jack a USB (pro napájení na pin 18). To lze navrhnout pomocí vícenásobné dědičnosti nebo agregace.

hdmi

Vyrábí se i vícevýstupové adaptéry (např. USB rozbočovač do více standardů). Protože však v softwarovém světě můžeme s objekty pracovat pomocí ukazatelů, můžeme napsat více jednovýstupových adaptérů, aniž bychom spotřebovali více zdrojů a aniž bychom vytvářeli příliš složité konstrukce. Vícevýstupové adaptéry jsou zde tedy antivzor.

Dekorátor

doplňuje kód do metody již existující třídy


Díky Adaptéru dokážeme použít nekompatibilní třídu Code v našem systému. Pokud bychom chtěli k výstupu její metody Show něco dodat, je osvědčené udělat to návrhovým vzorem Dekorátor, což je následující postup.

  1. Vytvoříme novou třídu implementující totéž rozhraní, co původní třída.
  2. Její konstruktor bude očekávat objekt téhož rozhraní.
  3. Třída implementuje vlastní metodu, ve které využije metodu objektu dodaném v konstruktoru.

Použijeme vzor Dekorátor, aby metoda Show() třídy CodeBase() (adaptované v příkladu na Adaptér) vypisovala také čísla řádků.

// C++ class CodeLines : public IShowAble { IShowAble* code; public: CodeLines(IShowAble* code) { this->code = code; } string Show() { istringstream iss(code->Show()); stringstream ss; string line; for(int i=1; getline(iss,line,'\n'); i++) { ss << i << ' ' << line << '\n'; } return ss.str(); } }; // Javascript function CodeLines(code) { IShowAble.call(this); this.Show = function() { var data = code.Show().split("\n"); var result = ""; for(var i=0; i<data.length; i++) result+= (i+1)+" "+data[i]+"\n"; return result; } }

Tímto způsobem můžeme vytvořit i jiné Dekorátory, které lze díky rozhraní libovolně kombinovat. Tato libovolná kombinace (určitelná až za běhu) je důvod, proč nepoužít dědičnost. (Pokud by návrh vylučoval tyto kombinace, je dědičnost na místě.) Vytvořme tedy ještě jeden dekorátor, který za každý řádek přidá <br/>, aby šel kód zobrazit v prohlížeči

// C++ class CodeBr : public IShowAble { IShowAble* code; void replaceAll(string& str, const string& from, const string& to) { if(from.empty()) return; for(int i=0; (i=str.find(from,i))!=string::npos; i+= to.length()) { str.replace(i, from.length(), to); } } public: CodeBr(IShowAble* code) { this->code = code; } string Show() { string s = code->Show(); replaceAll(s,"\n","<br/>\n"); return s; } }; // Javascript function CodeBr(code) { IShowAble.call(this); this.Show = function() { return code.Show().replace(/\n/g,"
\n"); } }

Nyní už můžeme okusit výhody pohodlného zacházení s kódem. Všimněte si, že dekorátory lze upravovat původní třídu mnoha kombinovanými způsoby, aniž bychom do ní museli tvořit alternativy původní metody Show(). To umožňuje udržovat přehledný kód i v rozsáhlých systémech.

// C++ int main() { CodeAdapted* c = new CodeAdapted(); c->AddLine("Hello"); c->AddLine("world!"); IShowAble* cl = new CodeLines(c); IShowAble* clb = new CodeBr(cl); cout << clb->Show(); /* // alternativní dynamická kombinace IShowAble* cb = new CodeBr(c); IShowAble* cbl = new CodeLines(cb); cout << cbl->Show(); */ return 0; } // výstup (pro clb i cbl) 1 Hello<br/> 2 world!<br/> // Javascript var c = new Code(); var ca = new CodeAdapted(c); ca.AddLine("Hello"); ca.AddLine("World!"); var cal = new CodeLines(ca); var calb = new CodeBr(cal); alert(calb.Show()); /* // alternativní dynamická kombinace var cab = new CodeBr(ca); var cabl = new CodeLines(cab); alert(cabl.Show()); */

Dodatek k C++

V Javascriptu by tímto mohl příběh o Dekorátoru skončit, o zbytek se postará interpret. C++ by ale ten zbytek ponechává úvaze programátora: máme zde alokované objekty c, cl, cb, clb a cbl, které můžeme po úvaze uvolnit. Nejčastěji ale bývá závislost prostá lineární, kdy jeden objekt je dekorován pouze jednou (nebýt zakomentování v kódu, nebyl by to tento případ!). Potom můžeme zodpovědnost za správu paměti dekorovaného objektu přenést do Dekorátoru. Zařídí se to takto:

  1. v rozhraní deklarujeme virtuální destruktor umožňující volat při uvolňování paměti destruktor zděděné třídy:
struct IShowAble { virtual string Show()=0; virtual ~IshowAble() { } };
  1. v destruktoru Dekorátoru uvolníme dekorovanou třídu:
class CodeLines : public IShowAble { IShowAble* code; public: CodeLines(IShowAble* code) { this->code = code; } ~CodeLines() { delete code; } string Show() { ... }
  1. v kódu pak uvolnění Dekorátoru uvolní i všechny dekorované třídy
int main() { CodeAdapted* ca = new CodeAdapted(); ca->AddLine("Hello"); ca->AddLine("world!"); IShowAble* c = ca; c = new CodeLines(c); // dekorování c c = new CodeBr(c); // druhé dekorování c delete c; // uvolní oba dekorátory i dekorovaný objekt return 0;