Using a factory class as an DLL gateway

in
Platforms:
Keywords:

This is a basic example of a DLL API in the Symbian OS:

//  Include Files
#include <e32base.h>        // CBase

//  Class Definitions
class CEngine : public CBase
        {
        private: // constructors
                CEngine();

        public:         // new functions
                IMPORT_C static CEngine* NewL();
                IMPORT_C static CEngine* NewLC();
                IMPORT_C ~CEngine();

                IMPORT_C const TVersion Version() const;
                IMPORT_C void ProcessL ( const TDesC& aString );
               
        protected:
                void ConstructL();
        };

The problem with this design is that it violates the “Program To An Interface, Not An Implementation” design principle. Due to that violation the client is coupled to the actual implementation which leads to inflexibility.

Dependency to the interface

Our goal in this tutorial is to lose the dependency to the implementation. If the client is coupled only with the interface, the implementation can be easily changed according to the needs (e.g. an earlier version of the implementation).

First we have to design an interface class:

class MEngine
    {
    public:       
        virtual ~MEngine() {}
               
        virtual const TVersion Version() const = 0;
        virtual void ProcessL ( const TDesC& aString ) = 0;
    };

Notice that the interface also defines a virtual destructor so that implementations could be deleted trough it.

Decouple the implementation

Now that we have an interface we need to provide a way to construct an implementation without needing to add dependency with the implementation class. We can use the Factory design pattern to achieve this:

#include "EngineIf.h"

class EngineFactory
    {
    public:
        IMPORT_C static MEngine* EngineL( TVersion ver );
    };

The EngineL (factory function) returns pointer to an MEngine implementation. The client can only operate the engine trough it. In the example the function has one parameter to determine the implementation required: TVersion. This way the design can be used as a way to preserve the backward compatibility.

EXPORT_C MEngine* EngineFactory::EngineL( TVersion ver )
    {
    if ( ver.iMajor == 1 )
        {
        CEngineV1* engine = CEngineV1::NewL();
        return engine;
        }
    else
        {
        User::Leave ( KErrNotSupported );
        }

Of course in a real world not-so-trivial applications there could be more parameters to determine the implementation required. For example the factory function could interpret some kind of domain language and construct implementations based on that.

Now that the factory class provides MEngine implementations there is no need to export functions from the actual implementation:

//  Include Files
#include <e32base.h>        // CBase
#include "EngineIf.h" //MEngine

//  Class Definitions
class CEngineV1 : public CBase,
                  public MEngine
        {
        private: // constructors
                CEngineV1();

        public:         // new functions
                static CEngineV1* NewL();
                static CEngineV1* NewLC();
                ~CEngineV1();

        public:         // from MEngine
                const TVersion Version() const;
                void ProcessL ( const TDesC& aString );
               
        protected:
                void ConstructL();
        };

The files that should be exported in the bld.inf are the header files that define the interface and the factory.

Integrating the DLL

To integrate the DLL you must add the lib-file to the MMP-file and include the needed files.

This code segment demonstrates how to use the examples engine:

MEngine* engine = EngineFactory::EngineL( TVersion ( 1, 0, 0 ) );
CleanupDeletePushL( engine );
engine->ProcessL( _L(“test”) );
CleanupStack::PopAndDestroy( engine );

Notice that the engine is pushed to the cleanup stack with the CleanupDeletePushL, because the MEngine is not derived from the CBase.

Conclusion

We have been able to change the DLL so that it doesn't violate the “Program To An Interface, Not An Implementation” design principle. We have also concentrated the exported functions to a single static factory class. This will reduce the need for freezing because you have to freeze the DLL only if the factory interface changes.


Full source codes are attached to the tutorial.

AttachmentSize
source.zip17.16 KB

Re: Using a factory class as an DLL gateway

Excellent article! I really encourage developers to hide implementation details from (API) clients this way.

Note that I've also used this approach when design new APIs, however, I missed the point of using CleanupDeletePushL in combination with an empty virtual destructor. What I did on the other hand is that I added a Release() method to the interface and clients had to call that. That still isolated the real implementation from the interface so that clients had to bother only with the interface, however, it's less convenient then your approach.

Thanks for sharing!

Re: Using a factory class as an DLL gateway

Great article!
Designing your API in this way will save you a lot of headache when maintaining your code.

Keep up the good work introducing more good design patterns, the Symbian community really needs it Smiling

Re: Using a factory class as an DLL gateway

good one,,
I also suggest developers whoever want the deeper and better understanding of the same conceipt to go through "Design Patterns - factory Method" from Thinking in C++ as well..

Re: Using a factory class as an DLL gateway

Couple of basic things:

1. It should be noted that from Symbian OS 9.1 and onwards, the preferred method of dynamic code loading using a the factory design pattern is through ECOM. ECOM gives you all the advantages of a plug-in architecture, including versioning and interface management. It also avoids static linkage Smiling

2. I didn't get the bit about CleanupDeletePushL... calling does this mean you the object pushed on the cleanup stack will have its operator delete called when its popped? In that case, its not really going to work, is it? Because the destruction won't be virtual! It will just delete the M class, not the C class.

The better way would be to have a methold, Release in your interface class (M class) that's implemented by the C class and then you can use CleanupReleasePushL, which will give the C class to do cleanup in its Release method

Re: Using a factory class as an DLL gateway

1. Thanks for pointing that out. So the designer planning to use this design should also consider using ECOM instead of reqular DLL's. I have used this design when there is no need for ECOM's plug-in architecture.

2. Yes, the point is that the delete operator gets called when the M class is popped. It works, I just double checked it Smiling. The destructor in the M class is virtual and it is extended by the actual implementations destructor so that the actual destructor is called first. I quickly Googled for more information about this and found this: http://www.devx.com/tips/Tip/12729.

I think that the Release function is less sofisticated than using a virtual destructor.

Thanks for comments!

-Aleksi

Re: Using a factory class as an DLL gateway

Indeed the virtual destructor will ensure the derived objects destructor is called.

Actually, symbians cleanupstack heavily depend on this feature of C++.

Why do you think CBase:s destructor is virtual? Smiling

Re: Using a factory class as an DLL gateway

Hang on!

I didn't see that the M class had a virtual dtor. There's good reason why I didn't, because, my eyes are trained to that M classes or Mixins or Interfaces or whatever you call them should not normally have virtual destruction, because they are specifying an interface. The Symbian OS coding standards doesn't recommend having a virtual dtor for M classes so your approach, although works, is deviant from the norm Smiling

Re: Using a factory class as an DLL gateway

Great comments again! Thanks.

I have been only seeing the ease of use when using virtual destructor (M classes can be used as any other object). I investigated this from Stichbury's “Symbian Os Explained” and I noticed that I didn't consider enough the restrictions that the virtual destructor in an M class puts for implementations:
- Implementations have to be C classes (T classes don’t have destructors).
- Implementations are limited to cleaning up through destructor.

Using the Release() function is more flexible because it doesn't restrict the implementation in any way. Implementations can be for example singletons when the Release won't destroy the object if someone is still using it.

So recommendation to use the Release() is justified.

Re: Using a factory class as an DLL gateway

Indeed. A classic example is the IUnknown interface in COM.