【问题标题】:Abstract Factory Design Pattern抽象工厂设计模式
【发布时间】:2010-09-06 20:06:47
【问题描述】:

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

有什么建议吗?

【问题讨论】:

  • 检查所有案例具有安全优势。 reflection answer 存在安全漏洞,有人将 Task 类(不是您创建的)注入您的路径并通过将其名称放入 XML 来执行它。黑客做了更难的事情。

标签: c# design-patterns factory


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

    为每个类创建一个“原型”实例,并将它们放在工厂内的哈希表中,以您期望在 XML 中的字符串作为键。

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

    然后调用 LoadFromXML 就可以了。

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

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

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

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

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

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

        @jholland

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

        枚举?

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

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

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

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

        【讨论】:

          【解决方案5】:

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

          【讨论】:

            【解决方案6】:

            @ChanChan

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

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

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

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

            【讨论】:

              【解决方案7】:

              枚举?

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

              然后反思吧!我会在大约 30 分钟后将您的回答标记为已接受,以便让其他人有时间参与讨论。这是一个有趣的话题。

              【讨论】:

                【解决方案8】:

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

                【讨论】:

                  【解决方案9】:

                  @戴尔

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

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

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

                  【讨论】:

                    【解决方案10】:

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

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

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

                    【讨论】:

                    • 也许我误解了您对不使用反射的 Ninject 的评论 - 但您的博客文章以反映整个程序集并注册兼容类型 (IController) 的整个类结尾。不要误解我的意思——我想我会使用那个代码,但我不确定你会如何自动注册所有实现所需接口的类型。
                    • 您可以根据需要手动注册类型:Bind(typeof(IController)).To(typeof(HomeController)).Only(When.ContextVariable(“controllerName”).EqualTo("Home ") );博客文章只是展示了如何以编程方式完成它,因为所有控制器都继承自 IController。在 ASP.NET MVC 应用程序中,您肯定有很多控制器,并且手动注册每个控制器可能很乏味。