Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Abstract Factory Design Pattern

I'm working on an internal project for my company, and part of the project is to be able to parse various "Tasks" from an XML file into a collection of tasks to be ran later.

Because each type of Task has a multitude of different associated fields, I decided it would be best to represent each type of Task with a seperate class.

To do this, I constructed an abstract base class:

public abstract class Task
{
    public enum TaskType
    {
        // Types of Tasks
    }   

    public abstract TaskType Type
    {
        get;
    }   

    public abstract LoadFromXml(XmlElement task);
    public abstract XmlElement CreateXml(XmlDocument currentDoc);
}

Each task inherited from this base class, and included the code necessary to create itself from the passed in XmlElement, as well as serialize itself back out to an XmlElement.

A basic example:

public class MergeTask : Task
{

    public override TaskType Type
    {
        get { return TaskType.Merge; }
    }   

    // Lots of Properties / Methods for this Task

    public MergeTask (XmlElement elem)
    {
        this.LoadFromXml(elem);
    }

    public override LoadFromXml(XmlElement task)
    {
        // Populates this Task from the Xml.
    }

    public override XmlElement CreateXml(XmlDocument currentDoc)
    {
        // Serializes this class back to xml.
    }
}

The parser would then use code similar to this to create a task collection:

XmlNode taskNode = parent.SelectNode("tasks");

TaskFactory tf = new TaskFactory();

foreach (XmlNode task in taskNode.ChildNodes)
{
    // Since XmlComments etc will show up
    if (task is XmlElement)
    {
        tasks.Add(tf.CreateTask(task as XmlElement));
    }
}

All of this works wonderfully, and allows me to pass tasks around using the base class, while retaining the structure of having individual classes for each task.

However, I am not happy with my code for TaskFactory.CreateTask. This method accepts an XmlElement, and then returns an instance of the appropriate Task class:

public Task CreateTask(XmlElement elem)
{
    if (elem != null)
    {
        switch(elem.Name)
        {
            case "merge":
                return new MergeTask(elem);
            default:
                throw new ArgumentException("Invalid Task");
        }
    }
}

Because I have to parse the XMLElement, I'm using a huge (10-15 cases in the real code) switch to pick which child class to instantiate. I'm hoping there is some sort of polymorphic trick I can do here to clean up this method.

Any advice?

like image 952
FlySwat Avatar asked Aug 26 '08 02:08

FlySwat


4 Answers

I use reflection to do this. You can make a factory that basically expands without you having to add any extra code.

make sure you have "using System.Reflection", place the following code in your instantiation method.

public Task CreateTask(XmlElement elem) {     if (elem != null)     {          try         {           Assembly a = typeof(Task).Assembly           string type = string.Format("{0}.{1}Task",typeof(Task).Namespace,elem.Name);            //this is only here, so that if that type doesn't exist, this method           //throws an exception           Type t = a.GetType(type, true, true);            return a.CreateInstance(type, true) as Task;         }         catch(System.Exception)         {           throw new ArgumentException("Invalid Task");         }     } } 

Another observation, is that you can make this method, a static and hang it off of the Task class, so that you don't have to new up the TaskFactory, and also you get to save yourself a moving piece to maintain.

like image 183
DevelopingChris Avatar answered Sep 23 '22 00:09

DevelopingChris


Create a "Prototype" instanace of each class and put them in a hashtable inside the factory , with the string you expect in the XML as the key.

so CreateTask just finds the right Prototype object, by get() ing from the hashtable.

then call LoadFromXML on it.

you have to pre-load the classes into the hashtable,

If you want it more automatic...

You can make the classes "self-registering" by calling a static register method on the factory.

Put calls to register ( with constructors) in the static blocks on the Task subclasses. Then all you need to do is "mention" the classes to get the static blocks run.

A static array of Task subclasses would then suffice to "mention" them. Or use reflection to mention the classes.

like image 39
Tim Williscroft Avatar answered Sep 25 '22 00:09

Tim Williscroft


How do you feel about Dependency Injection? I use Ninject and the contextual binding support in it would be perfect for this situation. Look at this blog post on how you can use contextual binding with creating controllers with the IControllerFactory when they are requested. This should be a good resource on how to use it for your situation.

like image 29
Dale Ragan Avatar answered Sep 27 '22 00:09

Dale Ragan


@Tim, I ended up using a simplified version of your approach and ChanChans, Here is the code:

public class TaskFactory
    {
        private Dictionary<String, Type> _taskTypes = new Dictionary<String, Type>();

        public TaskFactory()
        {
            // Preload the Task Types into a dictionary so we can look them up later
            foreach (Type type in typeof(TaskFactory).Assembly.GetTypes())
            {
                if (type.IsSubclassOf(typeof(CCTask)))
                {
                    _taskTypes[type.Name.ToLower()] = type;
                }
            }
        }

        public CCTask CreateTask(XmlElement task)
        {
            if (task != null)
            {
                string taskName = task.Name;
                taskName =  taskName.ToLower() + "task";

                // If the Type information is in our Dictionary, instantiate a new instance of that task
                Type taskType;
                if (_taskTypes.TryGetValue(taskName, out taskType))
                {
                    return (CCTask)Activator.CreateInstance(taskType, task);
                }
                else
                {
                    throw new ArgumentException("Unrecognized Task:" + task.Name);
                }                               
            }
            else
            {
                return null;
            }
        }
    }
like image 30
FlySwat Avatar answered Sep 25 '22 00:09

FlySwat