Know How - MultipleInterfacesTen kdo programuje v Javě určité zná slovo interface. Programátor v C++ to slovo nezná, pokud ho tedy neslyšel v podobné souvislosti, ale v C++ jej jako příkaz nenalezneme. Nicméně i tam můžeme napsat, že interface je vlastně abstraktní třída, která neobsahuje implementaci žádné z funkcí, kterou deklaruje
class IExampleIfc
{
public:
virtual void fn1(...)=0;
virtual void fn2(...)=0;
....
};
(já nejčastěji označuji interface písmenem I)
Nedávno jsem řešil problem. Mám interface, který je obecným základem pro nějakou hierarchii tříd. To znamená, že existuje několik (desítek) potomků, kteří tento interface dědí a implementují jeho funkce. Potud by to nebyl žádný zádrhel. Modul o kterém mluvím je víceméně hotový a používá se v několika projektech. A tak jsem si jednoho dne sedl a řekl, že napíšu vizualizátor této struktury, tj, že si napíšu nějaký program, který vypisovat data objektů z nějaké kolekce, kde každý objekt je právě instancí třídy některého potomka.
Mno, co s tím, ideální by bylo, kdybych měl na interface funkci Draw, kterou pak budu volat na každý objekt a podle typu se ten který objekt nakreslí. Jenže na interface taková metoda není, musela by se tam dopsat. Jenže jak jsem řekl, modul je víceméně hotový a používá se v několika projektech, zasahovat do jeho základní části jen proto, abych uspokojil nároky jednoho programu je dost velká odvaha (a hloupost). Co teď s tím?
Mohl bych zjišťovat typ každého objektu a mít připravený obrovský switch, jenž by na základě typu vyvolal nějaké kreslení. Nejde to napsat lépe?
Ono by stačilo, kdyby součástí rozhraní byla funkce, která by mi umožnila vrátit něco obecného, z čeho bych poznal co mám dělat dál. Ideálně ukazatel na nějaké jiné rozhraní. Jenže to rozhraní sám modul nezná, muselo by to být nějaký univerzální rozhraní...
Teď trochu odskočíme do Windows a jeho COM+ objektů. Ti co programují v COM+ znají, že základní rozhraní (ze kterého se dědí celé COM+) se jmenuje IUnknown a má tři metody. AddRef, Release a QueryInterface. AddRef a Release nechme stranou, slouží jen k počítání referencí pro automatické uvolnění opuštěních objektu (jednoduchý GC). Zaměřme se na QueryInterface. Každý objekt COM+ tuto metodu má. Funkcí dodáme nějakou identifikaci rozhraní a výsledkem je ukazatel na to rozhraní. Už svítá?
Udělejme return k našemu problému. Jak to tedy udělat s tím Draw? Budu muset do základního rozhraní dopsat něco podobného jako QueryInterface. Na základě nějakého smluveného ID se budu moci dotázat na případná další rozhraní přístupné v objektu. Výchozí implementace přitom může vracet NULL. Budu-li tedy implementovat Draw, musím každého potomka ještě podědit a rozšířit o nějaké moje rozhraní, které bude obsahovat deklaraci Draw.
class Potomek10: IPredek
{
// pisu jen proto, aby bylo vědět, jak potomek dědí základní rozhraní
}
class IMojeDrawRozhrani
{
virtual void Draw(Graphics &g)=0;
}
class MujPotomek10: public Potomek10, public IMojeDrawRozhrani
{
public:
virtual void *QueryInterface(int id)
{
if (id==smluvene_id)
return static_cast<IMojeDrawRozhrani *>(this);
else
return __super::QueryInterface(int id);
}
virtual void Draw(Graphics &g)
{
//malovani
}
}
Ještě musím do základního interface doplnit ještě toto
virtual void *QueryInterface(int id) {return 0;}
Ano, rozhraní vracím jako void *. Pro konverzi na rozhraní pak musí použít reinterpret_cast Je to malý flíček na celé struktuře. Hezčí by bylo, kdyby to bylo něco obecnějšího. Kam ale chcete zobecnit rozhraní? A pomohl bych si nějak? Jediným efekt by byla změna přetypování z reinterpret_cast na static_cast. Ano, mohl bych taky použít dynamic_cast pro runtime kontrolu, zda-li objekt opravdu vrací to co chci (ale to mohu myslím teď taky - a navíc je to zbytečné, pokud by tomu tak nebylo, je to chyba kterou musím řešit na úrovni zdrojáků, né v době běhu).
Pokud budu chtít nakreslit objekt:
IPredek *p=container[x];
IMojeDrawRozhrani *r=reinterpret_cast<IMojeDrawRozhrani *>(p->QueryInterface(smluvene_id));
if (r) r->Draw(g);
else
DrawNeznamyObjekt(g); //objekt neznám, nemohu kreslit.
Celá operace má jednu podmínku. Má aplikace, která chce toto využít musí na počítku být schopná kontrolovat vznik objektů. Musí upravit tento vznik tak, aby vznikali rozšíření potomci (o rozhraní Draw), Znamená to pro každého potomka napsat další třídu a tu pak použít při new. Dobře napsané knihovny které například vytváří struktury při čtení z disku obsahují tzv. faktory třídy. Což jsou třídy, které pro každy typ objektu v souboru obsahují virtuální metodu, jejíž úkolem je podle typu vytvořit instanci. Aplikace pak samozřejmě tuto třídu dědí a implementuje si metody tak, aby vracely instance potomků těchto tříd.
Tak ať máme o čem mluvit.... |