Introduction
The abstract factory provides an interface for creating families of related dependent objects without specifying their concrete classes. Suppose you have a great number of concrete factories, and the user is given the ability to choose which type of concrete product they would like to use at runtime. You'll need an efficient way to determine the correct factory to use, in order to create the desired and related types. This article shows how to develop a small utility to manage the creation of objects using the abstract factory pattern and custom attributes.
Background
Context
The reason for developing this code was to eliminate or at least significantly reduce the amount of coding to be done whenever a new concrete factory class is added to a project. I also needed a way so users could choose the type they wished to create without knowing how the specific object is created. How do we determine which factory creates the type the user has chosen? Of course! A giant switch
statement... No, that just won't do, that would just be too tedious and require an additional case
statement for every new factory. What we really need is a custom attribute for factories which tell us the types they can create. Yeah, that would be nifty.
The Interfaces
Consider a system composed of data objects that have corresponding controls. An abstract factory for such a system might look like so:
public interface IBeerFactory
{
IBeerIngredient CreateIngredient();
IBeerControl CreateControl();
}
So now we know that all types implementing IBeerFactory
must create both Controls and Ingredients, we had better define these as well:
public interface IBeerIngredient
{
string Name { get;set; }
double Amount { get;set; }
string Description { get;set; }
}
public interface IBeerControl
{
void UpdateControl( IBeerIngredient ingr );
void UpdateIngredient( IBeerIngredient ingr );
}
The Code
Of course, we will need to go on and develop some concrete factories, ingredients, and controls. For instance, there could be a set of classes for Hops, Barley, and Yeast. To keep things short, I'll show the code for the Hops
ingredient, control and factory. Then we'll be ready to get to work on the Factory Manager.
public class Hops : IBeerIngredient
{
private string mName;
private string mDesc;
private double mAmt;
internal Hops()
{
mName = "";
mDesc = "";
mAmt = 0.0;
}
public string Name
{
get
{
return mName;
}
set
{
mName = value;
}
}
public double Amount
{
get
{
return mAmt;
}
set
{
mAmt = value;
}
}
public string Description
{
get
{
return mDesc;
}
set
{
mDesc = value;
}
}
}
public class HopsControl : IBeerControl, UserControl
{
private TextBox txtName;
private TextBox txtDesc;
private TextBox txtAmt;
internal HopsControl()
{
Initialize();
}
private void Initialize()
{
txtName = new TextBox();
txtName.Parent = this;
txtDesc = new TextBox();
txtDesc.Parent = this;
txtAmt = new TextBox();
txtAmt.Parent = this;
}
public void UpdateControl(IBeerIngredient ingr)
{
Hops hops = ingr as Hops;
if( hops != null )
{
txtName.Text = hops.Name;
txtDesc.Text = hops.Description;
txtAmt.Text = hops.Amount;
}
}
public void UpdateIngredient(IBeerIngredient ingr)
{
Hops hops = ingr as Hops;
if( hops != null )
{
hops.Name = txtName.Text;
hops.Description = txtDesc.Text;
hops.Amount = Convert.ToDouble( txtAmt.Text );
}
}
}
public class HopsFactory : IBeerFactory
{
public IBeerIngredient CreateIngredient()
{
return new Hops();
}
public IBeerControl CreateControl()
{
return new HopsControl();
}
}
Okay, that was simple enough. So now we will turn to Custom Attributes so our factory manager will be able to determine what can be created by the concrete factories. We'll definitely need something generic because I too am lazy and never want to reproduce this code again. What came to mind was an Attribute that stores all the types a factory can create. Let's call it CreatesAttribute
. Like any custom attribute, it must inherit from System.Attribute
.
public class CreatesAttribute : System.Attribute
{
private Type [] mCreatableTypes;
}
Custom attributes also need to tell the compiler how it can be used so the correct meta data will be emitted into the created assembly. This is done by using the AttributeUsageAttribute
.
[AttributeUsage( AttributeTargets.Class, Inherited=false, AllowMultiple=true )]
public class CreatesAttribute : System.Attribute
{
private Type [] mCreatableTypes;
}
So our custom attribute can be placed on classes, it will not be inherited by any sub classes, and we will allow multiple CreatesAttribute
to be specified for the factories. Sounds good? ...Cool!
No doubt we'll need some constructors and a way to access the creatable types. How about creating two constructors, one that will take an array of types and another taking a single type. As for accessing, let's just make a simple little get
property.
[AttributeUsage( AttributeTargets.Class, Inherited=false, AllowMultiple=true )]
public class CreatesAttribute : System.Attribute
{
private Type [] mCreatableTypes;
public CreatesAttribute( Type type )
{
mCreatableTypes = new Type [] { type };
}
public CreatesAttribute( Type [] types )
{
mCreatableTypes = types;
}
public Type [] CreatableTypes
{
get
{
return mCreatableTypes;
}
}
}
Don't forget to go and mark the concrete factories with our new custom attribute. Use either of the following ways:
[Creates( new type [] { typeof(Hops), typeof(HopsControl) } )]
public class HopsFactory : IBeerFactory
or
[Creates( typeof(Hops) )]
[Creates( typeof(HopsControl) )]
public class HopsFactory : IBeerFactory
Okay, okay, I know I've been yacking on long enough so let's get to the good stuff. The FactoryManager
is the class that will manage the retrieval of the concrete classes implementing the IBeerFactory
interface. Yet that still isn't good enough. Let's design it so that it will manage any possible factory interface that we'll ever create.
using System;
using System.Collections;
using System.Reflection;
public sealed class FactoryManager
{
private Type mFactoryInterface;
private SortedList mFactories;
public FactoryManager( Type factoryInterface )
{
mFactoryInterface = factoryInterface;
mFactories = new SortedList();
}
You may be wondering how we are going to retrieve all of our concrete factories and place them in the sorted list. Well, let's reflect on that a little bit (pun intended, ha ha ha, very lame). Because we may not know where all the factories exist, let's give that choice to the developer by making them load the appropriate assembly.
public void LoadFactoriesFromAssembly( Assembly assembly )
{
Type [] allTypes = assembly.GetTypes();
foreach( Type type in allTypes )
{
Type intrface = type.GetInterface(mFactoryInterface.FullName);
if( intrface != null )
{
object [] attribs =
type.GetCustomAttributes( typeof(CreatesAttribute), false );
object factory;
if( attribs.Length != 0 )
{
factory = assembly.CreateInstance( type.FullName );
foreach( CreatesAttribute createsAttrib in attribs )
{
Type [] createsTypes = createsAttrib.CreatableTypes;
foreach( Type creatable in createsTypes )
{
mFactories[creatable.FullName] = factory;
}
}
}
}
}
}
Wow! That's pretty cool isn't it? Well I think so anyway. Of course we better finish up with a couple functions that allow us to get the factories.
public object GetFactory( string fullname )
{
if( mFactories.ContainsKey( fullname )
return mFactories[fullname];
return null;
}
public object GetFactory( Type type )
{
return GetFactory( type.FullName );
}
}
Finally, I'll show a simple example of the FactoryManager
at work.
public class Example
{
public static void Main()
{
FactoryManager mgr = new FactoryManager( typeof(IBeerFactory) );
Console.WriteLine( "Loading Assembly: AbstractFactoryManager");
mgr.LoadFactoriesFromAssembly( mgr.GetType().Assembly );
Console.WriteLine( "Retreiving a HopsFactory");
IBeerFactory factory = (IBeerFactory)mgr.GetFactory( typeof(Hops) );
Console.WriteLine( "IBeerFactory factory is of type {0}\n",
factory.GetType() );
Console.WriteLine( "Creating an Ingredient.");
IBeerIngredient ingr = factory.CreateIngredient();
Console.WriteLine( "Creating a Control.");
IBeerControl ingrCtrl = factory.CreateControl();
Console.WriteLine( "IBeerIngredient ingr is of type {0}",
ingr.GetType() );
Console.WriteLine( "IBeerControl ingrCtrl is of type {0}\n\n",
ingrCtrl.GetType() );
Console.WriteLine( "Retrieving a YeastFactory");
factory = (IBeerFactory)
mgr.GetFactory( "AbstractFactoryManager.YeastControl" );
Console.WriteLine( "IBeerFactory factory is of type {0}\n",
factory.GetType() );
Console.WriteLine( "Creating an Ingredient.");
ingr = factory.CreateIngredient();
Console.WriteLine( "Creating a Control.");
ingrCtrl = factory.CreateControl();
Console.WriteLine( "IBeerIngredient ingr is of type {0}",
ingr.GetType() );
Console.WriteLine( "IBeerControl ingrCtrl is of type {0}\n",
ingrCtrl.GetType() );
}
}
Points of Interest
The FactoryManager
class can be very helpful, and can be used in any project where there is a need for the abstract factory design pattern. I hope this will help a few developers out there save some time. Enjoy!