题
我正在为我的公司开发一个内部项目,该项目的一部分是能够将 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,并且还可以为自己节省维护的移动部分。
其他提示
创建每个类的“原型”实例,并将它们放入工厂内的哈希表中,并以 XML 中期望的字符串作为键。
因此,createTask只能通过从哈希台上get()找到正确的原型对象。
然后调用 LoadFromXML 即可。
你必须将类预先加载到哈希表中,
如果你想要更自动...
您可以通过调用工厂上的静态注册方法来使类“自注册”。
将对 register 的调用(使用构造函数)放在 Task 子类的静态块中。然后您需要做的就是“提及”类以使静态块运行。
任务子类的静态数组就足以“提及”它们。或者使用反射来提及类。
您对依赖注入有何看法?我使用 Ninject,其中的上下文绑定支持非常适合这种情况。看这个 博客文章 关于如何在请求时使用上下文绑定通过 IControllerFactory 创建控制器。这应该是一个很好的资源,可以帮助您了解如何根据您的情况使用它。
@jholland
我认为不需要 Type 枚举,因为我总是可以这样做:
枚举?
我承认这感觉很hacky。反思一开始感觉很肮脏,但一旦你驯服了这头野兽,你就会享受它允许你做的事情。(记住递归,感觉很脏,但是很好)
诀窍是要意识到,您正在分析元数据(在本例中是从 xml 提供的字符串),并将其转换为运行时行为。这就是反思最擅长的。
顺便提一句:is 运算符,也是反射。
http://en.wikipedia.org/wiki/Reflection_(computer_science)#Uses
@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 语句会更快。
你确实让我思考,我认为不需要 Type 枚举,因为我总是可以做这样的事情:
if (CurrentTask is MergeTask)
{
// Do Something Specific to MergeTask
}
也许我应该再次打开我的 GoF 设计模式书,但我真的认为有一种方法可以多态地实例化正确的类。
枚举?
我指的是抽象类中的 Type 属性和枚举。
反思就是这样!我会在大约 30 分钟内将您的答案标记为已接受,以便为其他人提供参与权衡的时间。这是一个有趣的话题。
感谢您将其打开,我不会抱怨。这是一个有趣的话题,我希望你能够多态实例化。
即使是 ruby(及其卓越的元编程)也必须使用其反射机制来实现这一点。
@戴尔
我没有仔细检查过 nInject,但从我对依赖注入的高度理解来看,我相信它会完成与 ChanChans 的建议相同的事情,只是有更多的粗糙层(呃抽象)。
在我在这里只需要它的一次性情况下,我认为使用一些手卷反射代码是一种比使用额外的库来链接并仅在一个地方调用它更好的方法......
但也许我不明白 nInject 在这里给我带来的优势。
某些框架可能在需要时依赖反射,但大多数时候您可以使用引导程序(如果您愿意的话)来设置在需要对象实例时要执行的操作。这通常存储在通用字典中。直到最近我开始使用 Ninject 时,我才使用自己的。
对于 Ninject,我最喜欢它的一点是,当它确实需要使用反射时,它却不需要。相反,它利用了 .NET 的代码生成功能,使其速度快得令人难以置信。如果您觉得反射在您正在使用的上下文中会更快,它也允许您以这种方式进行设置。
我知道这对于您目前的需求来说可能有点过分了,但我只是想指出依赖注入并为您提供一些对未来的思考。参观 道场 一个教训。