Вопрос

Я работаю над внутренним проектом для своей компании, и часть проекта заключается в том, чтобы иметь возможность разбирать различные "Задачи" из 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 случаев в реальном коде) переключатель, чтобы выбрать, какой дочерний класс создавать.Я надеюсь, что есть какой-то полиморфный трюк, который я могу сделать здесь, чтобы очистить этот метод.

Есть какой-нибудь совет?

Это было полезно?

Решение

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

убедитесь, что у вас есть "использование System.Отражение", поместите следующий код в свой метод создания экземпляра.

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 заново, а также вы сможете сэкономить себе движущуюся часть для обслуживания.

Другие советы

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

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

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

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

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

Вы можете сделать классы "саморегистрирующимися", вызвав статический метод register на заводе.

Поместите вызовы register (с конструкторами) в статические блоки подклассов Task.Затем все, что вам нужно сделать, это "упомянуть" классы, чтобы запустить статические блоки.

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

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

@джхолланд

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

Перечисление?

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

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

Кстати:оператор is тоже является отражением.

http://en.wikipedia.org/wiki/Reflection_ (computer_science)#Использует

@Tim, в итоге я использовал упрощенную версию вашего подхода и 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;
            }
        }
    }

@Чанчан

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

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

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

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

Перечисление?

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

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

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

@Дейл

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top