Jump to content

  • Log In with Google      Sign In   
  • Create Account


Like
1Likes
Dislike

Super Factory

By Francis Xavier | Published Jul 17 2009 06:20 AM in General Programming

If you find this article contains errors or problems rendering it unreadable (missing images or files, mangled code, improper text formatting, etc) please contact the editor so corrections can be made. Thank you for helping us improve this resource

Introduction
With object factory implementations already done to death, you might be wondering what on earth a "Super Factory" is. Simply put, it's an object factory which can create objects of any type and return them through any of their respective base class interfaces. This is a very useful feature indeed (especially for persistence frameworks, and especially for the one I'm writing (: ). Conventional object factory implementations require the user to either (a.) derive the objects to be created at runtime from a common base class, or (b.) use multiple factories to create heterogeneous objects. A Super Factory exposes a single unified interface to create heterogeneous objects at runtime.

(Please note that the term "Super" here, is used to mean "very generic", or "broad in scope or content"; not a superior method/implementation.)


How to use the code
Using the code is quite easy. All we need do is add one file: SuperFactory.h (see the source code accompanying this article), to our project. To add Factory support to a class, we need to "register" it in the implementation file (.cpp) for that class. Macros have been included for the most common use cases.

Macro syntax:
SF_Register_Type[_Base(n)]( , , , , ... );

So, for e.g., to register an abstract class A, which doesn't have any base classes, we would use: SF_Register_Type( SF_Abstract, A );

And to register a concrete class C, which has two base classes A and B, we would use: SF_Register_Type_Base2( SF_Concrete, C, A, B );

Once the classes are registered, the Factory's Create method can then be used to create objects as required.


A simple example
Consider the following classes defined in their respective header files: // A.h struct A { virtual void Print() =0; virtual ~A() { } }; // B.h struct B : public A { virtual void Print() { cout << "In B::Print" << endl; } virtual ~B() { } }; // C.h struct C : public B { virtual void Print() { cout << "In C::Print" << endl; } virtual ~C() { } }; To add Factory support, we register each class in it's respective implementation file: // A.cpp SF_Register_Type( SF_Abstract, A ); // B.cpp SF_Register_Type_Base1( SF_Concrete, B, A ); // C.cpp SF_Register_Type_Base1( SF_Concrete, C, B ); Once that's done, we can use the Factory to create objects as required: A *pObj1; B *pObj2; A *pObj3; if( SuperFactory::Create( "B", pObj1 ) ) // Creates a B pObj1->Print(); // In B::Print if( SuperFactory::Create( "C", pObj2 ) ) // Creates a C pObj2->Print(); // In C::Print if( SuperFactory::Create( "C", pObj3 ) ) // Creates a C! pObj3->Print(); // In C::Print! Note that even though classes A and C might be implemented by two different people who know nothing of each other's implementations, the Factory has been indirectly "told" (by the registrations) that A is a base class of C. Hence, the Factory is able to create objects of type C and return them through an interface of type A, like pObj3 in the above example.
Another simple example
Consider the following classes defined in their respective header files: // A.h struct A { virtual void Print() =0; virtual ~A() { } }; // B.h struct B { virtual void Print() =0; virtual ~B() { } }; // C.h struct C : public A, public B { virtual void Print() { cout << "In C::Print" << endl; } virtual ~C() { } }; To add Factory support, we register each class in it's respective implementation file: // A.cpp SF_Register_Type( SF_Abstract, A ); // B.cpp SF_Register_Type( SF_Abstract, B ); // C.cpp SF_Register_Type_Base2( SF_Concrete, C, A, B ); Once that's done, we can use the Factory to create objects as required: A *pA; B *pB; if( SuperFactory::Create( "C", pA ) ) // Creates a C pA->Print(); // In C::Print if( SuperFactory::Create( "C", pB ) ) // Creates a C pB->Print(); // In C::Print Note that C derives from both A and B, so the Factory can create objects of type C and return them through either base interface, A or B.
Working with primitive types
The Factory can also create primitive types (provided that they're registered). So, suppose the user wanted the ability to create float and unsigned int objects via the Factory, he would first register them as usual (in one of the implementation files):

SF_Register_Type( SF_Concrete, float ); SF_Register_Type( SF_Concrete, unsigned int ); Once that's done, objects of these primitive types can be created as required: float *pFloat; if( SuperFactory::Create( "float", pFloat ) ) { // Do something with pFloat } unsigned int *pUint; if( SuperFactory::Create( "unsigned int", pUint ) ) { // Do something with pUint }
How does it work?
It's not required to know how it works in order to use it, so those who are not really interested can skip this section. The working of this Factory is very similar to the one described in the wonderful book "Modern C++ Design". For each specific product type (concrete/abstract), there exists an instance of a templated factory class to handle it. Each factory can only create objects of the product type it's designed to handle (provided that the product type it handles is not abstract). However, it also has the ability to delegate creation to the factories which handle derived Products.

So when asked to create a product, given a product identifier, the factory first sees if the requested product is the one it's designed to handle. If so, it simply uses the user-supplied product creation function and returns whatever's created to the user. But if the product identifier is not recognized by the factory, it delegates creation to factories which handle derived product types, hoping that the product will be recognized by one of them. This happens recursively until one of the factories in the product hierarchy recognizes the identifier (which means that it knows how to create products of that type), or none of the factories in that product hierarchy recognize the identifier. If a product identifier is not recognized by any of the factories in that product hierarchy, then:
  • the product to be created doesn't fall within that hierarchy (i.e., it's a base product, or an unrelated product).
  • the product to be created was not (or incorrectly) registered with its associated factory.
  • one of the products in the hierarchy was not (or incorrectly) registered, thereby breaking the "chain" of creation delegation.
If one of the factories in the hierarchy recognizes the product identifier, it creates the product and returns it to its caller, which would be the immediate base product factory. The happens recursively until the product finally reaches the user. When the user registers a product and its immediate base classes, behind the scenes something tricky happens. The product gets registered straightforwardly with its factory (via the Factory::Register function). However, as described in the paragraphs above, each product needs information about its derived products (to delegate creation to); not about its base products. Hence there's no "RegisterBase" function; instead there's a "RegisterDerived" function. So, the product is instead registered with the factories of its base classes (via the Factory::RegisterDerived function). This is how each factory knows about its derived products (even though it appears to the user that he's registering classes and their bases).


Pros/Cons
Pros:
  • Very easy to use and maintain.
  • Exposes a single unified interface to create all objects.
  • Doesn't require RTTI.
  • Doesn't require the objects to be polymorphic.
  • No overhead is added to objects (in terms of time/space).
  • Supports primitive types (float, unsigned int, etc.)
  • Supports (instantiated) templated types. (see Example4 in accompanying code)
  • Can provide Factory services to existing classes without any modifications.
Cons:
  • The implementation provided only has out of the box support for objects which have a public parameterless constructor. However, bypassing the normal registration macros allows you to provide your own functions to create objects, so you can use that to customize object creation. (Otherwise you can always hack the code to support your specific needs (: ).
  • Requires some maintenence if class hierarchies change (but usually, that's not something which happens too frequently).
  • A linear search is employed to find the required object type. The time taken depends on the hierarchical "distance" between the target type and the type to be returned. Normally this shouldn't be a problem, unless you have *extremely* deep class hierarchies.
Closing
Please note that in the code examples above, for the purpose of clarity, I've left out deleting the objects allocated by the factory; in real world code you'll want to delete them after use, or use std::auto_ptr (or a smart pointer) to do it automatically for you. In the accompanying source code, there are two more examples. One shows the Factory working with a diamond inheritance hierarchy, and the other shows the Factory working with templated types.

The source code is released under the MIT license and has been tested on thefollowing compilers:

  • Microsoft Visual C++ 2005/2008
  • GCC 4.3.2
There is much potential for improvement of the code; if you make changes to the code, improve it, or have some better ideas, I would love to know. I can be reached by email at francisxavierjp [at] gmail [dot] com. Comments/suggestions are always welcome!
References
[1] "Modern C++ Design"
[2] Loki C++ library
[3] "Creating a Generic Object Factory"
[4] Factory method pattern





Comments

Note: Please offer only positive, constructive comments - we are looking to promote a positive atmosphere where collaboration is valued above all else.




PARTNERS