【问题标题】:Factory class knows too much工厂班知道的太多
【发布时间】:2009-03-30 16:40:30
【问题描述】:

已更新我已更新示例以更好地说明我的问题。我意识到它遗漏了一个具体点——即CreateLabel() 方法总是采用标签类型,因此工厂可以决定创建哪种类型的标签。问题是,它可能需要获取更多或更少的信息,具体取决于它想要返回的标签类型。

我有一个工厂类,它返回代表要发送到打印机的标签的对象。

工厂类如下所示:

public class LargeLabel : ILabel
{
    public string TrackingReference { get; private set; }

    public LargeLabel(string trackingReference)
    {
        TrackingReference = trackingReference;
    }
}

public class SmallLabel : ILabel
{
    public string TrackingReference { get; private set; }

    public SmallLabel(string trackingReference)
    {
        TrackingReference = trackingReference;
    }
}

public class LabelFactory
{
    public ILabel CreateLabel(LabelType labelType, string trackingReference)
    {
        switch (labelType)
        {
            case LabelType.Small:
                return new SmallLabel(trackingReference);
            case LabelType.Large:
                return new LargeLabel(trackingReference);
        }
    }
}

假设我创建了一个名为 CustomLabel 的新标签类型。我想把它从工厂退回,但它需要一些额外的数据:

public class CustomLabel : ILabel
{
    public string TrackingReference { get; private set; }
    public string CustomText { get; private set; }

    public CustomLabel(string trackingReference, string customText)
    {
        TrackingReference = trackingReference;
        CustomText = customText;
    }
}

这意味着我的工厂方法必须改变:

public class LabelFactory
{
    public ILabel CreateLabel(LabelType labelType, string trackingReference, string customText)
    {
        switch (labelType)
        {
            case LabelType.Small:
                return new SmallLabel(trackingReference);
            case LabelType.Large:
                return new LargeLabel(trackingReference);
            case LabelType.Custom:
                return new CustomLabel(trackingReference, customText);
        }
    }
}

我不喜欢这样,因为工厂现在需要满足最小公分母,但同时 CustomLabel 类需要 获取自定义文本值。我可以提供额外的工厂方法作为覆盖,但我想强制执行 CustomLabel 需要该值的事实,否则它只会被赋予空字符串。

实现这个场景的正确方法是什么?

【问题讨论】:

标签: c# design-patterns refactoring factory


【解决方案1】:

那么,你想如何调用工厂方法?

专注于您希望如何使用您的 API,实现通常会非常清晰。如果您将 API 的所需结果编写为单元测试,这将变得更加容易。

在这里,重载可能是正确的做法,但这实际上取决于您希望如何使用工厂。

【讨论】:

  • 问题是我希望工厂根据参数决定返回哪个标签。问题在于,根据它想要返回的标签,它可能需要额外的信息。因为调用代码不能事后猜测工厂,它不会知道它需要传递这些附加信息,所以我们处于僵局,工厂无法获得所需的信息,但被调用者没有不知道它需要提供它。
【解决方案2】:

仅仅使用工厂方法来决定你需要什么标签怎么样?

public class LabelFactory {
    public ILabel CreateLabel(string trackingReference, string customText) {
        return new CustomLabel(trackingReference, customText);
    }

    public ILabel CreateLabel(String trackingReference) {
        return new BasicLabel(trackingReference);
    }
}

您的工厂仍然需要了解每种类型(尽管您可以通过接口实现动态加载),但客户端需要知道的很少 - 根据提供的数据,工厂生成正确的实现。

这是您描述的简单问题的简单解决方案。我认为这个问题是对更复杂问题的过度简化,但在不知道你真正的问题是什么的情况下,我宁愿不设计一个过于复杂的解决方案。

【讨论】:

    【解决方案3】:

    这可能表明工厂模式并不适合您。但是,如果您确实需要或希望坚持使用它,我建议创建可以传递给工厂的初始化类/结构,而不是字符串。您是想使用基本信息类的各种子类(基本上是创建一个模仿标签类的初始化类层次结构)还是使用包含所有信息的一个类,这取决于您。

    【讨论】:

    • 你能推荐一个替代的、更合适的创作模式吗?
    • 好吧,不知道您的具体环境,我想知道在实例化标签本身的客户端代码中会出现什么问题?换句话说,是什么让你走上了工厂模式的道路?
    【解决方案4】:

    您应该尝试使用配置类并将其实例传递给工厂。配置类将构建一个层次结构,其中对于您期望从工厂获得的每个结果都存在一个特殊的配置类。每个配置类都捕获工厂结果的特定属性。

    对于您给出的示例,我将编写一个 BasicLabelConfiguration 和一个从它派生的 CustomLabelConfiguration。 BasicLabelConfiguration 捕获跟踪引用,而 CustomLabelConfiguration 捕获自定义文本。

    最终工厂根据传递的配置对象的类型做出决定。


    下面是代码示例:

    public class BasicLabelConfiguration
    {
      public BasicLabelConfiguration()
      {
      }
    
      public string TrackingReference { get; set; }
    }
    
    public class CustomLabelConfiguration : BasicLabelConfiguration
    {
      public CustomLabelConfiguration()
      {
      }
    
      public string CustomText { get; set; }
    }
    
    public class LabelFactory
    {
      public ILabel CreateLabel(BasicLabelConfiguration configuration)
      {
        // Possibly make decision from configuration
        CustomLabelConfiguration clc = configuration as CustomLabelConfiguration;
        if (clc != null)
        {
          return new CustomLabel(clc.TrackingReference, clc.CustomText);
        }
        else
        {
          return new BasicLabel(configuration.TrackingReference);
        }
      }
    }
    

    最后你会像这样使用工厂:

    // Create basic label
    ILabel label = factory.CreateLabel(new BasicLabelConfiguration 
    {
      TrackingReference = "the reference"
    });
    

    // Create basic label
    ILabel label = factory.CreateLabel(new CustomLabelConfiguration 
    {
      TrackingReference = "the reference",
      CustomText = "The custom text"
    });
    

    【讨论】:

    • 哇!这很复杂。像 Adam Robinson 一样,如果您将底层实现的 API 的所有复杂性暴露在至少同样复杂(如果不是更多)的“配置”API 上,我根本看不到使用工厂的好处。
    • 工厂仍然负责构建实际对象。配置基本上是创建真实对象的纪念品。但我同意这很复杂。另一种选择是使用原型模式,但作者没有描述他的环境,所以我不知道它是否适用。
    【解决方案5】:

    如果没有更多信息,很难给出任何建议,但假设工厂模式是您真正需要的,您可以尝试以下方法:

    将所需的参数打包到某种属性映射中(例如,字符串到字符串的映射)并将其作为参数传递给工厂的 create 方法。使用知名标签作为映射中的键,允许专业工厂根据自己的喜好提取和解释映射值。

    这至少可以让您暂时维持一个单一的工厂接口,并在您发现工厂模式在这里不正确时(或何时)推迟处理架构问题。

    (哦,如果您真的想在这里使用工厂模式,我强烈建议您将其设置为可插拔,以避免必须为每种新标签类型修改工厂)。

    【讨论】:

      【解决方案6】:

      您正试图将模式强加到它不适合的场景中。我建议放弃这种特定的模式并专注于而不是使最简单的解决方案成为可能。

      我认为在这种情况下,我只会有一个类,Label,它有一个用于自定义文本的文本字段,该文本字段通常为空/空,但如果需要自定义标签,可以设置哪个。它很简单,一目了然,不会给您的维护程序员带来任何噩梦。

      public class Label
      {
          public Label(string trackingReference) : this(trackingReference, string.Empty)
          {
          }
      
          public Label(string trackingReference, string customText)
          {
              CustomText = customText;
          }
      
          public string CustomText ( get; private set; }
      
          public bool IsCustom
          {
              get
              {
                  return !string.IsNullOrEmpty(CustomText);
              }
          }
      }
      

      【讨论】:

      • 如果您想为标签添加 10 种新的行为类型会怎样?我敢打赌,使用单一课程将是一场噩梦;)。 en.wikipedia.org/wiki/Open/closed_principle
      • 然后,也只有到那时,我才会重构代码以满足时代的要求。不要试图预测未来:这是昂贵的,而且你经常会出错。
      【解决方案7】:

      在问题更新后回答更新 - 见下文

      我仍然认为您使用工厂模式是正确的,并且正确地重载了​​ CreateLabel 方法;但我认为在将 LabelType 传递给 CreateLabel 方法时,您错过了使用工厂模式的要点。

      关键点:工厂模式的全部目的是封装选择要实例化和返回的具体子类的逻辑。调用代码不应该告诉工厂要实例化哪种类型。好处是调用工厂的代码因此不受将来对该逻辑的更改以及向工厂添加新的具体子类的影响。您的所有调用代码需要依赖的是工厂,以及从 CreateLabel 返回的接口类型。

      调用工厂时代码中的逻辑当前必须类似于此伪代码...

      // Need to create a label now
      ILabel label;
      if(we need to create a small label)
      {
         label = factory.CreateLabel(LabelType.SmallLabel, "ref1");
      }
      else if(we need to create a large label)
      {
         label = factory.CreateLabel(LabelType.LargeLabel, "ref1");
      }
      else if(we need to create a custom label)
      {
         label = factory.CreateLabel(LabelType.CustomLabel, "ref1", "Custom text")
      }
      

      ...所以你明确地告诉工厂要创建什么。这很糟糕,因为每次向系统添加新的标签类型时,您都需要...

      1. 更改工厂代码以处理新的 LabelType 值
      2. 去工厂调用的任何地方添加一个新的 else-if

      但是,如果您将选择 LabelType 值的逻辑移到您的工厂中,则可以避免这种情况。逻辑与其他所有内容一起封装在工厂中。如果将新类型的标签添加到您的系统中,您只需更改出厂设置。所有调用工厂的现有代码保持不变,没有重大更改。

      您当前的调用代码使用什么数据来决定是否需要大标签或小标签?那条数据应该传递给工厂的 CreateLabel() 方法。

      您的工厂和标签类可能如下所示...

      // Unchanged
      public class BasicLabel: ILabel
      {
          public LabelSize Size {get; private set}
          public string TrackingReference { get; private set; }
      
          public SmallLabel(LabelSize size, string trackingReference)
          {
              Size = size;
              TrackingReference = trackingReference;
          }
      }
      
      
      
      // ADDED THE NULL OR EMPTY CHECK
      public class CustomLabel : ILabel
      {
          public string TrackingReference { get; private set; }
          public string CustomText { get; private set; }
      
          public CustomLabel(string trackingReference, string customText)
          {
              TrackingReference = trackingReference;
              if(customText.IsNullOrEmpty()){
                 throw new SomeException();
              }
              CustomText = customText;
          }
      }
      
      
      
      public class LabelFactory
      {
          public ILabel CreateLabel(string trackingReference, LabelSize labelSize)
          {
               return new BasicLabel(labelSize, trackingReference);
          }
      
          public ILabel CreateLabel(string trackingReference, string customText)
          {
               return new CustomLabel(trackingReference, customText);
          }
      }
      

      我希望这会有所帮助。

      【讨论】:

        【解决方案8】:

        从阅读您的问题看来,您的 UI 会收集信息,然后使用工厂创建适当的标签。我们在我开发的 CAD/CAM 应用程序中使用了不同的方法。

        在启动期间,我的应用程序使用工厂方法创建标签主列表。 我的一些标签具有初始化参数,因为它们是彼此的变体。例如,我们有三种类型的扁平零件标签。而其他的具有用户定义的或在设置时未知的参数。

        在第一种情况下,初始化是在工厂方法中处理的。因此,我创建了三个传入所需参数的 FlatPartLabel 实例。

        在第二种情况下,标签接口有一个配置选项。这由标签打印机对话框调用以填充设置面板。在您的情况下,这是跟踪参考和 CustomText 将被传入的地方。

        我的标签接口还为每个标签类型返回一个唯一的 ID。如果我有一个特定的命令来处理该类型的标签,那么我将遍历我的应用程序中的标签列表,找到与 ID 匹配的标签,将其转换为特定类型的标签,然后对其进行配置。当用户只想为特定的平面部分打印一个标签时,我们会这样做。

        这样做意味着您可以对标签所需的参数进行任意复杂化,而不必为工厂添加不必要的参数。

        【讨论】:

          猜你喜欢
          • 2020-09-08
          • 1970-01-01
          • 1970-01-01
          • 2016-12-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多