In almost all projects in which I participated at some point there was a need for a factory implementation.
There is already a pattern concept about factory, but it is not always the best solution. The solution proposed below is a particular approach (general and flexible) of the pattern concept.
The purpose of this solution was not to implement the original pattern, but to give an alternative implementation based on some of the pattern's exposed principles.
With this current factory implementation the idea is to have a base class and derived classes so that at some point to be able to get the instance of a derived class based on an identifier (not the class type, a label).
To exemplify such a need here is a small example: we have a base class called ImageBase and the derived classes called JPEG, GIF, etc. We have the image stored somewhere and we need to load it based on a label/identifier. It’s identifier comes from a different source (database, config file..). A quick solution is to make a method that will return an instance of the ImageBase class based on a parameter:
public static class Factory
{
public static ImageBase Create(ImageType type)
{
switch (type)
{
case ImageType.BMP:
return new ImageBMP();
case ImageType.GIF:
return new ImageGIF();
case ImageType.JPEG:
return new ImageJPEG();
default:
return null;
}
}
}
This seems to be a good and apparently quick solution, but when we want to support new image types this method must be modified in order to support/include the new types.
The solution I propose is based on labeling implementation classes with a custom Attribute for specifying implementation type (JPEG, GIF). When the factory is called it will receive a label and will return the instance of the corresponding implementation (marked with the label given as parameter).
public abstract class Factory<TDescriber, TBase>
{
// list of assemblies to search in for implemented classes
List<Assembly> searchAssemblies;
//cached implementations
Dictionary<TDescriber, RuntimeTypeHandle> implementationList;
readonly object cacheLock = new object();
protected Factory()
{
searchAssemblies = new List<Assembly>();
}
public abstract TBase Create(TDescriber label);
protected TBase CreateImplementation(TDescriber label)
{
Type implementation = GetImplementationType(label);
return (TBase)Activator.CreateInstance(implementation);
}
protected Type GetImplementationType(TDescriber label)
{
RuntimeTypeHandle handle = GetImplementationFromCache(label);
return Type.GetTypeFromHandle(handle);
}
RuntimeTypeHandle GetImplementationFromCache(TDescriber label)
{
if (implementationList == null)
{
lock (cacheLock)
{
if (implementationList == null)
{
implementationList = new Dictionary<TDescriber, RuntimeTypeHandle>();
//if no assemblies are defined for search add executing assembly
if (searchAssemblies.Count == 0)
searchAssemblies.Add(Assembly.GetExecutingAssembly());
BuildCache();
}
}
}
if (implementationList.ContainsKey(label))
{
return implementationList[label];
}
else
{
throw new Exception("Cannot find type");
}
}
protected List<Assembly> SearchAssemblies
{
get { return searchAssemblies; }
}
void BuildCache()
{
Type attributeType = typeof(FactoryLabelAttribute);
Type baseType = typeof(TBase);
bool isBaseInterface = baseType.IsInterface;
//search all assemblies and types
var implementations = from assembly in searchAssemblies
from selType in assembly.GetTypes()
let attribute = Attribute.GetCustomAttribute(selType, attributeType) as FactoryLabelAttribute
where (attribute != null && attribute.Label is TDescriber) && (isBaseInterface ? selType.GetInterfaces().Contains(baseType) : selType.IsSubclassOf(baseType))
select new { Type = selType, Describer = attribute.Label };
//add results to cache
foreach (var foundImplementation in implementations)
implementationList.Add((TDescriber)foundImplementation.Describer, foundImplementation.Type.TypeHandle);
}
}
Some observations about this solution:
- Each implementation will add the FactoryLabel attribute which will specify the type that is implemented and derived from the base class.
- CreateImplementation method returns an instance of the implementation matching the label parameter.
- GetImplementationType method returns the type of the implementation which matches the label parameter
- When CreateImplementation or GetImplementationType is called first time a dictionary of Label – Type will be built. This dictionary acts like a cache for further requests.
- Implementation classes can be present in different assemblies. In this case a list of containing assemblies can be passed to factory. This list of assemblies is used to search for implementations. If no assembly is pointed, executing assembly is used to search for implementations.
- RunTypeHandler is used instead of Type, because for caching purpose is more lightweight than the Type.
- FactoryLabelAttribute takes an object for implementation identification. Practically this should be a value type (int, double, enum).
Bellow you can find some samples of this approach:
public abstract class ImageBase
{
public abstract void Test();
}
public enum ImageType
{
GIF,
BMP,
JPEG
}
[FactoryLabel(ImageType.JPEG)]
public class ImageJPEG : ImageBase
{
public override void Test()
{
Console.WriteLine("JPEG type");
}
}
[FactoryLabel(ImageType.BMP)]
public class ImageBMP : ImageBase
{
public override void Test()
{
Console.WriteLine("BMP type");
}
}
[FactoryLabel(ImageType.GIF)]
public class ImageGIF : ImageBase
{
public override void Test()
{
Console.WriteLine("GIF type");
}
}
public class ImageFactory : Factory<ImageType, ImageBase>
{
static readonly ImageFactory instance = new ImageFactory();
public static ImageFactory Instance { get { return instance; } }
private ImageFactory()
{
SearchAssemblies.Add(typeof(ImageBMP).Assembly);
}
public override ImageBase Create(ImageType label)
{
return CreateImplementation(label);
}
}
static void DoTestFactory()
{
ImageBase gif = ImageFactory.Instance.Create(ImageType.GIF);
gif.Test();
ImageBase bmp = ImageFactory.Instance.Create(ImageType.BMP);
bmp.Test();
ImageBase jpeg = ImageFactory.Instance.Create(ImageType.JPEG);
jpeg.Test();
}
This might not be the perfect solution but is a point of start in creating a generic factory adapted to your needs.