AppTrends

Sign up for our newsletter.

I agree to this site's Privacy Policy.

Industrial Strength Pluggable Factories

. Timothy Culp is the Chief Software Architect for the Harris Corporation ORIGIN Laboratory and an Instructor at Rollins College. He can be contacted at timothy.culp@computer.org.


THE PLUGGABLE FACTORIES pattern discussed in John Vlissides' Patterns Hatching column earlier this year1 is indeed a real pattern used in industry and is a powerful tool for designing extensible, object-oriented software. Harris Corporation accepts Mr. Vlissides challenge to document its use by providing a short history of our experiences in this area. We have found that Pluggable Factories not only provides a way to separate code developed under different funding but also provides a way for projects to "plug-in" new features and functionality to existing applications without having to modify common code.

Factories are a means of abstracting away the details of virtual construction,2 and our team has used them extensively throughout our image-processing software baseline. At first, we hard coded factories into a few key components, such as data import, radiometric conversions, and elevation surfaces. Over time, we generalized the commonality between the factories into a single template class called Maker.

The Maker template is a powerful creational pattern, but our original design did not address the variety of creational needs for all our projects. We found, however, that by combining several patterns we were able to satisfy each of the creational problems encountered. These compound patterns3 have since been named StreamMaker, ChainMaker, PriorityMaker, and SingletonMaker and are all just special derivatives of Pluggable Factories.

FACTORIES FOR PERSISTENCE Factories are an excellent mechanism for properly constructing a subtype from persistent storage. Consider the construction of a polymorphic shape from a stream. We have a shape.txt file with the following data:

  Circle {
      radius  3.14159
}
While creating the stream was a straightforward task, reconstructing the proper subtype from the stream turned out to be problematic.

Simple Factory We could have examined the contents of the stream at the application level to determine which subtype to create, but we chose to localize object creation4 by deferring construction to a factory. Our first approach was to create a static factory method within the Shape class that knew the right subtype to construct with a given stream. This method switched on a unique piece of the data within the stream, such as class name, and then passed the remainder of the stream to the proper Shape constructor:

class Shape {
public:
	static Shape* newShape(istream&);
};
Shape* Shape::newShape(istream& params) {
string className;
    params >> className;
    if (className == "Circle")
         return new Circle(params);
    else if (className == "Triangle") 
         return new Triangle(params);
    else…
}
Problems with the Simple Factory Although this mechanism was very clean and simple, it meant the Shape base class was aware of all the subtypes derived from Shape. It also required a modification to the newShape factory method every time a new shape was added to the system. This violated two important principles in object-oriented design: The first was the Dependency Inversion Principle,5 which states that high level modules should not depend on lower level modules/details. The second was Bertrand Meyer's Open-Closed Principle, 6,7 which states that a framework is open to extension but closed to modification for building maintainable code.

Was a one-line modification to previously delivered code really that much of a maintenance nightmare? Consider the situation when the base class source code isn't available for modification. In our Object Reuse In Geospatial INformation (ORIGIN) Laboratory, we are responsible for managing a common reuse repository of useful geospatial components for a large product line. The ORIGIN baseline is released as a set of header files and shared libraries for a variety of platform configurations. ORIGIN is maintained as a separate project so that changes to common code can be monitored and controlled, keeping the architecture focused and the interfaces stable. This is critical for successful reuse within a large organization. ORIGIN must provide the ability for other projects to extend base class functionality without modifying common code. While it did not seem like a maintenance nightmare, a one-line code change for ORIGIN was one line too many.

PLUGGABLE FACTORIES Because modifying previous code was not an option, we needed a dynamic mechanism for adding new subtypes to existing class hierarchies. We chose to build an associative container where each value in the container was a factory object, or a maker object, that knew how to create exactly one subtype from an abstract hierarchy. We formatted our streams so that the first thing present in the stream was the class name of the object followed by the parameters needed for construction. The name was stripped from the stream and used as a keyword for looking up the proper maker. The remainder of the stream was then passed to the maker to construct a new object. This was a simple permutation of the Prototype pattern,8 except instead of storing prototypes of objects in a list, we were storing prototypes of factories in a map.

Prototype Factory Compound Pattern Using the FactoryMethod, we created an abstract ShapeMaker class with a single public method called newShape and a concrete CircleMaker that knew how to create circles from a stream:

class ShapeMaker {
public:
   static Shape* newShape(istream&);
protected:
   typedef map<string,ShapeMaker*> MakerMap;
   virtual Shape* makeShape(istream&) const=0;
   static MakerMap registry;
};
class CircleMaker : public ShapeMaker {
private:
   Shape* makeShape(istream& params) const { 
      return new Circle(params); 
	}
};
In addition to CircleMaker, we then made other concrete makers for shapes such as TriangleMaker and RectangleMaker. An instance of each of these concrete ShapeMaker factories was stored in a map container in the ShapeMaker base class that associated a class name with a specific maker. When constructing from a stream, the ShapeMaker map was used to find the maker associated with the class name stored in the stream. The maker was then used to create the right type of Shape using the rest of the stream:
Shape* ShapeMaker::newShape(istream& is) {
    string className;
    is >> className;
    ShapeMaker* maker = 
       (*registry.find(className)).second;
    return maker->makeShape(is);
}
As a result, we had a much more general factory method that did not require modification when new shapes were added to the system.

Registering Factories Using Prototype All of this hinged on the fact that a mapping existed between class names and their associated makers. The Prototype pattern provided a means to dynamically register makers into our map.

Every concrete maker type owned a static instance of itself that got constructed during static initialization. The only purpose of this static instance was to register this maker with the static maker map:

class CircleMaker : public ShapeMaker {
private:
    CircleMaker() : ShapeMaker("Circle") {}
    static const CircleMaker registerThis;
};
ShapeMaker::ShapeMaker(string className) {
   registry.insert( make_pair(className, this) );
}
When applications need to create a shape object from a persistent store, the stream is passed to ShapeMaker and the right subtype is created:
fstream params("shapes.txt");
Shape* shape =ShapeMaker::newShape(params);
Plugging in with Prototype Factory The combination of the Prototype and Factory Method as our Pluggable Factory pattern allowed projects to extend the types of shapes that ShapeMaker could create by providing a shared library with their own project specific shapes and shape makers. These plug-in libraries are loaded at runtime (using dlopen for UNIX or LoadLibrary for Windows) to extend the shape hierarchy dynamically without modifying the Shape class.

The ShapeMaker and its collaborating classes represent a Prototype Factory compound pattern. The Factory Method supplies a means to create new shapes and the Prototype pattern provides a means to dynamically register factories at runtime. The participating classes are shown in Figure 1.

Figure 1. Prototype Factory static view.

GENERIC FACTORIES As we started the design of other Prototype Factory patterns, we quickly saw that the algorithm was independent of the data type being constructed. A natural progression was to create a template class parameterized on the type of object being made. For historical reasons within our organization, the template that captured the design of the Prototype Factory compound pattern was called Maker.

template 
<class Object> class Maker {
public:
   virtual ~Maker();
   static Object* newObject(istream&);
protected:
   Maker(const string& className);
   virtual Object* makeObject(istream&) const=0;
private:
   typedef Maker<Object>* MakerPtr; 
   typedef map<string,MakerPtr> MakerMap;	
   static MakerMap registry;
};
Making Makers With the Maker template in place, ShapeMaker was rewritten to be a derivation of the Maker template parameterized on Shape:
class ShapeMaker : public Maker<Shape> {
protected:
   ShapeMaker(const string& className)
      : Maker<Shape>(className) {}
};
class CircleMaker : public ShapeMaker { 
private:
   CircleMaker () : ShapeMaker ("Circle") {}
    Shape* makeObject(istream& params) const { 
      return new Circle(params); }
    static const CircleMaker registerThis;
};
To create Maker factories for other abstract class hierarchies, we made new subclasses derived from the Maker template as needed:
class CoordSysMaker
    : public Maker<CoordSys>{…};
class XyzMaker : public CoordSysMaker{…};
class RasterMaker : public CoordSysMaker{…};
LEGACY FILE FORMATS One problem with generic programming is that templates must make assumptions about the data types they are operating on. If our assumptions are too specific, then we limit the usefulness of the template by over-specifying what types of classes can be used. Our team encountered problems where our template was too assumptive when we tried to apply our Maker template to existing legacy file formats.

Problems with the Maker Prototype Factory The first assumption we made was that the objects could be reconstructed from a stream. Legacy classes sometimes use stdio instead of IOStreams. Entire sections of code could have been rewritten to change FILE* to istreams, but this was a costly approach that resulted in very little return.

Our second assumption was that the stream format was a class name identifying the type of object being stored followed by the data parameters required to restore the object to its previous state. This assumption was fine for classes written after the Maker pattern was developed. Legacy formats or proprietary formats did not follow this convention.

Making Objects from Files Rather than coming up with a separate Maker mechanism to address these existing file formats, we chose to generalize our existing template so that it supported both persistent and legacy formats. In other words, we needed to address the assumptions that were overspecified to see if they could be expressed in another way. For instance, the assumption that the construction parameters that were embedded in a stream could be fixed simply by adding a new template parameter that allowed the user to specify the data type of the construction parameters at runtime:

template <class Object, class Params> 
class Maker {
public:
   static Object* newObject(Params);
protected:
   typedef Maker<Object,Params>* MakerPtr;
   virtual Object* makeObject(Params) const=0;
};
The Maker template could then be instantiated for streams or FILE* simply by identifying the construction parameters along with the object types being made:
class ShapeMaker 
    : public Maker<Shape, istream&> { … };
class LegacyShapeMaker
    : public Maker<Shape, FILE*> { … };
Making Objects from Aggregate Classes At this point, our Maker template was general enough to handle construction parameters that were different from stream or FILE*. We could now make objects from an aggregate class that contained the required data to construct a number of subtypes. For instance, we construct geometry models that describe the relationship of a satellite sensor to the ground with the name of the sensor, a FILE* to a set of geometric parameters, and a file path to where adjusted geometry files can be written after image registration. We grouped these construction parameters into an aggregate class called GeomModelParams that was then used as the construction parameters for our Maker template:
struct GeomModelParams {
     const char*    sensorName;
     FILE*          geomFile;
     const char*    outputDir;
};
class GeomModelMaker 
: public Maker<GeomModel,GeomModelParams> { … };
Chain of Factories Our second mistake was to assume the type of object being stored was the first item in the stream. Because we could not rely on this mechanism for identifying the Maker for legacy files, we needed an alternative lookup mechanism that was more general.

We chose to add Chain of Responsibility to our compound pattern. This pattern allowed us to iterate sequentially through a list of Makers and let the makers decide whether or not they could recognize the constructions parameters. If the maker could not make the object, a null was returned and we tried the next maker. Our more general lookup mechanism looked like this:

Object* Maker<Object,Params>::newObject(
    Params params) {
	
    Object* object = 0;
    for (const_iterator iter = registry.begin(); 
        !object && iter != registry.end(); 
        ++iter )
	{
        MakerPtr maker = (*iter).second;
        object = maker->makeObject(params);
	}
	return object;
}
STRATEGY FACTORIES Our implementation for newObject was more general using Chain of Responsibility, but it did have some consequences. We had given up a logarithmic search for a linear search in favor of a more general algorithm. Also, because each maker must look at the construction parameters, there may be performance issues if the constructors need to ingest large amounts of data or do significant processing before determining if the stream contains valid data. For our domain, we had a reasonable number of legacy data formats, and file identifiers were quickly found in the header, but obviously our generalization affected the scalability of the design. Did we really want legacy formats to hamper the lookup efficiency of the newer persistent stream formats?

We chose to support both by using the Strategy pattern. Strategy allowed us to break up the Maker into two specialized subclasses, each with its own unique lookup strategy. We called the first subclass a StreamMaker, which is used when a keyword is available in a stream for associative lookup. The second subclass we called ChainMaker because a chain of makers is traversed until an object is constructed.

PRIORITIZING FACTORIES Up to this point, we had assumed there existed a 1-1 mapping between construction parameters and makers. In other words, for any set of data there was one and only one maker that would construct a subtype. Our algorithm failed when more than one maker could make an object from a set of data. We had to allow for makers with duplicate keys to be registered.

Our solution was to change our Maker map to a multimap. However, allowing duplicates introduced ambiguity as to which maker to use when more than one maker was registered for a class name. We wanted the maker that would return an object with the most functionality. Given a set of makers registered under the same name, we wanted to select the one with the highest priority.

Priority Lookup Strategy We created another subclass of Maker called a PriorityMaker. Its lookup strategy was to search for the highest priority maker given a range of makers. This lookup method started as usual by stripping off the class name from the stream:

template <class Object>
class PriorityMaker 
    : public Maker<Object,istream&> {
public:
   static Object* newObject(istream& params) {
        string className;
        params >> className;
After identifying the class type to construct, we wanted to find the highest priority maker registered under that name. A simple lookup was no longer sufficient. The first occurrence in our map was found using the lower_bound algorithm:
const_iterator iter
   = registry.lower_bound(className);
After finding the initial maker, we continued looping through the multimap while the class names were the same. At the end of the loop, we had selected the highest priority maker:
const_iterator tmp = iter;	
while (	++tmp != registry.end() && 
           (*tmp).first == className) 
{
    if (*(*iter).second < *(*tmp).second)
    iter=tmp; 
}
When we had an iterator identifying the highest priority maker, we used the maker to create the object:

    MakerPtr maker = (*iter).second;
    return maker->makeObject(params);
}
Priority was determined by using operator< defined on the base class. The default behavior of most makers was to return true if the key was less than the argument maker's key. In the case of a PriorityMaker, operator< was overridden to use a simple integer priority criteria to determine whether this maker was "less than" another.

Applications for PriorityMaker We have used the PriorityMaker scheme to switch ORIGIN objects with their project-specific counterparts at runtime. Most image processing algorithms are common between many image processing projects except for tweaks and specializations for a specific project. This means most of the algorithm development could be done in the ORIGIN lab, while the specializations and experimentations could be done in the project labs. Applications running in the ORIGIN lab would not have any project libraries present, so only the default makers would register. When the application is run in a project environment, the project-specific makers are dynamically loaded, thus out-prioritizing their ORIGIN counterparts.

Another useful application for the PriorityMaker is to load new objects on a continuously running system. If a bug is reported in an object, a new maker that creates a new and improved object is dynamically loaded. In this case, the priority criteria is the version of the maker itself. Alternatively, the current object could be unloaded by deregistering its maker, so the application drops back to the previously working version of the object.

SINGLETON FACTORIES Just like applications have Singleton objects, we had a need for SingletonMakers when there was only one choice to be made as to which type of Singleton object should be created at runtime.

In the ORIGIN baseline, we have a Singleton Log object so every message gets sent to the same log regardless of where in the program the entry is generated. We have many different concrete subtypes of abstract Logs. Terminal applications use a ConsoleLog that is a pass through to standard output. Batch applications have a FileLog that is a pass through to a file output stream. User applications have a DisplayLog associated with a GUI widget. Only one Log object is created at runtime, and the type is determined by our SingletonMaker.

Singleton Lookup Strategy The interesting thing about the SingletonMaker is that lookup strategy was very simple—only one maker was ever registered at a time. This was controlled as part of the environment either by explicitly linking with only one library or by dynamically loading a single library at runtime. In the errant event that more than one maker got registered, our rule of thumb was "last one registered wins."

template <class Object>
class SingletonMaker 
: public Maker<Object,void> {
public:
   static Object* newObject() {
        MakerPtr maker = 
        (*registry.rbegin()).second;
            return maker->makeObject();
	}
protected:
SingletonMaker() 
: Maker<Object,void>("SingletonMaker"){}
};
The static view of the Strategy Factory Prototype pattern with all the different flavor of makers are shown in Figure 2.

Figure 2. Strategy Prototype Factory static view.

Applications for SingletonMaker We use the SingletonMaker to construct an interface dynamically for accessing operating system services. An abstract OsService is instantiated as either a UNIXService or a WindowsService, depending on the platform architecture. Developers use this abstract interface to make system calls so code is portable between platforms. The selection of the type is completely automatic because either a UNIX ".so" containing a UNIXServiceMaker or a Windows ".dll" containing a WindowsServiceMaker will be dynamically loaded at runtime. Both will never be loaded within the same application.

Conclusion The Maker template has provided a simple mechanism to plug in new features to ORIGIN for a variety of scenarios and continues to evolve. We have since parameterized the registration key and the comparison function on the Maker map so makers can register by something other than class name:


template <class Object, class Params,
   class Key, class Compare> class Maker { … };
We also plan on supporting a mode that will search through a range of makers and make a selection based on a list of keywords and preferences, similar to a CORBA locator service.

What started off as an elementary pattern four years ago eventually grew to a combination of several coordinating patterns necessary to meet our somewhat lofty goal of providing a single, cross-project, platform-independent creational pattern.

  • StreamMaker combines both Prototype and Factory Method to create objects from streams using a dynamically populated registry of factories.

  • ChainMaker uses a Strategy-Prototype Factory compound pattern to allow for different lookup strategies to handle legacy file formats. The lookup strategy uses Chain of Responsibility to iterate through a chain of factories.

  • PriorityMaker, yet another variant of the Strategy-Prototype Factory pattern whose lookup strategy returns the factory with the highest priority.

  • SingletonMaker uses the Singleton pattern for those special cases when only a single factory exists.
These Maker classes have proven to be a critical asset in helping achieve ORIGIN's continued success in promoting successful and economical reuse within Harris's image processing product line. Now that we have taken the time to archive our experiences with Pluggable Factories, the only question remaining is exactly what kind of gift are we going to get from Mr. Vlissides?

Acknowledgments
Special thanks to Dennis Maly and Andy Breeden for their contributions, inspiration, and guidance.

References

  1. Vlissides, J. "Pluggable Factories I-II," C++ Report, 11(1)-11(2), Jan.-Feb., 1999.
  2. Coplien, J. "More on the Geometry of C++ Objects," C++ Report, 11(3), Mar. 1999.
  3. Vlissides, J. "Composite Design Patterns (They Aren't What You Think)," C++ Report, 10(6), June 1998.
  4. Stroustrup, B. The C++ Programming Language, 3rd ed., Addison-Wesley, Reading, MA, July 1997.
  5. Martin, R. "The Dependency Inversion Principle," C++ Report, 8(5), May 1996.
  6. Meyer, B. Object-Oriented Software Construction, 2nd ed., Prentice Hall, April 1997.
  7. Martin, R. "The Open-Closed Principle," C++ Report, 8(3), Mar. 1996.
  8. Gamma, et. al. Design Patterns, 1st ed., Addison-Wesley, Reading, MA, 1995.


Quantity reprints of this article can be purchased by phone: 717.560.2001, ext.39 or by email: sales@rmsreprints.com.
comments powered by Disqus