Introduction
Developing dynamic, flexible, and maintainable applications these days is a neccesary, yet difficult task. Systems grow at exponential rates as companies require more and more advanced logic to run their businesses. The ability of a programmer to understand and maintain such systems diminishes as a systems complexity increases. The creative programmer can come up with ways to combat the difficulties in maintaining a complex system, but often those ways are ignored when new functionality must be developed in a very short time (which, as any programmer who has worked in the corporate world will know, is always the case....time doesn't exist, only the deadline).
There are many ways to make systems extensible. The best way is to take the time up front to design and plan, doing what you can to predict what additions to a system may be neccesary in the future, and plan for them. Quite often, systems are designed with placeholders or hooks that in the future will hold other, already known code. Since a deadline prevents that code from being developed initially, it must be added later on. Since the required functionality is already known, its fairly easy to plan for it and make the system ready for it. Prior knowledge of what future functionality a system may require is not always possible, though.
In the situations where you know the system will need to be extended, but what extensions will be neccesary are not known, the ability to plan for those extensions is greatly reduced. You can't build in fixed placeholders or hooks for the future functionality. If you do build in hooks, they must be generic, capable of loading code without prior knowledge of its inner workings. This article will outline one of the most useful tools for designing extensible systems, regardless of whether the neccesary extensions are already known or not.
Design Patterns
In the last 15-20 years, many innovations and improvements in the way programs, applications, and systems are developed. Old, monolithic, flat systems design has fallen in the face of n-tier systems, object oriented programming, inheritance and polymorphism, modular development, and design patterns. Greater strides in project management have been made to make better use of these better ways of thinking about development. This article focuses on the aspect of Design Patterns, and in particular, a specific pattern called a Class Factory.
Design Patterns arn't as tangible a thing as a class or method. A design pattern is more a way of thinking, about the code you intend to write and how that code will be used in a system. Design patterns help fuel creative thinking when planning all or part of a larger system, and can help save time by providing something of a template to begin working from. There are many design patterns that are well defined and frequently used today. The Facade pattern provides a template that allows one system to communicate with another whithout requiring changes in either. Similarly, the Adapter pattern provides a template that allows one class to talk with one or more classes of different design and communications methods. The Singleton pattern makes it possible to use a single instance of a class anywhere in a system. And the Abstract Factory, similar to the topic of this article, allows the creation and use of classes without knowing all the details of those classes, and without needing to directly instantiate those classes.
The Abstract Factory: Some History
This article will focus on the creation of a Class Factory, which is similar in concept to the Abstract Factory. The definition of an abstract factory is as follows:
"Provide an interface for creating families of related or dependent objects without specifying their concrete classes."
The development of an abstract factory requires a fair amount of forthought and planning to develop the neccesary infrastructure. Numerous classes, both abstract and concrete, are required to allow a client to create and use classes it does not have direct knowledge of. Before providing you with in-depth details of how an abstract factory works, take a moment to examine the following diagram. This diagram outlines the structure and interactions in an abstract class factory system:
The theory behind the Abstract Factory is simply "implicit use", as opposed to explicit use. A class that is marked as abstract is non-instantiable, an implicit representation of all the classes that derive from it. When one of those derived classes is instantiated with new, the class is being "explicitly used". With an abstract factory, the client can implicitly use any classes derived from the abstract products that are producable with the abstract factory. The client does not have explicit knowledge of either ProductA1 or ProductB1, and any additional functionality in those classes not defined in AbstractProductA or AbstractProductB will not be available to the client. MethodA and MethodB can be called, but MethodX and MethodY are unknown to the cleint. The client also never directly uses ConcreteFactory1 or ConcreteFactory2, rather using an abstract instance of their parent class, AbstractFactory.
Collapse public abstract class AbstractFactory { public abstract AbstractProductA CreateProductA(); public abstract AbstractProductB CreateProductB(); } public class ConcreteFactory1: AbstractFactory { public override AbstractProductA CreateProductA() { return new ProductA1(); } public override AbstractProdictB CreateProductB() { return new ProductB1(); } } public class ConcreteFactory2: AbstractFactory { public override AbstractProductA CreateProductA() { return new ProductA2(); } public override AbstractProductB CreateProductB() { return new ProductB2(); } } public abstract class AbstractProductA { public abstract void MethodA(); } public abstract class AbstractProductB { public abstract void MethodB(); } public class ProductA1: AbstractProductA { public override void MethodA() { Console.WriteLine("ProductA1.MethodA() Called"); } } public class ProductA2: AbstractProductA { public override void MethodA() { Console.WriteLine("ProductA2.MethodA() Called"); } } public class ProductB1: AbstractProductB { public override void MethodB() { Console.WriteLine("ProductB1.MethodB() Called"); } } public class ProductB2: AbstractProductB { public override void MethodB() { Console.WriteLine("ProductB2.MethodB() Called"); } } public class Client { public static void Main() { AbstractFactory factory; AbstractProductA prodA; AbstractProductB prodB; factory = new ConcreteFactoryA(); prodA = factory.CreateProductA(); prodB = factory.CreateProductB(); prodA.MethodA(); prodB.MethodB(); factory = new ConcreteFactoryB(); prodA = factory.CreateProductA(); prodB = factory.CreateProductB(); prodA.MethodA(); prodB.MethodB(); } }
The Class Factory: A .NET Improvement
While the Abstract Factory provides a very powerful framework for implicitly instantiating classes, its design is both arbitrary and rigid. The arbitrary nature of Abstract Factory allows the pattern to be used in any object oriented language, and its rigidity is a natural byproduct of that portability. In this article, I'll introduce the Class Factory, a design pattern based on the Abstract Factory, but tuned more to the .NET way of design and thinking. (NOTE: If any other patterns exist with the name "Class Factory" they are likely different than this pattern. This pattern is derived from a task I have had to go several times, and appropriately, I developed a pattern for that task.) The Class Factory will use many core .NET features such as reflection, interfaces, dynamic class creation, etc.
The goals of the Class Factory pattern are similar but also different than those of the Abstract Factory pattern. While the Abstract Factory aims to provide a way to instantiate objects implicityly from a strict class higherarchy, the Class Factory aims to provide a way to instantiate objects dynamically from an arbitrary class pool. Rather than requiring a specific root abstract class or classes, like the Abstract Factory, the Class Factory requires the implementation of one or more interfaces, using interface instances to allow access to each object, rather than an entire class instance. Another difference, and possibly an advantage, of the Class Factory is it requires no prior knowledge of the product classes whatsoever, relying on keys to instantiate classes. The goals of the Class Factory in .NET are:
- Provide true "oblivious instantiation", where the clients have zero advanced knowledge of the product classes they are using.
- Allow any class from any class higherarchy, from any location, to be used as a factory product.
- Use only interface instances, rather than complete class instances, in clients.
- Offer dynamic discovery, description, and instantiation of product classes.
- Maintain memory efficiency, global accessability, and simplicity through the Singleton pattern.
The structure of the Class Factory pattern is as follows:
The first thing you will notice is that there are no root abstract classes. The clients make use of one or more interfaces, as well any product class that is to be used within the class factory. The class factory itself implements another design pattern, the Singleton. Only a single instance of the class factory is available, rather than one for each class that may use the factory. (NOTE: If neccesary, the class factory could be created as a normal class, rather than a singleton). One or more create methods are implemented in the class factory, each one returning an instance of the appropriate interface. When the class factory is first created (first call to the Instance property), an internal initialization call is made to dynamically discover available classes at runtime.
The Class Factory pattern in .NET provides more flexability than the Abstract Factory pattern. In an abstract factory, it is neccesary to create concrete classes and factories to perform your class creation, which also requires some prior knowledge at some level of the classes that need to be created. Thanks to an innovation in .NET, and made available in C#, is the concept of an interface. Similar to a class, an interface defines the neccesary functionality that a class must impliment, without the actual implementation. In C#, an interface may be used directly, but can only be instantiated when cast from a class that implements it. This allows the following:
- Simpler implementation of required functionality, in either a single class or a class higherarchy. No need to define a complete, abstract root class.
- Less rigid class requirements. Since an interface is simply a subset of a classes full "public interface", the requirement of one or more root abstract classes is eliminated.
- More than one interface can be implemented in a single class, allowing one class to be used from more than one class factory. This allows theproduct to be used as neccesary in the future.
The Class Factory pattern in .NET performs its object creation using Reflection, based on a key. The Reflection feature of .NET allows types stored in an assembly to be dynamically discovered, probed, and instantiated using the Assembly class, making the goal of true "oblivious instantiation" possible. The use of a key to publicly define, or name, product classes allows them to be discovered and created with the factory in any number of ways. Those ways can easily be extended by adding additional creation methods that use different types of keys to the factory. Depending on how you may need to use the class factory, a single product class can be defined with multiple keys, or may even be defined through some external means, such as an XML factory configuration file.
Through use of several advanced .NET features, and some of .NET's advanced object oriented technologies like interfaces, developing truely extensible systems that can be built modularly, quickly, and without prior knowledge of possible future enhancements, is within the realm of possability. The goals of oblivious instantiation, flexible class higherarchy structure, dynamic discovery and instantiation, and memory efficiency are easily attainable when you have a predefined template to work from.
Implementing the Pattern: ClassFactory class
A design pattern, while at best is just a definition, is quite useless unless its implemented. The Class Factory pattern, while looking somewhat intimidating, is quite simple to implement. This article will develop a theoretical class library that matches the diagram above, to help guide the user through the details of creating all the neccesary parts. As the core component of this pattern is the ClassFactory class itself, we'll start there. After we have developed a shell factory class, and implemented the Singleton pattern, we'll add the neccesary interfaces and product classes. Once those are complete, we'll move on to developing the client classes, and provide some examples as to how the factory can be used.
Before we start the ClassFactory class, you should know a little about the Singleton pattern. This pattern is extremely useful for utility classes or in situations where you need global access to a single object. The ClassFactory is, in essence, a utility class, facilitating the creation of objects by client. A Singleton, while a simple pattern, is difficult to create in most languages. One of the few languages that directly supports the creation of singletons is C#. Take a look at the following code:
Collapse public class Singleton { private static volatile Singleton m_instance; private static object m_syncRoot = new object(); public static Singleton Instance { get { if (m_instance == null) { lock (m_syncRoot) { if (m_instance == null) { m_instance = new Singleton(); } } } return m_instance; } } }
You will notice that the instance variable, syncroot object, and Instance property are all static, marking them as classifier-level members, rather than instance members. The instance variable, m_instance, is also marked as volatile, meaning that the .NET runtime guarantees the variable will always be up to date, and is always immediately written from any level of cache back to main system memory. In any multi-threaded environment, a non-volatile variable may be stored in any level of cache, allowing stale instances of that variable to be read from system memory by other threads. In a singleton, there must be no chance that a stale copy of the variable is read from anywhere.
Our class factory is, for memory efficiency and performance purposes, a singleton object. The pattern just described above will also be used in our class factory. When our class factory instance is created, we must initialize the factory, finding and describing all the possible product classes that can be created with it. The initialization routeen uses Reflection to retrieve an instance of the current assembly (the one that contains the currently executing code, which would be the Initialize()
method of our ClassFactory class). The Assembly class allows dynamic discovery of all its content through probing the assemblies metadata. The initialization gets a list of all defined types, and checkes each non-abstract class for implementations of the neccesary interfaces. Any classes that implement IFactoryProduct
and one or more of the IProductX
interfaces will be mapped in the factory by their key. The base ClassFactory class is defined below, without any of its create methods:
Collapse using System; using System.Collections.Specialized; using System.Reflection; public class ClassFactory { #region Singleton Pattern private static volatile ClassFactory m_instance; private static object m_syncRoot = new object(); public static ClassFactory Instance { get { if (m_instance == null) { lock (m_syncRoot) { if (m_instance == null) { m_instance = new Singleton(); } } } return m_instance; } } #endregion #region Constructor private ClassFactory() { m_aProducts = new ListDictionary(); m_bProducts = new ListDictionary(); m_cProducts = new ListDictionary(); Initialize(); } #endregion #region Variables private ListDictionary m_aProducts; private ListDictionary m_bProducts; private ListDictionary m_cProducts; private Assembly m_asm; #endregion #region Methods #endregion #region Helpers private void Initialize() { Assembly asm = Assembly.GetCallingAssembly(); Type[] allTypes = asm.GetTypes(); foreach (Type type in allTypes) { if (type.IsClass && !type.IsAbstract) { Type iFactoryProduct = type.GetInterface("IFactoryProduct"); if (iFactoryProduct != null) { object inst = asm.CreateInstance(type.FullName, true, BindingFlags.CreateInstance, null, null, null, null); if (inst != null) { IFactoryProduct keyDesc = (IFactoryProduct)inst; object key = keyDesc.GetFactoryKey(); inst = null; Type prodInterface = type.GetInterface("IProductA"); if (prodInterface != null) { m_aProducts.Add(key, type); } prodInterface = type.GetInterface("IProductB"); if (prodInterface != null) { m_bProducts.Add(key, type); } prodInterface = type.GetInterface("IProductC"); if (prodInterface != null) { m_cProducts.Add(key, type); } } } } } m_asm = asm; } #endregion }
Once a map of all the available types has been created, that map can be used with a key object to create an instance of the appropriate class. Once the class is created, an instance of the neccesary interface can be retrieved and returned for use by a client class. Below are the threeCreateProductX
methods of the ClassFactory
class. All three methods use pretty much the same code, with the exception of theListDictionary
collection used, and the interface instance returned. At first glance, many might wonder whether the inst created with Assembly.CreateInstance will stick around when the method returns. As long as there is any reference to a reference type in a .NET application, the garbage collector will keep the object in memory. While you are only returning an interface, essentially a subset of the whole inst object, that interface still points to the same object on the heap. Only when the instance of the interface is destroyed will the objects memory be recovered by the garbage collector.
Collapse public class ClassFactory { #region Methods public IProductA CreateProductA(object key) { if (key == null) throw new NullReferenceException("Invalid key supplied, must be " +
"non-null."); Type type = (Type)m_aProduct[key]; if (type != null) { object inst = m_asm.CreateInstance(type.FullName, true,
BindingFlags.CreateInstance, null, null, null, null); if (inst == null) throw new NullReferenceException("Null product instance. " +
"Unable to create neccesary product class."); IProductA prod = (IProductA)inst; return prod; } else { return null; } } public IProductB CreateProductB(object key) { if (key == null) throw new NullReferenceException("Invalid key supplied, must be " +
"non-null."); Type type = (Type)m_bProduct[key]; if (type != null) { object inst = m_asm.CreateInstance(type.FullName, true,
BindingFlags.CreateInstance, null, null, null, null); if (inst == null) throw new NullReferenceException("Null product instance. " +
"Unable to create neccesary product class."); IProductB prod = (IProductB)inst; return prod; } else { return null; } } public IProductC CreateProductC(object key) { if (key == null) throw new NullReferenceException("Invalid key supplied, must " +
"be non-null."); Type type = (Type)m_cProduct[key]; if (type != null) { object inst = m_asm.CreateInstance(type.FullName, true,
BindingFlags.CreateInstance, null, null, null, null); if (inst == null) throw new NullReferenceException("Null product instance. " +
"Unable to create neccesary product class."); IProductC prod = (IProductC)inst; return prod; } else { return null; } } #endregion }
Implementing the Pattern: Support Facilities
In addition to the ClassFactory itself, several interfaces and all the product classes must be implemented. We'll create the neccesary interfaces first. In C# (and .NET in general), an interface is like a class, but much simpler. Interfaces can not have constructors, and the methods defined in them can not be implemented. Very similar to a function prototype in C/C++, a definition before implementation. Any class that implements the interface must include it in its inheritance list.
Collapse public interface IFactoryProduct { object GetFactoryKey(); } public interface IProductA { void MethodA(); } public interface IProductB { void MethodB1(); string MethodB2(); } public interface IProductC { void MethodC(string msg); }
As you can see, the interfaces are quite simple. Implementing them is nearly as simple, and the explicit implementation can differ as neccesary between classes. Below are the implementations of all the products specified in the Class Factory diagram above. For the purposes of this article, we will use a floating point key for all of the product classes. Each class will only perform a Debug.WriteLine, but in a practical situation, the methods would perform some sort of useful operations on input data, or would output some kind of result based on a variety of criteria.
Collapse public class ProductA1: IFactoryProduct, IProductA { public ProductA1() { } #region IFactoryProduct Implementation public object GetFactoryKey() { return 1.0f; } #endregion #region IProductA Implementation public virtual void MethodA() { Debug.WriteLine("ProductA1.MethodA() was called!"); } #endregion } public class ProductA2B: ProductA1, IFactoryProduct, IProductA, IProductB { product ProductA2B() { } #region IFactoryProduct Implementation public object GetFactoryKey() { return 1.1f; } #endregion #region IProductA Implementation public override void MethodA() { Debug.WriteLine("ProductA2B.MethodA() was called."); } #endregion #region IProductB Implementation public virtual void MethodB1() { Debug.WriteLine("ProductA2B.MethodB1() was called."); } public virtual string MethodB2() { return "ProductA2B.MethodB2() was called."); } #endregion } public class ProductB: IFactoryProduct, IProductB { public ProductB() { } #region IFactoryProduct Implementation public object GetFactoryKey() { return 1.2f; } #endregion #region IProductB Implementation public virtual void MethodB1() { Debug.WriteLine("ProductB.MethodB1() was called."); } public virtual string MethodB2() { return "ProductB.MethodB2() was called."); } #endregion } public class ProductC: IFactoryProduct, IProductC { public ProductC() { } #region IFactoryProduct Implementation public object GetFactoryKey() { return 1.3f; } #endregion #region IProdctC Implementation public void MethodC(string msg) { Debug.WriteLine("ProductC.MethocC()::Printing: " + msg); } #endregion } public class ProductABC: IFactoryProduct, IProductA, IProductB, IProductC { public ProductABC() { } #region IFactoryProduct Implementation public object GetFactoryKey() { return 1.4f; } #endregion #region IProductA Implementation public override void MethodA() { Debug.WriteLine("ProductABC.MethodA() was called."); } #endregion #region IProductB Implementation public virtual void MethodB1() { Debug.WriteLine("ProductABC.MethodB1() was called."); } public virtual string MethodB2() { return "ProductABC.MethodB2() was called."); } #endregion #region IProdctC Implementation public void MethodC(string msg) { Debug.WriteLine("ProductABC.MethodC()::Printing: " + msg); } #endregion }
The important thing to notice here is any class can become a product of the factory. Provided the class implements the neccesary interfaces, it can be a lone class, part of a class higherarchy. The class can reside locally, or in another assembly, or can be spread out accross multiple assemblies. The class factory could even be extended to allow dynamic discovery of remote services, such as web services, remoting objects, etc. There are no required base abstract classes, so any class anywhere can become a product of the class factory, making it very flexible, and 100% dynamic.
Implementing the Pattern: Client Classes
Now that we have the framework of our class factory in place, we need to develop some clients to make use of it. In a real-world situation, you will most probably have an existing system that needs to be extended. That system will have certain rules that gouvern it, certain patterns that repeatedly appear. Every system processes data, each one in a unique way. For the purposes of this article, lets assume our pre-existing system is a simple number processing system. Our clients will process floating point numbers, and depending on the number, different things should happen. We can process each number in a standard way in the client class itself, but in the future, custom actions may be neccesary when certain numbers are encountered. We design our system with a default handler built into the client class, and allow future extensability through a class factory (which we just built).
We will build two clients, for this example. Our class factory is a singleton, which allows a single global instance to be used from anywhere. Our first client, ClientX, uses products A and C, while our second client, ClientY, uses products B and C. The two clients only know about the products, and require no detailed knowledge about the classes they will be using. When certain numbers are processed, the clients will call the class factory with the current number as the key, retrieving a product, or a null if no product exists. Depending on the current number, different classes will be instantiated when a product is retrieved, or possibly none, if there is no class with a matching key.
Collapse public class ClientX { #region Constructor public ClientX(float start, float end) { m_start = start; m_end = end; } #endregion #region Variables private float m_start, m_end; #endregion #region Methods public void Process() { ClassFactory fact = ClassFactory.Instance; for (float cur=m_start; cur <= m_end; cur += 0.1) { Debug.WriteLine("ClientX: Processing number " + cur.ToString()); IProductA prodA = fact.CreateProductA(cur); if (prodA != null) { prodA.MethodA(); } else { Debug.WriteLine("Default processing performed on number "
+ cur.ToString()); } IProductC prodC = fact.CreateProductC(cur); if (prodC != null) { prodC.MethodC("Floating-point number " + cur.ToString() +
" has been logged."); } } } #endregion } public class ClientY { #region Constructor public ClientY() { } #endregion #region Methods public void Process(float Max) { float cur = 1.0; ClassFactory fact = ClassFactory.Instance; while (cur <= Max) { Debug.WriteLine("ClientY: Processing number " + cur.ToString()); IProductB prodB = fact.CreateProductB(cur); if (prodB != null) { prodB.MethodB1(); Debug.WriteLine("Result from ProductB.MethodB2(): " +
prodB.MethodB2()); } else { Debug.WriteLine("Number " + cur.ToString() +
" was not processed."); } IProductC prodC = fact.CreateProductC(cur); if (prodC != null) { prodC.MethodC("Floating-point number " + cur.ToString() +
" has been logged."); } cur += 0.1; } } #endregion }
Now that we have the class factory, we can add product classes for any floating point number. We currently have products for numbers 1.0-1.4, but that range could be expanded to cover 1.0-2.0, 0.0-50.0, whatever is neccesary. The extensability of our floating point processing clients is now endless, and can easily and readily be extended at any time. The power of a class factory is expansive, and can help simplify large projects into smaller, more manageable parts.
Points of Interest
This article introduced a new design pattern, the Class Factory, and explained its usefulness in a practical environment. So far, you have seen what the Class Factory is, and how to implement it in a very simple, basic environment. While the example provided covers all the details, there are many more possabilities with this pattern that have yet to be explored. The example provided works in a single-threaded environment, and workes with a single key per product class. That provides flexability for the example depicted, but it may not be flexible enough for every system.
To extend this pattern so it works in a multi-threaded environment, a small and simple addition to the ClassFactory class will suffice. We already have a synchroniztion root object, the static m_syncRoot variable. Adding a SyncRoot property to the class will allow lock() to be called on the instance of ClassFactory, from any thread before calling a Create method. Since lock is a blocking call, only one thread at a time can retrieve a product. The example below depicts:
Collapse public class ClassFactory { private static object m_syncRoot = new object(); public object SyncRoot { get { return m_syncRoot; } } } ClassFactory fact = ClassFactory.Instance; IProductA prodA; lock (fact.SyncRoot) { prodA = fact.CreateProductA(); }
Another change that can be made to the pattern is the use of multiple keys for each product. This can either be a simple matter of changing the IFactoryProduct interfaces method, object GetFactoryKey(), to object[] GetFactoryKeys(). If hard-coded key mappings are not what you need for your project, its easy enough to develop an external XML format to describe keys, assembly locations, and more to product classes. The Initialize routeen of the class factory would then load and process the xml file, finding all the neccesary types based on their assembly locations (i.e. the path to a .dll, web service locations, etc.).
The class factory can also be extended to provide not only instantiated objects, but also information about the objects, or even more detailed knowledge about the actual class that implemented the interface. It all depends on your needs, the existing system, what kinds of new functionality the system needs, and how you map keys to products. The usefulness of a class factory extends beyond just providing easy extensability to existing systems. New systems or applications can be designed eith class factories in mind from the start, building in an innate extensability. Class Factories can be used as the core element of a plugin system for an application, too (see http://www.datgen.info/ for a practical example of a program I've written that makes use of class factories for the programs plugin and dynamic type system).
Ultimately, class factories provide a level of application or system extensability that is otherwise not possible. The use of interfaces, rather than explicit abstract classes, makes the class factory an extremely flexible pattern. Implementation of a required interface in a previously existing class immediately makes that class available through the class factory as a product, allowing already existing functionality to be integrated with the system in question. The system itself requires minimal modification, and the goal of true oblivious instantiation is possible. Disconnected and uninformed execution of dynamically discovered code is now within the realm of reality.
History
This article is largely conceptual in topic, and will probably have few updates. For those who like to see a changelog, though, here ya go:
- January 16, 2005 - Initial Article Version, Class Factory for .NET