× Aktuálně z oboru

Vychází Game Ready ovladače pro Far Cry 5 [ clanek/2018040603-vychazi-game-ready-ovladace-pro-far-cry-5/ ]
Celá zprávička [ clanek/2018040603-vychazi-game-ready-ovladace-pro-far-cry-5/ ]

Návrhový vzor Factory v C++

[ http://programujte.com/profil/6101-zdenko-vrabel/ ]Google [ ?rel=author ], [ http://programujte.com/profil/2893-petr-lepcio/ ]Google [ ?rel=author ]       [ http://programujte.com/profil/118-zdenek-lehocky/ ]Google [ ?rel=author ]       19. 12. 2007       26 416×

V poslední době, hlavně s rozmachem objektových jazyků jako JAVA, se čím dál tím víc začínají používat slova jako návrhové vzory. V tomto článku vám chci představit jednu z možných implementací návrhového vzoru Factory. Nepůjde však o žádný osvědčený kód, který byl publikován v knihách nebo na Internetu, ale o vysvětlení toho, co Factory je a o trochu odlišnější formy implementace.

Nezačneme hned nudnou definicí, která asi začátečníkům nic neřekne, spíše na to půjdeme způsobem jeho použití. Chci upozornit na fakt, že jde o ilustrační příklad a z pohledu návrhu jsem se snažil zaměřit na vysvětlení Factory, proto je ukázka taková, jaká je, a mnoho programátorů by ji určitě vyřešilo mnohem lépe. To však není cílem článku.

Kde se Factory používá?

Představme si, že programujeme editor obrázků. V tomto našem editoru je základem obrázek, čili z pohledu OOP třída Obrazek. Obrazek by měl mít možnost se uložit a načítat. Opět jako programátor vím, že by tedy měl mít členské funkce save() a load(). Pojďme tedy trochu dál. Chceme však, aby náš editor nepracoval jen s jedním formátem, jako je například JPG, ale aby dokázal pracovat i s BMP nebo jinými. Tady nás jako programátora napadne vytvořit funkce save() a load() virtuálně a od třídy Obrazek dědit třídy příslušných typů formátů jako JpegObrazek nebo BmpObrazek, který bude mít vlastní implementaci save() a load(). Z třídy Obrazek se nám tedy stane jakési rozhraní a implementaci přenecháme potomkům. Na save() nyní zapomeneme a budeme se věnovat spíše nahrávání. Proč? Protože při loadingu bude vznikat instance dané třídy. Dejme tomu, že budeme rozlišovat typ formátu podle koncovky souboru. Loading zahrnuje jak první vytvoření příslušné instance děděné od třídy Obrazek, s kterou však v aplikaci budeme pracovat jako Obrazek. Uff, šílená věta, ale vysvětlím. Pod tím vším, co jsem tu napsal, se skrývá něco následujícího:

Obrazok* VytvorObrazok(string& subor, string& koncovka)
{
    Obrazok* obrazok = NULL;
       
    if (koncovka == "jpg") {
       obrazok = new JpegObrazok();
    } else if (koncovka == "bmp") { 
       obrazok = new BmpObrazok();
    } else if (koncovka == "tiff") {
       obrazok = new TiffObrazok();
    }
    ...

    return obrazok;
}

Určitě jste se s podobným řešením setkali. Tento kousek kódu však má určité problémy. Je funkční, dokonce velmi dobře, ale z pohledu návrhu to lze vyřešit mnohem elegantněji. Proč ale měnit něco, co funguje? Představte si, že do takovéhoto kódu jste nuceni něco dopsat. Takových situací ve vaší větší aplikaci může být více, pokud všechno budete řešit stále jiným způsobem, brzy se vám to vymstí. Další problém spočívá v rozlišování typů souborů na základě stringů. V tomto kroku je to dost nevhodné. Příkladem je to, že JPG obrázek nemá koncovku jen "jpg" ale i "jpeg". Tím se nám podmínka zvětšuje. 2-3 takovéto změny a kód se stává velmi nečitelný. To se ale třídy Factory ani tak netýká, je to jen rada, abyste v takových případech používali enumerace a rozlišování na základě koncovek tak oddělili. Tento kousek kódu však lze jednoduše zevšeobecnit, generalizovat, čímž dosáhneme jeho vyšší kvality.

Jak Factory vypadá?

To bylo vysvětlení, kde se Factory používá. A co Factory je? Je to třída jako každá jiná, jejíž účel je ten, aby vytvořila příslušnou instanci třídy Obrazek a oddělila tak implementaci od rozhraní. Udělám ale jedno odbočení a potom se vrátím k Factory.

Když se tak nad tím zamyslíte, pro naše účely je vlastně konstruktor tříd JpegObrazek a BmpObrazek nepřítel. Vytvoření objektu je jen na jednom místě a nikde jinde by neměl vznikat. Potřebujeme tedy zamezit libovolné konstrukci a přenechat to na Factory. Řešení je úplně jednoduché. Stačí, když konstruktor třídy bude private. Kromě toho do třídy začleníme statickou funkci create(), která vytvoří objekt Obrazek. Účel této statické funkce vám vysvětlím později. Nyní si povíme, že je velmi důležité. Abych to jen nepopisoval, tak uvádím příklad třídy JpegObrazek:

class JpegObrazok : public Obrazok
{
    private:
          JpegObrazok() 
          {
          }

    public:
          static Obrazok* create() 
          {
               return new JpegObrazok();
          }

          ...          
};

Vrátím se zpět k třídě Factory. Factory tedy bude obsahovat asociativní pole std::map, kde klíč bude typ formátu a hodnota bude ukazatel na statickou funkci create() dané třídy. Kromě toho třída Factory bude obsahovat přidávání do pole pomocí registerObject() a samozřejmě členskou funkci buildObject(), které předáme klíč a ona nám vrátí příslušnou instanci. Protože naším cílem je zevšeobecňovat, tak se bude jednat o jednoduchou šablonu.

Factory.h:
#if !defined(_FACTORY_H_)
#define _FACTORY_H_

#include <map>

template <class Product>
class Factory
{
   private:
      typedef Product* (*creator_t)();
      typedef std::map<int, creator_t> objects_map_t;

      objects_map_t mObjectsMap;

   public:
      Factory()
      {
      }

      void registerObject(int key, creator_t creator)
      {
         mObjectsMap.insert( make_pair(key, creator) );
      }

      Product* buildObject(int key) {
         typename objects_map_t::iterator i;

         i = mObjectsMap.find(key);
         if (i != mObjectsMap.end()) {
            return (i->second)();
         }

         return NULL;
      }
};
#endif //_FACTORY_H_

Kód je jednoduchý, snažil jsem se ho nedělat zbytečně složitý, aby se lépe chápal. Jde o jednoduchou šablonu. Product je v tomto případě parametr šablony, který bude nahrazen v našem případě třídou Obrazek. Nachází se tady jedna členská proměnná, kterou je asociativní pole mObjectsMap. Klíčem je v tomto případě int. Tento typ jsem si zvolil proto, aby bylo možné používat enumerace. Jak jsem říkal, já osobně se stringu při různých typech snažím vyhnout. Pro vytvoření instance třídy Obrazek je ale zapotřebí nejdříve zaregistrovat všechny statické create() funkce potomků této třídy a přiřadit jim klíče. Na to nám poslouží funkce registerObject(). V kódu se nachází creator_t, což není nic jiného než typ pro ukazatel na funkci, která nemá žádný parametr a vrací ukazatel na daný Product. Když tedy máme všechny potomky zaregistrované, můžeme požádat o vytvoření příslušné instance pomocí funkce buildObject(), které předáme klíč a ona nám vrátí ukazatel na vytvořený objekt třídy Obrazek.V případě že pro klíč neexistuje třída, funkce vrátí NULL.

Snad vás předcházející vysvětlení úplně nezmátlo, ale věřím, že po následující ukázce použití vám bude Factory jasnější:

...
enum typ_formatu
{
    TYP_JPEG,
    TYP_BMP,
    TYP_TIFF
};

Obrazok* NahrajObrazok(string& subor, typ_formatu typ)
{
    Factory<Obrazok> factory;
    Obrazok* obrazok = NULL;
    ...
    factory.registerObject(TYP_JPEG, JpegObrazok::create);
    factory.registerObject(TYP_BMP,  BmpObrazok::create);
    factory.registerObject(TYP_TIFF, TiffObrazok::create);       
    
    obrazok = factory.buildObject(typ);    
    ...    
    return obrazok;
};
...

Generické Factory

Pokud jste se dopracovali až sem, tak počítám s tím, že princip třídy Factory už vám je celkem jasný. Výše uvedená implementace není nic nového, nic převratného, dokonce se najde x lepších implementaci, které jsou všeobecnější, které se dají více přizpůsobit. Pointa tohoto článku přijde až nyní. Pokud se člověk zamyslí, tak proč by měl používat Factory? V konečném důsledku je to režie navíc. Je stále zapotřebí vytvářet nějaký Factory objekt, který stále musíme naplňovat, neustále v něm musíme hledat. Přece jen ty IF podmínky, které jsem uvedl na začátku, jsou z pohledu výkonu úspornější. Když se nad tím člověk zamyslí, tak se sám sebe zeptá. A nedala by se tato režie navíc řešit během kompilace? Odpověď zní ano. Šablony a generické programování je velmi silný prvek C++ a právě pomocí generik se naše nechuť k Factory dá odstranit.

factory.h
#if !defined(_FACTORY_H_)
#define _FACTORY_H_

/******************************************
 * Sablona teto tridy sa pouzije na
 * konci zretezeneho seznamu. Jeji
 * funkce buildObject() vrati NULL.
 *
 ******************************************/
template<class PRODUCT>
class EmptyFactoryItem
{
   public:
      inline static PRODUCT* buildObject(int key)
      {
         return NULL;
      }
};


/******************************************
 * Sablona reprezentuje a zapouzdruje
 * pointer na statickou funkci, ktera
 * vytvari tridu a jeji prislusny klic.
 *
 ******************************************/
template<int KEY, class CREATOR>
class FactoryItem
{
   public:
      typedef CREATOR creator;
      enum { factory_key = KEY };
};


/******************************************
 * Sablona inspirovana TypeListami.
 * Obsahuje clenskou funkci buildObject(),
 * ktera vytvari prislusny objekt.
 ******************************************/
template<class PRODUCT, class ITEM, class TAIL>
class Factory
{
   public:
      inline static PRODUCT* buildObject(int key)
      {
         if (ITEM::factory_key == key) {
            return ITEM::creator::create();
         }

         return TAIL::buildObject(key);
      }

};


/*****************************************
 * Makra, ktera zjednodusuji vytvoreni
 * seznamu.
 *****************************************/
#define FACTORY_1(P, I1, C1)                                                                 Factory<P, FactoryItem<I1, C1>, EmptyFactoryItem<P> >
#define FACTORY_2(P, I2, C2, I1, C1)                                                         Factory<P, FactoryItem<I2, C2>, FACTORY_1(P, I1, C1)>
#define FACTORY_3(P, I3, C3, I2, C2, I1, C1)                                                 Factory<P, FactoryItem<I3, C3>, FACTORY_2(P, I2, C2, I1, C1)>
#define FACTORY_4(P, I4, C4, I3, C3, I2, C2, I1, C1)                                         Factory<P, FactoryItem<I4, C4>, FACTORY_3(P, I3, C3, I2, C2, I1, C1)>
#define FACTORY_5(P, I5, C5, I4, C4, I3, C3, I2, C2, I1, C1)                                 Factory<P, FactoryItem<I5, C5>, FACTORY_4(P, I4, C4, I3, C3, I2, C2, I1, C1)>
#define FACTORY_6(P, I6, C6, I5, C5, I4, C4, I3, C3, I2, C2, I1, C1)                         Factory<P, FactoryItem<I6, C6>, FACTORY_5(P, I5, C5, I4, C4, I3, C3, I2, C2, I1, C1)>
#define FACTORY_7(P, I7, C7, I6, C6, I5, C5, I4, C4, I3, C3, I2, C2, I1, C1)                 Factory<P, FactoryItem<I7, C7>, FACTORY_6(P, I6, C6, I5, C5, I4, C4, I3, C3, I2, C2, I1, C1)>
#define FACTORY_8(P, I8, C8, I7, C7, I6, C6, I5, C5, I4, C4, I3, C3, I2, C2, I1, C1)         Factory<P, FactoryItem<I8, C8>, FACTORY_7(P, I7, C7, I6, C6, I5, C5, I4, C4, I3, C3, I2, C2, I1, C1)>
#define FACTORY_9(P, I9, C9, I8, C8, I7, C7, I6. C6, I5, C5, I4, C4, I3, C3, I2, C2, I1, C1) Factory<P, FactoryItem<I9, C9>, FACTORY_8(P, I8, C8, I7, C7, I6, C6, I5, C5, I4, C4, I3, C3, I2, C2, I1, C1)>

#endif //_FACTORY_H_

Uvedený kód využívá princip zřetězení šablon, který jsem vytáhl z TypeListů. Jde o vytvoření jakýchsi seznamů typů. V kódu je jeden prvek seznamu reprezentovaný šablonou FactoryItem, což je vlastně dvojice klíč–třída. Na konci kódu jsou makro funkce, které jsou taktéž obdobou maker používaných u TypeListů s tím rozdílem, že první argument je PRODUCT a následují opět dvojice klíč-třída. Momentálně jsem vytvořil makra pro záznam o velkosti 9 tříd, není však problém tato marka podle potřeby dále rozšiřovat. Šablona třídy Factory počítá s tím, že parametr ITEM bude FactoryItem, čili bude disponovat vnořeným typem creator, což je vlastně vytvořená třída obsahující statickou funkci create() a jedinou enumeraci factory_key, která bude mít hodnotu klíče. Třída disponuje nám už známou členskou funkcí buildObject().Ta pracuje na rekurzivním principu. V případě, že se klíč shoduje s klíčem ITEM-u, je volána statická funkce create() třídy creator, v jiném případě se přejde na následující prvek TAIL a volá se jeho buildObject(). Na konci seznamu je jako TAIL použita šablona EmptyFactoryItem, jejíž buildObject() vrátí NULL a už dále nic nevolá.

Je potřeba uvědomit si, že uvedený kód používá určité techniky generického programování. Pokud vám tedy fungování kódu není celkem jasné, dodporučuji poohlédnout se po dokumentech, které se těmito technikami zabývají. Šablona Factory je však psána jako znovupoužitelný kód a neznalost jejích principů vám nebráni ji používat. To, že se jedná o složitější kód, co se chápání týče, se vyplatí v jeho jednoduché použitelnosti. Napíše se to jednou a pořádně a dále to člověk už jen hravě používá. V našem případě obrázkového editoru by použití vypadalo následovně:

...
enum typ_formatu
{
    TYP_JPEG,
    TYP_BMP,
    TYP_TIFF
};

typedef FACTORY_3( Obrazok,
   TYP_JPEG, JpegObrazok,
   TYP_BMP,  BmpObrazok,
   TYP_TIFF, TiffObrazok
) ObrazokFactory;

...

Obrazok* NahrajObrazok(string& subor, typ_formatu typ)
{
   Obrazok* obr = ObrazokFactory::buildObject(typ);
   ...
}

...

Momentálně už nevykonáváme žádnou registraci za běhu programu, ale přesunuli jsme ji do kompilačního procesu. Protože jde většinou o malé statické funkce, udělal jsem je jako inline, čímž se zredukovala režie ještě i o volání funkcí. Co se týká použití, je jednoduché a není potřeba vytvářet žádný objekt, stačí jen použít definování nového typu pomocí makro-funkce. V případě, že se objekt pro daný klíč nenajde, funkce buildObject() narazí na konec seznamu, kde se nachází šablona třídy EmptyFactory, která vrátí NULL. Takto jsme vlastně dosáhli návrhový vzor Factory s výkonem IF podmínky.


Článek stažen z webu Programujte.com [ http://programujte.com/clanek/2007121301-navrhovy-vzor-factory-v-c/ ].