7. 10. 2010, Brno Připravil: David Procházka Návrhové vzory Základy objektově orientovaného návrhu
Design Patterns NV (Design patterns) můžeme s nadsázkou označit za ntu, jak řešit určitý problém nejen při návrhu OO systému. Existuje 23 základních vzorů, od kterých se odvozují další. Vzorem může být jakékoliv řešení problému. NV byly poprvé popsány v knize Gama, Helm, Johnson, Vlissides: Design Patterns - Gang of Four
Jaká je ideální třída: jednoduchá 1. Tvořivé vzory: Jak skrýt implementaci objektů, aby nebylo nutné při jejich změně měnit systém. Jak správně navrhovat třídy, tvořit instance. 2. Strukturální změny: Snahou je zpřehlednit systém. Zabývají se propojením objektů. Jak propojení omezit nebo předejít změnám prop. 3. Vzory chování: Zapouzdřují určité procesy prováděné v systému.
Vzory spolu souvisí
Přepravka (Crate / Messenger) Prvním jednoduchým nástrojem je přepravka. Jedná se o konteiner, který je předáván metodám a ty do něj umisťují data. Výhodou je, že metody si nemusí předávat data navzájem velké množství parametrů, ale jen jednu přepravku. Některé jazyky nepodporují předávání hodnotou i odkazem a pokud je potřeba z metody vracet více hodnot. Navíc vracení více hodnot pomocí odkazů v parametrech je někdy považováno na nevhodné.
Přepravka class Pozice{ public: int x; int y; }; class Rozmer{ public: int sirka; int vyska; };
Přepravka Atributy mohou být veřejné, není dobré "recyklovat" přepravky (rozměr i pozice mají 2 int souřadnice), přepravka může být pro přenos mezi metodami nebo interní pro uchování skupiny hodnot pohromadě (metoda pracující s úkoly si je potřebuje sesbírat, než je zpracuje).
Továrna (Factory) Problém: V průběhu života systému se často stane, že musím změnit konstruktor určité třídy (změní se počet nebo typy parametrů, atp.). Důsledek: Musím procházet celý program, abych našel, kde všude se instance dané třídy vytváří. Řešení: Zapouzdřím vytváření objektů do speciální "továrny". Pokud budu potřebovat instanci dané třídy, pouze zavolám metodu továrny a ta mi instanci vrátí.
Továrna class Shape{ public: // rozhrani pro vsechny potomky virtual void draw() = 0; virtual void erase() = 0; }; // factory je metoda tridy static Shape* factory(string type);
Továrna class Circle : public Shape { public: void draw() { cout << "Circle::draw"; } }; void erase() { cout << "Circle::erase"; } // to stejne pro ctverec
Továrna Shape* Shape::factory(string type){ if (type == "Circle") return new Circle(); else return new Square(); }
Továrna int main () { Shape* ctverecek=shape::factory("square"); ctverecek->draw(); } delete ctverecek; return 0;
Abstraktní továrna (Abstract fac.) V předchozím příkladu byla tovární metoda součástí abstraktní třídy, existují i varianty, kdy je definována v samostatné třídě. Důvodem je možnost vytvářet potomky továren. Problém: Máme hru Doom, chceme aby ve hře byly postavy Voják, Mutant, SuperMutant. Navíc chceme aby pro lepší hratelnost existovali od každé z postav dvě varianty: hloupý a chytrý. Jak zajistit jednoduchou výrobu těchto postav a jak zajistit, aby vždy vznikaly pouze postavy jednoho typu.
Schéma potvor
Abstraktní továrna - vzor class AbstraktniTovarnaNepritele{ public: virtual Vojak* vytvorvojaka() = 0; virtual Mutant* vytvormutanta() = 0; virtual SuperMutant* vytvorsupermut() = 0; };
Abstraktní továrna - implementace class MalaObtiznost : public AbstraktniTovarnaNepritele{ public: Vojak* vytvorvojaka() { return new HloupyVojak() } Mutant* vytvormutanta() { return new HloupyMutant() } SuperMutant* vytvorsupermutant() { return new HloupySuperMutant() } };
Abstraktní továrna - implementace class VelkaObtiznost : public AbstraktniTovarnaNepritele{ public: Vojak* vytvorvojaka() { return new ChytryVojak() } Mutant* vytvormutanta() { return new ChytryMutant() } SuperMutant* vytvorsupermutant() { return new ChytrySuperMutant() } };
Abstraktní továrna - použití class HerniAplikace{ public: void zvolobtiznost(string obtiznost){ if (obtiznost == "mala"){ tovarna = new MalaObtiznost(); } else { tovarna = new VelkaObtiznost(); } } private: AbstraktniTovarnaNepritele* tovarna;...
Co jsme získali Logika vytváření příšer je skryta uvnitř továrny. Továrna sama zajistí, aby se daly vytvořit jen příšery zvoleného typu.
Jedináček (Singelton) Občas je nutné zajistit, aby se od dané třídy vytvořila vždy pouze jedna instace. Např. metoda na práci s tiskárnou, obrazovkou, atp. Pokud více objektů potřebuje s instancí této třídy pracovat, musí sdílet pouze jednou instanci.
Jedináček - struktura class Singleton{ public: static Singleton* vratinstanci(){ if (!uinstance) uinstance = new Singleton; return uinstance; }... metody... private: static Singleton* uinstance; Singleton(){}; Singleton(const Singleton&){}; };
Jedináček - použití Singleton* Singleton::uInstance = 0; int main () { Singleton* instance = Singleton::vratInstanci();... volani metod... return 0; }
Jedináček - použití Skrytím konstruktorů jsme zabránili ručnímu vytváření nových instancí i potenciálnu kopírování existující instance. To je dovoleno jen metodám třídy Singelton. Instance se vytvoří při prvním použití (daní je krátký test). Problém je, jak Singelton zrušit a řada dalších "drobností" (viz Alexandrescu, Moderní programování v C++, str. 151).
Jednostavový objekt (Monostate) Na první pohled podobné Singletonu. Tady ale může vzniknout více objektů používajících stejné metody a atributy. Problém je, že statická metoda nemůže být virtuální. Tím se komplikují změny v kódu.
Jednostavový objekt (Monostate) class MojeTiskarna{ public: static void PridatTiskovouUlohu(); private: static std::queue<tiskovauloha> tiskovafronta; static PortTiskarny porttiskarny;... };
Stav (State) Pokud řešíme ve většině metod série obdobných podmínek, můžeme často na návrh aplikovat vzor stav (State). Stav slouží k rozdělení podmínek na stavy. Např.: if(student == "nový") {...} else {...} stav nový a stav starý
Stav (State) Tyto stavy jsou pak uloženy ve třídě jako její vnořené třídy. Každá metoda vnořené třídy implementuje jen jednu část podmínky = přehledné. Při přidání podmínek se jen vytvoří nové stavy = rozšiřitelné.
Klasická podmínka class Creature { bool isfrog; public: Creature() { isfrog = true; } void greet() { if(isfrog) cout << "Kvaaak!" << endl; else cout << "Nazdar!" << endl; } void kiss() { isfrog = false; } };
Stav (State)
Stav (State) class Creature { private: class State { public: virtual string response() = 0; }; class Frog : public State { public: string response() { return "Kvak!"; } };...
Praktický příklad http://objekty.vse.cz/objekty/vzory-state
Příkaz (Command) Příkaz (Command) je přepravka, ve které objektu předáte pokyn, co má udělat. Smysl: Můžete takto rozšiřovat funkčnost již existujících objektů. Stačí aby nové příkazy splňovaly definované rozhraní. Princip: příkaz zapouzdříme do objektu a předáme ho objektu na zpracování.
Příkaz (Command)
Příkaz (Command) class Prikaz{ public: virtual void proved()=0; }; class Privitani:public Prikaz{ void proved(){ cout << "Dobry vecer..." << endl; } };...
Příkaz (Command) class Vecernicek{ private: Prikaz* prikaz; public: void ulozprikaz(prikaz* novyprikaz){ prikaz = novyprikaz; } void provedprikaz(){ prikaz->proved(); } };
Příkaz (Command) Vecernicek* vecernik = new Vecernicek; Privitani* uvod = new Privitani; Rozlouceni* zaver = new Rozlouceni; vecernik->ulozprikaz(uvod); vecernik->provedprikaz(); vecernik->ulozprikaz(zaver); vecernik->provedprikaz();
Zástupce (Proxy) Zástupce (Proxy) slouží jako prostředník pro přístup k různým implementacím určitého rozhraní. Odstiňuje uživatele od objektu a sám řídí přístup. Zástupců může být řada typů Vzdálený zástupce: zastupuje objekt umístěný jinde (často i na jiném počítači). Virtualní zás.: odloží vytvoření objektu na dobu až je to potřeba, do té doby použije náhradu. Ochranný zás.: blokuje přístup k některým met. Chytrý odkaz: rozšiřuje funkcionalitu.
Zástupce (Proxy) class Rozhranni{ public: virtual void metoda1()=0; virtual void metoda2()=0; virtual void metoda3()=0; };
Zástupce (Proxy) class Implementace : public Rozhranni{ public: void metoda1(){ cout << "Implementace met1" << endl; } void metoda2(){ cout << "Implementace met2" << endl; }... };
Zástupce (Proxy) class Zastupce : public Rozhranni{ private: Rozhranni* impl; public: Zastupce(){impl = new Implementace; } ~Zastupce(){ delete(impl); } // rozhranni void metoda1(){ impl->metoda1(); } void metoda2(){ impl->metoda2(); } void metoda3(){ impl->metoda3(); } };
Adaptér Je velmi podobný vzoru Proxy. Účelem adaptéru je že vytvoří převodník pro metody z jiným rozhraním. Používá se v případě, že třída má jiné rozhraní, než bychom potřebovali, např. při přístupu k již hotovým knihovnám, atp. Existuje řada různých implementací adaptéru.
Adaptér
Řetěz (Chain) Celý název je Řetěz zodpovědnosti (Chain of responsibility). Podstata: Máme požadavek a snažíme se ho uspokojit. Existuje řada strategií (metod), které ho mohou uspokojit. Strategie zkoušíme za běhu postupně aplikovat, až nalezneme správnou nebo je všechny vyzkoušíme. Obvykle se pro vyřízení požadavku volá pouze jedna funkce, v tomto případě se o to pokouší postupně mnoho funkcí (expertní systémy).
Řetěz (Chain) http://objekty.vse.cz/objekty/vzory-chain
Další informace Rudolf Pecinovský: Návrhové vzory (Computer Press) Bruce Eckel: Myslíme v jazyce C++, díl 2 (Grada, Knihovna zkušeného prog.) Bruce Eckel: Thinking in C++, vol 2 (free, on-line) http://www.mindviewinc.com/books/ Bruce Eckel: Thinking in Patterns (free, on-line) A. Alexandrescu: Moderní programování v C++ (Computer Press)