Super Factory

Published July 17, 2009 by Francis Xavier, posted by Myopic Rhino
Do you see issues with this article? Let us know.
Advertisement
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:

  1. the product to be created doesn't fall within that hierarchy (i.e., it's a base product, or an unrelated product).
  2. the product to be created was not (or incorrectly) registered with its associated factory.
  3. 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:

Cancel Save
0 Likes 0 Comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement