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





Для этого я использую отражение. Вы можете создать фабрику, которая в основном расширяется без добавления дополнительного кода.
убедитесь, что у вас есть «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, а также вы могли сэкономить подвижную часть для обслуживания.
@ChanChan
Мне нравится идея отражения, но в то же время я всегда стеснялся использовать отражение. Мне всегда казалось, что нужно обойти что-то, что должно быть проще. Я рассмотрел этот подход, а затем решил, что оператор switch будет быстрее при том же количестве запаха кода.
Вы заставили меня задуматься, я не думаю, что перечисление Type необходимо, потому что я всегда могу сделать что-то вроде этого:
if (CurrentTask is MergeTask)
{
// Do Something Specific to MergeTask
}
Возможно, мне стоит снова открыть мою книгу по шаблонам дизайна GoF, но я действительно думал, что есть способ полиморфно создать экземпляр нужного класса.
@jholland
I don't think the Type enum is needed, because I can always do something like this:
Enum?
Я признаю, что это кажется хакерским. Поначалу отражение кажется грязным, но как только вы приручите зверя, вам понравится то, что оно позволяет вам делать. (Помните рекурсию, она кажется грязной, но это хорошо)
Хитрость заключается в том, чтобы понять, что вы анализируете метаданные, в данном случае строку, предоставленную из xml, и превращаете ее в поведение во время выполнения. Вот в чем отражение лучше всего.
Кстати: оператор is тоже является отражением.
http://en.wikipedia.org/wiki/Reflection_(computer_science)#Uses
Enum?
Я имел в виду свойство Type и перечисление в моем абстрактном классе.
Отражение тогда! Я отмечу ваш ответ как принятый примерно через 30 минут, просто чтобы дать время всем подумать. Это забавная тема.
Спасибо, что оставили его открытым, я не буду жаловаться. Это забавная тема, я бы хотел, чтобы вы могли полиморфно создать экземпляр. Даже Ruby (и его превосходное метапрограммирование) должен использовать для этого свой механизм отражения.
Как вы относитесь к внедрению зависимостей? Я использую Ninject, и поддержка контекстной привязки в нем идеально подходит для этой ситуации. Посмотрите на этот Сообщение блога, как вы можете использовать контекстную привязку для создания контроллеров с помощью IControllerFactory, когда они запрашиваются. Это должен быть хороший ресурс о том, как использовать его в вашей ситуации.
@Дол
Я не изучал nInject внимательно, но, исходя из моего высокого уровня понимания внедрения зависимостей, я считаю, что он будет выполнять то же самое, что и предложение ChanChans, только с большим количеством слоев мусора (абстракции).
В разовой ситуации, когда мне это просто нужно здесь, я думаю, что использование некоторого обрабатываемого кода отражения - лучший подход, чем наличие дополнительной библиотеки для связывания и вызова ее только в одном месте ...
Но, возможно, я не понимаю преимущества, которое дает мне nInject.
Некоторые фреймворки могут полагаться на отражение там, где это необходимо, но в большинстве случаев вы используете загрузчик, если хотите, чтобы настроить, что делать, когда требуется экземпляр объекта. Обычно это хранится в общем словаре. Я использовал свой до недавнего времени, когда начал использовать Ninject.
В Ninject главное, что мне понравилось, это то, что когда ему действительно нужно использовать отражение, это не так. Вместо этого он использует возможности .NET для генерации кода, которые делают его невероятно быстрым. Если вы чувствуете, что отражение будет быстрее в контексте, который вы используете, это также позволяет вам настроить его таким образом.
Я знаю, что это может оказаться излишним для того, что вам нужно в данный момент, но я просто хотел указать на внедрение зависимостей и дать вам пищу для размышлений на будущее. Посетите додзё, чтобы получить урок.
Возможно, я неправильно понимаю ваш комментарий о том, что Ninject не использует отражение, но ваше сообщение в блоге заканчивается целым классом, который отражает всю сборку и регистрирует совместимые типы (IController). Не поймите меня неправильно - я думаю, что буду использовать этот код, но я не уверен, как еще вы могли бы автоматически регистрировать все типы, реализующие желаемый интерфейс.
Вы можете зарегистрировать типы вручную, если хотите: Bind (typeof (IController)). To (typeof (HomeController)). Only (When.ContextVariable («controllerName»). EqualTo («Home»)); Сообщение в блоге просто показывает, как вы можете сделать это программно, поскольку все контроллеры наследуются от IController. В приложении ASP.NET MVC у вас обязательно будет много контроллеров, и регистрация каждого из них вручную может быть утомительной.
Создайте экземпляр «прототипа» каждого класса и поместите их в хэш-таблицу внутри фабрики, используя строку, которую вы ожидаете в XML, в качестве ключа.
поэтому CreateTask просто находит правильный объект Prototype, с помощью get () из хеш-таблицы.
затем вызовите на нем LoadFromXML.
вам нужно предварительно загрузить классы в хеш-таблицу,
Если вы хотите, чтобы он был более автоматическим ...
Вы можете сделать классы «саморегистрирующимися», вызвав метод статической регистрации на фабрике.
Поместите вызовы для регистрации (с конструкторами) в статические блоки подклассов Task. Затем все, что вам нужно сделать, это «упомянуть» классы, чтобы запустить статические блоки.
Тогда будет достаточно статического массива подклассов Task, чтобы «упомянуть» их. Или используйте отражение, чтобы упомянуть классы.
@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;
}
}
}
Проверка всех случаев дает преимущество в плане безопасности. отражение ответ открыт для бреши в безопасности, когда кто-то внедряет класс Task (не созданный вами) в ваш путь и выполняет его, получая его имя в XML. Хакеры сделали еще более тяжелые вещи.