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