Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Applying the Factory Pattern within COM Components

Tech 1

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() and Release(): These methods manage the object's reference count. When the count drops to zero, the object typically deletes itself. Remember to call Release() when done, rather than using delete.
  • QueryInterface(): This method requests a specific interface pointer from the object. riid is the ID of the requested interface, and ppvObject is 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

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.