Applying the Factory Pattern within COM Components
In the previous discussion on the Abstract Factory pattern, we encountered a limitation regarding the flexibility of object creation. This article presents a solution using Component Object Model (COM) technology and explores how the Factory Pattern is utilized within it.
The fundamental reason the standard Factory Pattern can violate the Open/Closed Principle is that object creation logic is often hard-coded within the client module. If we could externalize class names—storing them in XML, the Registry, or configuration files—and then dynamically instantiate objects based on these names, we would achieve a level of flexibility similar to Reflection in C#. Fortunately, Microsoft's COM technology provides exactly this platform.
1. What is COM?
To maintain focus on the implementation, we will skip the theoretical introduction to COM. Please refer to standard documentation or technical encyclopedias for details.
2. Implementing a COM Component
To demystify the essence of COM, we will build an in-process COM component from scratch without relying on the ATL (Active Template Library) automation wizards.
2.1 COM Fundamentals
2.1.1 Return Types
HRESULT is the standard return type for COM methods. Its essentially a long value. A value greater than or equal to zero indicates success, while a negative value represents failure and corresponds to a specific error code.
2.1.2 GUID
A GUID (Globally Unique Identifier) and an IID (Interface Identifier) are essentially the same thing: a 128-bit number guaranteed to be unique. COM uses GUIDs to identify interfaces and classes, preventing naming collisions.
2.1.3 The IUnknown Interface
IUnknown is the root of the COM univerce. Every COM interface and factory class must inherit from it.
class IUnknown
{
public:
virtual ULONG AddRef() = 0;
virtual ULONG Release() = 0;
virtual HRESULT QueryInterface(REFIID riid, void **ppvObject) = 0;
};
AddRef()andRelease(): These methods manage the object's reference count. When the count drops to zero, the object typically deletes itself. Remember to callRelease()when done, rather than usingdelete.QueryInterface(): This method requests a specific interface pointer from the object.riidis the ID of the requested interface, andppvObjectis the output pointer to that interface.
2.1.4 IClassFactory
IClassFactory is the base interface for all factory classes. It inherits from IUnknown.
class IClassFactory : public IUnknown
{
public:
virtual HRESULT CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppv) = 0;
virtual HRESULT LockServer(BOOL bLock) = 0;
};
The CreateInstance() method is the core of the factory. It instantiates the final COM object for the user. pUnkOuter is used for aggregation (we will pass NULL), riid is the ID of the object being requested, and ppv is the output pointer.
2.2 Creating the DLL and Exporting Functions
A COM DLL must export four standard functions:
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv);
STDAPI DllCanUnloadNow();
STDAPI DllRegisterServer();
STDAPI DllUnregisterServer();
The most critical function here is DllGetClassObject, which retrieves the factory object.
2.3 Example: Database Operations
Let's reimplement the database operation example from the previous discussion using COM technology.
2.3.1 Defining Abstract Interfaces
(Note that they must inherit IUnknown)
class IEmployee : public IUnknown
{
public:
virtual bool Save(const Employee& data) = 0;
virtual Employee Retrieve(int id) = 0;
};
class IDepartment : public IUnknown
{
public:
virtual bool Save(const Department& data) = 0;
virtual Department Retrieve(int id) = 0;
};
2.3.2 Implementing the COM Object
We will create a class that implements both interfaces. The CMySqlDatabase implementation is similar.
class CAccessRepository : public IEmployee, public IDepartment
{
public:
CAccessRepository() : m_referenceCount(0) {}
ULONG STDMETHODCALLTYPE AddRef(void)
{
return ++m_referenceCount;
}
ULONG STDMETHODCALLTYPE Release(void)
{
ULONG count = --m_referenceCount;
if (0 == count)
{
delete this;
}
return count;
}
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject)
{
HRESULT hr = S_OK;
if (IsEqualIID(riid, IID_IEmployee))
{
*ppvObject = static_cast<IEmployee*>(this);
}
else if (IsEqualIID(riid, IID_IDepartment))
{
*ppvObject = static_cast<IDepartment*>(this);
}
else
{
hr = E_NOINTERFACE;
}
if (SUCCEEDED(hr))
{
AddRef();
}
return hr;
}
// IEmployee Implementation
bool Save(const Employee& data) override
{
_tprintf(_T("Saving employee %s to Access DB\n"), data.name.c_str());
return true;
}
Employee Retrieve(int id) override
{
Employee data;
printf("Fetching employee from Access DB with ID %d\n", id);
return data;
}
// IDepartment Implementation
bool Save(const Department& data) override
{
_tprintf(_T("Saving department %s to Access DB\n"), data.deptName.c_str());
return true;
}
Department Retrieve(int id) override
{
Department data;
printf("Fetching department from Access DB with ID %d\n", id);
return data;
}
private:
ULONG m_referenceCount;
};
COM allows a single object to expose multiple interfaces. The QueryInterface implementation uses casting to return the appropriate interface pointer based on the requested IID. This resembles the Simple Factory pattern in logic but differs in implementation mechanism.
2.3.3 Implementing the Object Factory
The CMySqlFactory implementation is similar.
class CAccessFactory : public IClassFactory
{
public:
CAccessFactory() : m_referenceCount(0) {}
ULONG STDMETHODCALLTYPE AddRef(void)
{
return ++m_referenceCount;
}
ULONG STDMETHODCALLTYPE Release(void)
{
ULONG count = --m_referenceCount;
if (0 == count)
{
delete this;
}
return count;
}
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject)
{
HRESULT hr = S_OK;
if (IsEqualIID(riid, IID_IClassFactory) || IsEqualIID(riid, IID_IFactoryAccess))
{
*ppvObject = static_cast<IClassFactory*>(this);
}
else if (IsEqualIID(riid, IID_IUnknown))
{
*ppvObject = static_cast<IUnknown*>(this);
}
else
{
hr = E_NOINTERFACE;
}
if (SUCCEEDED(hr))
{
AddRef();
}
return hr;
}
HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppv)
{
if (pUnkOuter)
{
return CLASS_E_NOAGGREGATION;
}
CAccessRepository* pObj = new CAccessRepository();
if (NULL == pObj)
{
return E_OUTOFMEMORY;
}
HRESULT hr = pObj->QueryInterface(riid, ppv);
if (FAILED(hr))
{
delete pObj;
return hr;
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE LockServer(BOOL bLock)
{
if (bLock) g_serverLockCount++;
else g_serverLockCount--;
return S_OK;
}
private:
ULONG m_referenceCount;
};
Every COM object requires a corresponding factory (satisfying the Factory Method pattern). The factory's CreateInstance method news the object and then calls its QueryInterface to return the specific interface requested by the client.
2.3.4 DllGetClassObject Implementation
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv)
{
if (!IsEqualGUID(rclsid, CLSID_DatabaseComponent))
{
return CLASS_E_CLASSNOTAVAILABLE;
}
HRESULT hr = S_OK;
IClassFactory* pFactory = NULL;
do
{
*ppv = NULL;
// Try MySQL Factory
pFactory = new CMySqlFactory();
if (pFactory)
{
if (S_OK == pFactory->QueryInterface(riid, ppv))
{
break;
}
else
{
pFactory->Release();
pFactory = NULL;
}
}
// Try Access Factory
pFactory = new CAccessFactory();
if (pFactory)
{
if (S_OK == pFactory->QueryInterface(riid, ppv))
{
break;
}
else
{
pFactory->Release();
pFactory = NULL;
}
}
hr = CLASS_E_CLASSNOTAVAILABLE;
} while (FALSE);
return hr;
}
This function iterates through available factories, checking which one satisfies the requested riid. This approach maintains flexibility and consistency with COM querying mechanisms.
2.4 Using the COM Component
2.4.1 Defining GUIDs and IIDs
// {A1B2C3D4-5E6F-7A8B-9C0D-1E2F3A4B5C6D}
static const GUID IID_IEmployee =
{ 0xa1b2c3d4, 0x5e6f, 0x7a8b, { 0x9c, 0x0d, 0x1e, 0x2f, 0x3a, 0x4b, 0x5c, 0x6d } };
// {B2C3D4E5-6F7A-8B9C-0D1E-2F3A4B5C6D7E}
static const GUID IID_IDepartment =
{ 0xb2c3d4e5, 0x6f7a, 0x8b9c, { 0x0d, 0x1e, 0x2f, 0x3a, 0x4b, 0x5c, 0x6d, 0x7e } };
// {C3D4E5F6-7A8B-9C0D-1E2F-3A4B5C6D7E8F}
static const GUID IID_IFactoryMySql =
{ 0xc3d4e5f6, 0x7a8b, 0x9c0d, { 0x1e, 0x2f, 0x3a, 0x4b, 0x5c, 0x6d, 0x7e, 0x8f } };
// {D4E5F6A7-8B9C-0D1E-2F3A-4B5C6D7E8F9A}
static const GUID IID_IFactoryAccess =
{ 0xd4e5f6a7, 0x8b9c, 0x0d1e, { 0x2f, 0x3a, 0x4b, 0x5c, 0x6d, 0x7e, 0x8f, 0x9a } };
// {E5F6A7B8-9C0D-1E2F-3A4B-5C6D7E8F9A0B}
static const GUID CLSID_DatabaseComponent =
{ 0xe5f6a7b8, 0x9c0d, 0x1e2f, { 0x3a, 0x4b, 0x5c, 0x6d, 0x7e, 0x8f, 0x9a, 0x0b } };
2.4.2 Registering the Component
Run the following command in the command prompt:
Regsvr32 DatabaseCom.dll
2.4.3 Client Code
void TestComponentUsage()
{
CoInitialize(NULL);
HRESULT hr = S_OK;
// Change this IID to switch between MySQL and Access factories
IID factoryIID = IID_IFactoryAccess;
IClassFactory* pClassFactory = NULL;
// Retrieve the class factory
hr = CoGetClassObject(CLSID_DatabaseComponent, CLSCTX_INPROC_SERVER, NULL, factoryIID, (void**)&pClassFactory);
if (SUCCEEDED(hr))
{
IEmployee* pEmployee = NULL;
if (SUCCEEDED(pClassFactory->CreateInstance(NULL, IID_IEmployee, (void**)&pEmployee)))
{
Employee empData;
empData.id = 1;
empData.name = _T("Alice");
pEmployee->Save(empData);
pEmployee->Release();
}
else
{
printf("Failed to get IEmployee interface!\n");
}
IDepartment* pDept = NULL;
if (SUCCEEDED(pClassFactory->CreateInstance(NULL, IID_IDepartment, (void**)&pDept)))
{
Department deptData;
deptData.id = 101;
deptData.deptName = _T("Engineering");
deptData.manager = _T("Bob");
pDept->Save(deptData);
pDept->Release();
}
else
{
printf("Failed to get IDepartment interface!\n");
}
pClassFactory->Release();
}
else
{
printf("Failed to get Class Factory!\n");
}
CoUninitialize();
}
The client calls CoGetClassObject to retrieve the factory. After registration, the DLL's path and CLSID are stored in the Registry. CoGetClassObject locates the DLL via CLSID_DatabaseComponent, loads it, and calls DllGetClassObject with the factory's IID. Once the factory is obtained, its CreateInstance method is used to create the specific COM interfaces needed by the client.
To switch databases (e.g., from Access to MySQL), we only need to change the factoryIID variable. Since IIDs are just numerical constants, they can be stored in configuration files or the Registry. Adding a new data source (e.g., SQL Server) simply involves adding a new factory and interface to the component and updating the client's configuration. No client code changes are required, perfectly satisfying the Open/Closed Principle.
COM Registry Entries