Абстрактный дизайн фабрики шаблон - PullRequest
21 голосов
/ 26 августа 2008

Я работаю над внутренним проектом для моей компании, и часть проекта должна быть в состоянии проанализировать различные «Задачи» из файла XML в набор задач, которые будут выполняться позже.

Поскольку у каждого типа Задач есть множество различных связанных полей, я решил, что будет лучше представить каждый тип Задач отдельным классом.

Для этого я создал абстрактный базовый класс:

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);
}

Каждая задача унаследована от этого базового класса и включает код, необходимый для создания себя из переданного в XmlElement, а также сериализации обратно в XmlElement.

Базовый пример:

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.
    }
}

Затем парсер будет использовать код, подобный этому, для создания коллекции задач:

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));
    }
}

Все это прекрасно работает и позволяет мне распределять задачи по базовому классу, сохраняя при этом структуру индивидуальных классов для каждой задачи.

Однако мне не нравится мой код для TaskFactory.CreateTask. Этот метод принимает XmlElement, а затем возвращает экземпляр соответствующего класса Task:

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

Поскольку я должен проанализировать XMLElement, я использую огромный (10-15 случаев в реальном коде) переключатель, чтобы выбрать, какой дочерний класс нужно создать. Я надеюсь, что есть какой-то полиморфный трюк, который я могу сделать здесь, чтобы очистить этот метод.

Любой совет?

Ответы [ 10 ]

12 голосов
/ 26 августа 2008

Я использую отражение, чтобы сделать это. Вы можете создать фабрику, которая в основном расширяется, без необходимости добавлять дополнительный код.

убедитесь, что у вас есть «using System.Reflection», поместите следующий код в ваш метод создания экземпляра.

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");
        }
    }
}

Еще одно наблюдение состоит в том, что вы можете сделать этот метод статическим и отключить его от класса Task, чтобы вам не приходилось обновлять TaskFactory, а также вы спасли себя от движущейся части, чтобы поддерживать ее. .

6 голосов
/ 26 августа 2008

Создайте экземпляр Prototype для каждого класса и поместите их в хеш-таблицу внутри фабрики со строкой, ожидаемой в XML, в качестве ключа.

так что CreateTask просто находит нужный объект Prototype, путем получения () из хеш-таблицы.

затем вызовите LoadFromXML для него.

вы должны предварительно загрузить классы в хеш-таблицу,

Если вы хотите, чтобы это было более автоматическим ...

Вы можете сделать классы "саморегистрационными", вызвав метод статического регистра на фабрике.

Размещать вызовы для регистрации (с конструкторами) в статических блоках подклассов Task. Тогда все, что вам нужно сделать, это «упомянуть» классы для запуска статических блоков.

Тогда для их «упоминания» достаточно статического массива подклассов Task. Или используйте отражение, чтобы упомянуть классы.

4 голосов
/ 26 августа 2008

Как вы относитесь к инъекции зависимости? Я использую Ninject, и поддержка контекстной привязки в нем была бы идеальной для этой ситуации. Посмотрите на это сообщение в блоге о том, как вы можете использовать контекстную привязку при создании контроллеров с помощью IControllerFactory, когда они запрашиваются. Это должен быть хороший ресурс о том, как использовать его в вашей ситуации.

2 голосов
/ 26 августа 2008

@ Тим, в итоге я использовал упрощенную версию вашего подхода и ChanChans, вот код:

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;
            }
        }
    }
2 голосов
/ 26 августа 2008

@ jholland

Я не думаю, что перечисление Type необходимо, потому что я всегда могу сделать что-то вроде этого:

Enum

Я признаю, что это кажется хакерским. Отражение поначалу кажется грязным, но как только вы приручите зверя, вам понравится то, что оно позволяет вам делать. (Помните рекурсию, она кажется грязной, но это хорошо)

Хитрость в том, чтобы понять, что вы анализируете метаданные, в данном случае строку, предоставленную из xml, и превращаете ее в поведение во время выполнения. Это то, что отражение лучше всего.

Кстати: оператор is, тоже отражение.

http://en.wikipedia.org/wiki/Reflection_(computer_science)#Uses

1 голос
/ 26 августа 2008

Некоторые фреймворки могут полагаться на отражение там, где это необходимо, но большую часть времени вы используете загрузчик, если хотите, для настройки того, что делать, когда требуется экземпляр объекта. Это обычно хранится в общем словаре. Я использовал свой до недавнего времени, когда я начал использовать Ninject.

В Ninject главное, что мне понравилось, это то, что когда ему нужно использовать отражение, это не так. Вместо этого он использует возможности генерации кода .NET, которые делают его невероятно быстрым. Если вы чувствуете, что отражение будет быстрее в контексте, который вы используете, это также позволит вам настроить его таким образом.

Я знаю, что это может быть излишним для того, что вам нужно в данный момент, но я просто хотел отметить инъекцию зависимости и дать вам пищу для размышлений на будущее. Посетите додзё для урока.

1 голос
/ 26 августа 2008

@ Dale

Я не проверял nInject близко, но, исходя из моего высокого уровня понимания внедрения зависимостей, я считаю, что он будет выполнять то же самое, что и предложение ChanChans, только с большим количеством слоев коры (абстракция).

В единственной ситуации, когда мне просто нужно здесь, я думаю, что использование некоторого управляемого кода отражения является лучшим подходом, чем наличие дополнительной библиотеки для ссылки и вызов только одного места ...

Но, может быть, я не понимаю, какое преимущество даст мне здесь nInject.

1 голос
/ 26 августа 2008

Спасибо, что оставили его открытым, я не буду жаловаться. Это забавная тема, я бы хотел, чтобы вы могли полиморфно ее реализовать. Даже ruby ​​(и его превосходное метапрограммирование) должны использовать для этого механизм отражения.

1 голос
/ 26 августа 2008

Enum?

Я имел в виду свойство Type и enum в моем абстрактном классе.

Отражение тогда! Я отмечу ваш ответ как принятый примерно через 30 минут, просто чтобы дать время кому-то еще взвесить. Это забавная тема.

1 голос
/ 26 августа 2008

@ ChanChan

Мне нравится идея отражения, но в то же время я всегда стеснялся использовать отражение. Меня всегда удивляло, что я работаю над тем, что должно быть проще. Я рассмотрел этот подход, а затем решил, что оператор switch будет быстрее при том же количестве запаха кода.

Вы заставили меня задуматься, я не думаю, что перечисление Type необходимо, потому что я всегда могу сделать что-то вроде этого:

if (CurrentTask is MergeTask)
{
    // Do Something Specific to MergeTask
}

Возможно, мне следует снова открыть книгу по шаблонам GoF Design, но я действительно думал, что есть способ полиморфно создать экземпляр нужного класса.

...