Deprecated: mysql_connect(): The mysql extension is deprecated and will be removed in the future: use mysqli or PDO instead in /home/www/novacisko.cz/subdomains/bredy/init.php on line 11

Warning: mysql_connect(): Headers and client library minor version mismatch. Headers:50562 Library:100020 in /home/www/novacisko.cz/subdomains/bredy/init.php on line 11

Warning: Cannot modify header information - headers already sent by (output started at /home/www/novacisko.cz/subdomains/bredy/init.php:11) in /home/www/novacisko.cz/subdomains/bredy/index.php on line 38
Multiple Interface a Instance Factory - Bredyho blog - Ondřej Novák
Bredyho blog - Ondřej Novák

Multiple Interface a Instance Factory

Používáme-li v C++ dědičnost a polymorfizmus, často řešíme problém, jak psát operace nad objekty, jejiž implementace se liší podle typu potomka, bez toho aniž bysme zasáhli do Base třídy.


Každý na to jednou narazil, pokud používal nějaké datové typy vytvářené z nějakého základního rozhraní (dále IBase). IBase často definuje funkce, které potomci implementují a které jsou potřebné pro zajištění řešení problému, jenž potomci řeší.

Multiple Interface a Instance Factory obr1.gif

(Příklad hierarchické struktury třídy)

Způsoby detekce typu potomka

Nechť IBase definuje nějaké funkce, např: fn1(),fn2(),fn3().... Pomocí těchto funkcí můžeme řešit problém, kvůli kterému tato struktura existuje.

Nyní ale máme úkol řešit s touto strukturou jiný problém. Například chceme strukturu vypsat, nebo vizualizovat. Pokud není struktura serializovatelná, pak stejný problém budeme řešit při psaní serializace.

Vezměme jednoduchý příklad a to vypsání struktury na obrazovce. Máme jednotlivé instance uložené v kontejneru, který obsahuje ukazatele typu IBase. Každý tento ukazatel však ukazuje na některou instanci třídy DerivedX

Multiple Interface a Instance Factory obr2.gif

(Kontejner obsahující různé typy objektů)

Možností je hodně, proberme si některé z nich.

dynamic_cast<...>
První a vlastně nejdoporučovanější cesta v C++, která připadá v úvahu. Při vypisování postupně zkoušíme ukazatel přetypovat na potomky, které umíme vypsat. Pokud přetypování skončí úspěšně, známe typ potomka, můžeme jej vypsat. Pokud projde přetypování u Derived3, můžeme se pokusit přetypovat na Derived4 a Dervied5 a teprve při obou neúspěších předpokládáme, že třída je Derived3 a vypíšeme ji (Pokud by existoval nějaký další potomek, bude vypsán jako Dervied3).
Nevýhody: dynamic_cast nebývá nejrychlejší operací. Navíc vyžaduje, aby modul obsahoval RTTI informace. V určitých situacích to může být kanón na vrabce. Další nevýhoda je v Dervied3, pokud existuje nějaký další potomek, pak jej nepoznáme, a budeme jej číst jako Derived3. To znamená, že se ani nedozvíme, že existuje další potomek. Další nevýhodou je, že dynamic_cast nelze použít pro potomky využívající virtuální dědičnost.
IBase *ptr=array[i]
Derived1 *d1=dynamic_cast<Derived1 *>(ptr);
if (d1) {VypisDerived1(d1);return;}
Derived2 *d2=dynamic_cast<Derived2 *>(ptr);
if (d2) {VypisDerived2(d2);return;}
Derived3 *d3=dynamic_cast<Derived3 *>(ptr);
if (d3)
{
Derived4 *d4=dynamic_cast<Derived4 *>(d3);
if (d4) {VypisDerived4(d4);return;}
Derived5 *d5=dynamic_cast<Derived5 *>(d3);
if (d5) {VypisDerived4(d5);return;}
VypisDerived3(d2);return;
}
...
typeid()
Používáním porovnání typeid instance s typeid požadované třídy můžeme postupně testovat typ instance. Pokud nalezneme správný typ, využijeme static_cast a získaný ukazatel využijeme k získání údajů pro výpis instance.
Nevýhody: typeid bývá rychlejší než dynamic_cast, ale i tak vyžaduje RTTI informace. V tomto případě platí totéž jako u dynamic_cast. Zcela opačná situace nastane u Derived3. Pokud kromě Derived4 a Derived5 existuje ještě nějaký nový potomek, který neznáme, nenajdeme žádnou třídu, na kterou ukazatel můžeme převést a budeme muset vypsat "neznámý objekt". Můžeme ovšem zkombinovat dynamic_cast a typeid (dynamic_cast pro hledání třídy a typeid pro test,zda-li se skutečně jedná o finální třídu nebo je instance součástí dalšího potomka.
IBase *ptr=array[i];
if (typeid(*ptr)==typeid(Derived1) VypisDerived1(static_cast<Derived1 *>(ptr));
else if (typeid(*ptr)==typeid(Derived2) VypisDerived2(static_cast<Derived2 *>(ptr));
else if (typeid(*ptr)==typeid(Derived3) VypisDerived3(static_cast<Derived3 *>(ptr));
else if (typeid(*ptr)==typeid(Derived4) VypisDerived4(static_cast<Derived4 *>(ptr));
...
Typ objektu jako proměnná nebo funkce
Tato technika je založena na možnosti označkovat si objekt nějakou značkou. Každýmu potomkovi definuje příslušnou značku. Značka může být uložena v proměnné nebo může existovat virtuální funkce, která značku vrací.
class IBase1
{
public:
virtual TagType GetTag() const=0;
}
Nevýhody: Pokud je TagType enum, pak nelze přidávat nové značky bez modifikaci tohoto enumu (problém s modularizací). Pokud definujeme značku pomocí konstant, je to lepší, ale můžeme narazit na problém zajištění unikátních značek. V každém případě musí celá hierarchická struktura řešit značky, které ani nepotřebuje ke svému běhu, pouze řeší problém identifikace třídy.
IBase *ptr=array[i];
TagType tag=ptr->GetTag();
switch(tag)
{
case tagDerived1: VypisDerived1(static_cast<Derived1 *>(ptr));break;
case tagDerived2: VypisDerived2(static_cast<Derived2 *>(ptr));break;
case tagDerived3: VypisDerived3(static_cast<Derived3 *>(ptr));break;
case tagDerived4: VypisDerived4(static_cast<Derived4 *>(ptr));break;
...
};

K jádru problému

Položme si otázku, proč vlastně potřebujeme typ třídy (jejíž instanci "vidíme") identifikovat. Téměř vždy odpovíme proto aby bylo možné vykonat unikátní operaci závislou na typu instance. Pokud používáme systém identifikace třídy k tomuto účelu, neprogramujeme ve stylu OOP, jen pouze obcházíme svojí neschopnost vymyslet objektové řešení.

Nechtěl bych nikoho vinit z neschopnosti (i já to do nedávna řešil stejně). Řesení využívající možnosti OOP existuje, využívající polymorfizmus.

Konečně, jak lze docílit unikátní operace podle typu třídy? Přece virtuální funkcí. Base třída definuje funkce, které lze předefinovat v potomkovi, tak aby vykonaly nějaké operace unikátně napsané pro potomka.

Problém, který tu je, že nechceme zasahovat přímo do IBase rozhraní, které by mělo být nezávislé na konkrétním problému. Například začleněním virtuální funkce pro výpis do IBase bysme IBase učinili závislé na platformě (protože identifikace zařízení, do kterého se výpis provádí a jehož identifikaci je třeba předat parametrem je závislé na platformě).

class IBase
{
public:
....
....
virtual void VypisSe(PrintDevice &kam)=0;
};

I tady to jde "ošulit" pomocí Pimpl Idiomu, ale v této části to nedoporučuju.

Jak tedy napsat virtuální funkci, aby byla přístupná z IBase a přitom IBase přimo nebyla uvedena?

Jak dynamicky přidat virtuální funkci do base třidy bez nutnosti ji tam dopisovat?

Multiple Interface

Řešení není nijak jedinečné ani složité. Používají ho například COM+ objekty v Microsoft Windows. Těm, kteří COM+ znají, už asi svítá. Nebo pořád nic? QueryInterface

Tato technika umožňuje pod jednu IBase začlenit mnoho dalších interfaců, které vlastní IBase ani nemusí znát. Tyto interfacy lze definovat až podle konkrétního problému, který IBase a její struktura řeší.

class IPrintInterface
{
public:
virtual void VypisSe(PrintDevice &kam)=0;
};

Nakresleme si tuto situaci.

Multiple Interface a Instance Factory obr3.gif

Základem celého systému je funkce GetInterface<...>() (definovaná jako template), která vrací ukazatel na nový interface. Tento interface pak definuje novou funkci kterou původní IBase nezná. Funkce je virtuální, takže ji mohu zavolat na libovolný ukazatel typu IBase

IBase *ptr=array[i];
IPrintInterface *iprint=ptr->GetInterface<IPrintInterface>();

if (iprint) //pozor, co když takový interface neexistuje?
{
iprint->VypisSe(kam);
}

Funkci VypisSe musí implementovat potomci. Proto je naobrázku zakreslené vícenásobné dědění. Z každého potomka, který umí IBase děděním vytvoříme potomka MyDerivedX. Tento potomek bude navíc dědit IPrintInterface. Bude definovat funkci VypisSe, a bude definovat variantu funkce GetInterface, která vrátí ukazatel na jeho variantu rozhraní IPrintInterface.

Virtuální template? Jak na to?

Jak známo, template funkce nesmí být virtuální. Ale my ji potřebujeme napsat tak, aby jsme pak mohli tuto funkci implementovat v potomkovi. Najde se určitě mnoho způsobů, já uvedu ten, který používám ve svých projektech

class IBase
{
public:
virtual void *GetInterfacePtr(unsigned int ifcuid) {return 0;}

template<class InterfaceClass>
InterfaceClass *GetInterface()
{
return reinterpret_cast<InterfaceClass *>(GetInterfacePtr(InterfaceClass::IFCUID));
}
};

Základem této techniky je číslo IFCUID. Číslo identifikuje, který interface požadujeme. I příslušného interface pak musí být definována konstanta, která právě slouží k označení interface. Konstanta musí být unikátní vzhledem k IBase ve kterém se používá (pokud se stejné rozhraní sdílí u více hierarchických struktur, pak samozřejmě platí , že musí být unikátní vzhledem ke všem těmto strukturám). Upravme tedy IPrintInterface

class IPrintInterface
{
public:
const unsigned int IFCUID=1;
virtual void VypisSe(PrintDevice &kam)=0;
};

Příklad implementace potomka a unikátní operace

class MyDerived1: public Derived1, public IPrintInterface
{
public:

virtual void *GetInterfacePtr(unsigned int ifcuid)
{
if (ifcuid==IPrintInterface::IFCUID) //ptá se nás opravdu někdo na IPrintInterface?
{
return static_cast<IPrintInterface *>(this);
/*static_cast je nutný,
protože následuje přetypování na void * a pak reinterpret_cast, a vrácený ukazatel
by nebyl platný */
}
else
return Derived1::GetInterfacePtr(ifcuid); /*neznámé interface, necháme to předkovi */
}

virtual void VypisSe(PrintDevice &kam)
{
kam.Print("Derived1"); /*Pouze příklad*/
kam.NewLine();
}
};

Instance factory

Techninka Multiple Interface je založená na nutnosti derivovat ze všech potomků. To se dá naštěstí ulehčit vhodnou konstrukcí template:

template <class Predek>
class MyDerived: public Predek, public IPrintInterface
{
public:
//Konstrukce pomocí kopie (jedna z možností)
MyDerived(const Predek &predek):Predek(predek) {}
virtual void *GetInterfacePtr(unsigned int ifcuid)
{
if (ifcuid==IPrintInterface::IFCUID) //ptá se nás opravdu někdo na IPrintInterface?
return static_cast<IPrintInterface *>(this);
else
return Predek::GetInterfacePtr(ifcuid); /*neznámé interface, necháme to předkovi */
}

virtual void VypisSe(PrintDevice &kam);
};

template<>
void MyDerived<Derived1>::VypisSe(PrintDevice &kam)
{
...
}

template<>
void MyDerived<Derived2>::VypisSe(PrintDevice &kam)
{
...
}

template<>
void MyDerived<Derived3>::VypisSe(PrintDevice &kam)
{
...
}


//instance
IBase *k=MyDerived<Derived2>(Derived2(....)); //konstrukce kopií.

//výpis
IPrintInterface *p=k->GetInterface<IPrintInterface>();
k->VypisSe(kam);

Problém nastane až v okamžiku, kdy instance tříd vznikají uvnitř nějakých knihoven a uživatel nemůže nijak ovlivnit jaký objekt nakonec vznikne. Je tedy potřeba do takové knihovny dopsat cosi, co nazývám Instance Factory.

Jedná se o rozhraní, které se dotazuje na vytvoření instancí jednotlivých potomků.

class IBaseObjectFactory
{
public:
virtual IBase *CreateDerived1() const =0;
virtual IBase *CreateDerived2() const =0;
virtual IBase *CreateDerived3() const =0;
virtual IBase *CreateDerived4() const =0;
virtual IBase *CreateDerived5() const =0;
virtual IBase *CreateDerived6() const =0;
}

Dále existuje výchozí implementace, která pro každou funkci definuje vytvoření příslušné instance

class BaseObjectFactoryDefault
{
public:
virtual IBase *CreateDerived1() const {return new Derived1();}
virtual IBase *CreateDerived2() const {return new Derived2();}
virtual IBase *CreateDerived3() const {return new Derived3();}
virtual IBase *CreateDerived4() const {return new Derived4();}
virtual IBase *CreateDerived5() const {return new Derived5();}
virtual IBase *CreateDerived6() const {return new Derived6();}
}

Knihovna, která provádí tvorbu tříd pak má možnost změnit instanci třídy která slouží jako factory. V tomto místě pak můžeme ovlivnit typ objektu, který se vytvoří namísto požadovaného objektu.

Inicializace kopií

(využívá konstruktoru deklarováného v příkladu s template třídou)

class IBaseObjectFactory
{
public:
virtual Derived1 *CreateDerived1(const Derived1 &origin) const =0;
virtual Derived2 *CreateDerived2(const Derived2 &origin) const =0;
virtual Derived3 *CreateDerived3(const Derived3 &origin) const =0;
virtual Derived4 *CreateDerived4(const Derived4 &origin) const =0;
virtual Derived5 *CreateDerived5(const Derived5 &origin) const =0;
virtual Derived6 *CreateDerived6(const Derived6 &origin) const =0;
}

class BaseObjectFactoryMyDerived
{
public:
virtual Derived1 *CreateDerived1(const Derived1 &origin) const {return new MyDerived<Derived1>(origin);}
virtual Derived2 *CreateDerived2(const Derived2 &origin) const {return new MyDerived<Derived2>(origin);}
virtual Derived3 *CreateDerived3(const Derived3 &origin) const {return new MyDerived<Derived3>(origin);}
virtual Derived4 *CreateDerived4(const Derived4 &origin) const {return new MyDerived<Derived4>(origin);}
virtual Derived5 *CreateDerived5(const Derived5 &origin) const {return new MyDerived<Derived5>(origin);}
virtual Derived6 *CreateDerived6(const Derived6 &origin) const {return new MyDerived<Derived6>(origin);}
}

Výhodou je, že knihovna vytvářející objekty má větší volnost, může pracovat přímo s objekty které zná. Po vytvoření objektu však zavolá factory třídu a nechá si převést vytvořený objekt na jiný objekt jehož třída dědí třídu původní. Nevýhodou je nutnost vytvářet kopii.

To je celé přátelé

Doufám že vás to inspirovalo. V některým příštím článku si povíme, jak využít Multiple Interface k řešení v zájemných vztahů mezi potomky (například porovnávání dvou potomků, když znám pouze jejich ukazatel na IBase, nebo vzájemná poloha dvou grafických objektů, jako je například průsečíky, atd).

vytvořeno: 24.8.2006 09:54:45, změněno: 24.8.2006 12:38:01
Jsou informace v článku pro Vás užitečné?
  • (2)
  • (0)
  • (1)
  • (0)
  • (1)

Podobné články

Vracíme z funkce objekty - praktická ukázka

V článku Vracíme z funkce objekty jsem ukazoval, jak probíhá návrat objektu z funkce a jak toto optimalizovat tak, aby se minimalizovalo používání kopírování. Teď si to ukážeme prakticky - použil jsem gcc bez optimalizací

C++ - Akce a Zpráva jako objekt

Akcí nazývám obyčejné volání funkce, zprávou pak volání metody objektu (jak je chápáno podle OOP). Lze vůbec volání čehokoliv reprezentovat jako objekt? A k čemu je to vlastně dobré?

Úvod do obecného OOP 3. díl

Dědičnost

Automatické klonování objektů v C++

C++ nenabízí standardní prostředky jako klonovat objekty. Pouze kopírovací kontruktor, který nám však v případě polymorfních objektů moc nepomůže. Naše "lenost" nám však pomůže nalézt řešení.

Multithreading v C++ (ve Win32) II - ThreadHook

Pokračování dokumentace k Multithreading v C++ (ve Win32)
Reklama: