21

我正在为我的公司开发一个内部项目,该项目的一部分是能够将 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 例)开关来选择要实例化的子类。我希望我可以在这里做一些多态技巧来清理这个方法。

有什么建议吗?

4

10 回答 10

12

我使用反射来做到这一点。您可以创建一个基本上可以扩展的工厂,而无需添加任何额外的代码。

确保您有“使用 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,而且您还可以为自己节省一个需要维护的移动部件。

于 2008-08-26T02:45:00.907 回答
6

为每个类创建一个“Prototype”实例,并将它们放在 factory 内的哈希表中,使用您期望在 XML 中的字符串作为键。

所以 CreateTask 只是通过 get() 从哈希表中找到正确的 Prototype 对象。

然后调用 LoadFromXML 就可以了。

您必须将类预加载到哈希表中,

如果你想让它更自动...

您可以通过调用工厂的静态注册方法使类“自注册”。

在 Task 子类的静态块中调用 register (使用构造函数)。然后,您需要做的就是“提及”类以运行静态块。

Task 子类的静态数组就足以“提及”它们。或者使用反射来提及类。

于 2008-08-26T05:00:21.753 回答
4

您如何看待依赖注入?我使用了 Ninject,其中的上下文绑定支持非常适合这种情况。查看这篇博客文章,了解如何在请求时使用上下文绑定通过 IControllerFactory 创建控制器。这应该是一个很好的资源,可以帮助您了解如何根据您的情况使用它。

于 2008-08-26T03:19:15.677 回答
2

@jholland

我认为不需要 Type 枚举,因为我总是可以这样做:

枚举?

我承认这感觉很hacky。反射一开始感觉很脏,但是一旦你驯服了野兽,你就会享受它让你做的事情。(记住递归,感觉很脏,但很好)

诀窍是要意识到,您正在分析元数据,在本例中是从 xml 提供的字符串,并将其转换为运行时行为。这就是反射最擅长的。

顺便说一句: is 运算符,也是反射。

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

于 2008-08-26T03:04:51.920 回答
2

@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;
            }
        }
    }
于 2008-08-26T17:17:05.040 回答
1

@ChanChan

我喜欢反射的想法,但同时我总是害羞地使用反射。解决一些应该更容易的事情总是让我感到震惊。我确实考虑过这种方法,然后认为 switch 语句对于相同数量的代码气味会更快。

你确实让我想到了,我认为不需要 Type 枚举,因为我总是可以做这样的事情:

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

也许我应该再次打开我的 GoF 设计模式书,但我真的认为有一种方法可以多态地实例化正确的类。

于 2008-08-26T02:56:37.057 回答
1

枚举?

我指的是抽象类中的 Type 属性和枚举。

那就反思吧!我会在大约 30 分钟内将您的回答标记为已接受,只是为了给其他人时间来权衡。这是一个有趣的话题。

于 2008-08-26T03:08:44.487 回答
1

感谢您将其打开,我不会抱怨。这是一个有趣的话题,我希望你可以多态地实例化。
甚至 ruby​​(以及它的高级元编程)也必须为此使用它的反射机制。

于 2008-08-26T03:12:34.697 回答
1

@戴尔

我没有仔细检查过 nInject,但是从我对依赖注入的高级理解来看,我相信它会完成与 ChanChans 建议相同的事情,只是需要更多的层次(或者抽象)。

在我在这里只需要它的一次性情况下,我认为使用一些手动反射代码是一种更好的方法,而不是有一个额外的库来链接并只在一个地方调用它......

但也许我不明白 nInject 在这里给我的优势。

于 2008-08-26T03:38:21.980 回答
1

一些框架可能在需要时依赖反射,但大多数情况下,如果您愿意,您会使用引导程序来设置在需要对象实例时要执行的操作。这通常存储在通用字典中。直到最近,当我开始使用 Ninject 时,我才使用自己的。

对于 Ninject,我喜欢它的主要一点是,当它确实需要使用反射时,它就不需要了。相反,它利用了 .NET 的代码生成功能,使其速度非常快。如果您觉得反射在您使用的上下文中会更快,它也允许您以这种方式进行设置。

我知道这对于您目前的需求可能有点过头了,但我只是想指出依赖注入并为您提供一些未来的思考。参观道场上课。

于 2008-08-26T03:48:50.740 回答