Pages

Friday, July 13, 2007

The Factory pattern with Attributes

I've been using the Factory design pattern for some time, it's nice and simple, creating different objects depending on what parameters you pass to a given 'Create' function. The only problem is that for most of the situations that I come across, all the Create function seems to do is contain a switch case statement, which is rather in-elegant and not really scalable.


public Cheese CreateCheese(string cheeseType)
{
Cheese newCheese = null;

switch (cheeseType)
{
case "Cheddar":
newCheese = CheddarCheese();
break;
case "Edam":
newCheese = EdamCheese();
break;
}

return newCheese;
}

The problem obviously is that (in this case) if you now wanted to add a new type of cheese, that you would have to add a new case statement to the list.

I recently found a few good examples of how to get around this problem using Reflection and custom Attributes, I especially liked the Implementing the factory pattern using attributes and activation solution by John Gunnarsson.

However, what I didn't like about this was that you swapped your switch case statement for a series of statements adding types to a collection:

factory.RegisterCheeseType(typeof(CheddarCheese));
factory.RegisterCheeseType(typeof(EdamCheese));

My alternate solution is to store this list a custom section within the config file. This way you can define the classes that the Factory can create outside the Factory Assembly and reference them in the config file. This is a good thing because it provides an extensible architecture allowing other developers to extend the functionality without recompiling the main assembly

The Factory class would now look like this:

CheeseFactory.cs



public class CheeseFactory
{
private CheeseFactorySettings settings;

public CheeseFactory()
{
settings = (CheeseFactorySettings)ConfigurationSettings.GetConfig("cheeseFactory");
}

public Cheese CreateCheese(string cheeseType)
{
return settings.GetCheese(cheeseType);
}
}


What we're doing here is to pass the actual decision as to which class to make to this CheeseFactorySettings class. You could argue that we're duplicating the 'Create' Factory method in both the CheeseFactorySettings class and the CheeseFactory class, and that that's a bad thing. If you think that smells a bit too much, then just stick the collection traversing logic in the CheeseFactorySettings.GetCheeseFactory() method into the CheeseFactory.CreateCheese() method. In any case, the CheeseFactorySettings class is defined below:

CheeseFactorySettings.cs



public class CheeseFactorySettings : CollectionBase
{
public CheeseFactorySettings(XmlNode section)
{
foreach (XmlNode node in section.ChildNodes)
{
if (node.Name == "formatter")
{
List.Add(Type.GetType(node.Attributes["type"].InnerText));
}
}
}

public Cheese GetCheeseFactory(string format)
{
foreach (Type implementation in List)
{
CheeseFactoryAttribute[] attributeList = (CheeseFactoryAttribute[])implementation.GetCustomAttributes(typeof(CheeseFactoryAttribute), true);

foreach (object attribute in attributeList)
{
if (attribute is CheeseFactoryAttribute)
{
if (((CheeseFactoryAttribute) attribute).Format == format)
{
return (Cheese) Activator.CreateInstance(implementation);
}
else
{
break;
}
}
}
}

throw new InvalidCastException("Could not find a Cheese implementation for this format");
}
}


A possible addition you could do to this class is to implement the OnValidate method of the CollectionBase class to prevent classes that do not inherit from Cheese from being added to the collection of types. (The base.OnValidate call checks to see if the newly added value is not a null reference).


protected override void OnValidate(object value)
{
base.OnValidate(value);

if (!(value as Type).IsSubclassOf(typeof(Cheese)))
{
throw new InvalidCastException("Cheese types must inherit from Cheese");
}
}


There's a couple more final classes to go before the whole barebones of the solution is complete. Firstly the IConfigurationSectionHandler implementation to read the values from the config file at runtime. This is pretty standard stuff:

CheeseFactorySectionHandler.cs



public class CheeseFactorySectionHandler : IConfigurationSectionHandler
{
public object Create(object parent, object configContext, XmlNode section)
{
CheeseFactorySettings settings = new CheeseFactorySettings(section);

return settings;
}
}


All we're doing is creating a new CheeseFactorySettings class and passing through the XmlNode from the config file to it's constructor.