65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 坏实践:通过坏代码示例学习如何写出好代码 – 第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (170投票s)

2016年5月3日

CPOL

17分钟阅读

viewsIcon

278122

改进你的工厂!

引言

本文是我上一篇文章的延续

我强烈建议在阅读本文之前先阅读它(我将多次引用它)。

我注意到很多人觉得我的第一篇文章很有帮助,所以我决定写第二部分。

那么… 简单回顾一下我的第一篇文章,我展示了几种可以应用于代码的技术,使代码

  • 更易读
  • 更易维护
  • 更易扩展

我是在一个极其简化的方法上展示的 – 以避免在文章中放入大量代码(很多人 – 请看评论 – 没有理解文章的意图,他们以为我试图重构一个只有几行代码的极其简化的方法),因为我认为这会让文章完全难以阅读。

总而言之,我展示了如何使用一些技术和设计模式,这些技术和设计模式可以让你(以及公司里的同事)在实现一个复杂、可能需要长期扩展和维护的应用程序时生活更轻松。

我是在一个展示实际功能实现 – 折扣计算器 – 的简化示例中展示的。

文章目标

在上一篇文章中,我最终得到了一个干净且易于维护的解决方案。

然而,正如许多聪明人指出的那样,下面的工厂中的 switch-case 语句

public class DefaultAccountDiscountCalculatorFactory : IAccountDiscountCalculatorFactory
{
  public IAccountDiscountCalculator GetAccountDiscountCalculator(AccountStatus accountStatus)
  {
    IAccountDiscountCalculator calculator;
    switch (accountStatus)
    {
      case AccountStatus.NotRegistered:
        calculator = new NotRegisteredDiscountCalculator();
        break;
      case AccountStatus.SimpleCustomer:
        calculator = new SimpleCustomerDiscountCalculator();
        break;
      case AccountStatus.ValuableCustomer:
        calculator = new ValuableCustomerDiscountCalculator();
        break;
      case AccountStatus.MostValuableCustomer:
        calculator = new MostValuableCustomerDiscountCalculator();
        break;
      default:
        throw new NotImplementedException();
    }
 
    return calculator;
  }
}

仍然违反了开闭原则。他们提出了一些非常好的解决方案。我完全同意这一点,并在撰写上一篇文章时,我计划写下一篇文章,展示如何解决这个问题。

上一篇文章非常长,我决定单独写一篇文章,因为这个主题非常复杂,而且有几种实现方式。

总而言之,本文将重点关注如何从我们的抽象工厂中移除 switch-case 语句。

因为我认为没有一种万能的方法可以解决所有情况下的这个问题,所以我决定展示几种实现版本,并描述每种版本的优缺点。

工厂代码将是我们本文的基础代码。

由于我们在上一篇文章中通过抽象(interface)隐藏了工厂的实现

public interface IAccountDiscountCalculatorFactory
{
  IAccountDiscountCalculator GetAccountDiscountCalculator(AccountStatus accountStatus);
}

我们将能够切换到工厂的新实现,而无需修改任何其他类。

如果需要,我们只需将 IAccountDiscountCalculatorFactory 接口的不同实现注入到 DiscountManager 类或其他调用者中。

“面向接口编程”的方法难道不是很棒吗?!!当然是!

Switch Case 与 Dictionary 模式

为什么 switch-case 或多个 if-else if 语句是个坏主意。

好吧… 让我们看看下面的工厂

public class DefaultAccountDiscountCalculatorFactory : IAccountDiscountCalculatorFactory
{
  public IAccountDiscountCalculator GetAccountDiscountCalculator(AccountStatus accountStatus)
  {
    IAccountDiscountCalculator calculator;
    switch (accountStatus)
    {
      case AccountStatus.NotRegistered:
        calculator = new NotRegisteredDiscountCalculator();
        break;
      case AccountStatus.SimpleCustomer:
        calculator = new SimpleCustomerDiscountCalculator();
        break;
      case AccountStatus.ValuableCustomer:
        calculator = new ValuableCustomerDiscountCalculator();
        break;
      case AccountStatus.MostValuableCustomer:
        calculator = new MostValuableCustomerDiscountCalculator();
        break;
      default:
        throw new NotImplementedException();
    }
 
    return calculator;
  }
}

我们现在有四种账户状态。现在想象一下,我们的公司计划增加更多。

第一个问题

这意味着每次我们增加对新状态的支持时,都必须修改我们的工厂,添加新的 case 块。这意味着每次更改都可能在我们的类中引入 bug,或者破坏现有的单元测试。

第二个问题

我们的工厂还与具体实现紧密耦合 – 这违反了控制反转原则。我们无法在不修改工厂的情况下,用 ExtendedSimpleCustomerDiscountCalculator 替换 SimpleCustomerDiscountCalculator 的实现。

更生动的例子

我们可以设想这些问题会更明显的情况。如果我们有一个巨大的 switch-case 语句,并且每个 case 块都返回一个包含给定国家特定业务逻辑的类实例,我们可以想象,每次我们想处理一个新的国家(世界上大约有 200 个国家)时,我们的 switch-case 语句都会被扩展。过一段时间,我们的 switch-case 语句将变得巨大且难以阅读。并且有可能重复添加同一个国家等等。

理解对象生命周期

在我开始展示我们问题的解决方案之前,我想简单谈谈我们将要创建的对象生命周期。

在实现实际解决方案时,我们必须始终考虑每个对象的生命周期相对于整个应用程序应该是什么样子。

由于我们不需要工厂的多个实例(一个实例可以返回所有调用、所有线程的对象),我们应该每个应用程序只创建一个实例。

我们可以通过使用 IOC 容器并将其配置为每次调用我们的工厂都返回相同的对象来实现这一点。例如,如果我们使用 AutoFac 库,其配置将如下所示

var builder = new ContainerBuilder();
builder.RegisterType<DefaultAccountDiscountCalculatorFactory>().As
                    <IAccountDiscountCalculatorFactory>().SingleInstance();

如果我们手动注入依赖项,我们必须在应用程序根目录创建一个实例,并将相同的实例注入到每个使用它的组件中。我所说的应用程序根目录,是指例如控制台应用程序Main 方法。

在我们的计算器实现中,例如

  • NotRegisteredDiscountCalculator
  • SimpleCustomerDiscountCalculator
  • 等等。

答案并不明显。

有两种管理方式

  • 我们希望我们的工厂为每一次调用返回相同IAccountDiscountCalculator 实现实例
  • 我们希望我们的工厂为每一次调用返回IAccountDiscountCalculator 实现实例
public class SimpleCustomerDiscountCalculator : IAccountDiscountCalculator
{
  private readonly IUser _user;
 
  public SetUser(IUser user)
  {
    _user = user;
  }
 
  public decimal ApplyDiscount(decimal price)
  {
  //business logic which is using _user field
  }
}

在上面的类中,我们的状态是一个字段:_user

如果我们正在实现多线程应用程序,如 ASP MVC 项目,我们不能为每次调用使用一个相同的类的实例。每个线程都希望使用 SimpleCustomerDiscountCalculator 类来操作不同的用户。所以我们需要为每次调用工厂返回一个 SimpleCustomerDiscountCalculator 类的新实例。

我将我的解决方案分为两组

  • 工厂每次调用返回相同的类实例
  • 工厂每次调用返回的类实例

有用的 Dictionary

在下面所有建议的解决方案中,我将使用 C# Dictionary 类。但是,我将使用它不同的变体。

现在让我们来看具体的实现。

每次调用返回同一实例

在这个解决方案组中,工厂对 IAccountDiscountCalculator 的具体实现的每一次调用,例如

_factory.GetAccountDiscountCalculator(AccountStatus.SimpleCustomer);

都将返回相同的对象。

基本版本

我的解决方案的第一个版本是一个简单的工厂

public class DictionarableAccountDiscountCalculatorFactory : IAccountDiscountCalculatorFactory
{
    private readonly Dictionary<AccountStatus, 
            IAccountDiscountCalculator> _discountsDictionary;
    public DictionarableAccountDiscountCalculatorFactory
           (Dictionary<AccountStatus, IAccountDiscountCalculator> discountsDictionary)
    {
        _discountsDictionary = discountsDictionary;
    }

    public IAccountDiscountCalculator 
           GetAccountDiscountCalculator(AccountStatus accountStatus)
    {
        IAccountDiscountCalculator calculator;

        if (!_discountsDictionary.TryGetValue(accountStatus, out calculator))
        {
            throw new NotImplementedException
            ("There is no implementation of IAccountDiscountCalculatorFactory interface 
            for given Account Status");
        }

        return calculator;
    }
}

上面的工厂包含一个对象字典(计算器 – IAccountDiscountCalculator 接口的实现)。这是工厂的配置。

当我们想从工厂获取对象时,它将返回分配给 AccountStatus enum 值的实现。

为了使其工作,我们必须在创建工厂时对其进行配置。正如我之前提到的,我们只需要一个实例,并且我们将在应用程序根目录(如果我们想手动注入它)或创建 IoC 容器时创建该实例。

所以在创建我们的工厂时,我们需要创建一个配置(为每个实现分配一个选定的 AccountStatus

var discountsDictionary = new Dictionary<AccountStatus, IAccountDiscountCalculator>
      {
        {AccountStatus.NotRegistered, new NotRegisteredDiscountCalculator()},
        {AccountStatus.SimpleCustomer, new SimpleCustomerDiscountCalculator()},
        {AccountStatus.ValuableCustomer, new ValuableCustomerDiscountCalculator()},
        {AccountStatus.MostValuableCustomer, new MostValuableCustomerDiscountCalculator()}
      };

并将其注入工厂

  • 手动
    var factory = new DictionarableAccountDiscountCalculatorFactory(discountsDictionary);

    或使用 IOC 容器(在此示例中,我使用的是 - AutoFac 库),这是负责我们工厂配置的部分

    var builder = new ContainerBuilder();
    builder.RegisterType<DictionarableAccountDiscountCalculatorFactory>().As
            <IAccountDiscountCalculatorFactory>()
    .WithParameter("discountsDictionary", discountsDictionary)
    .SingleInstance();

现在,我们可以像以前一样(参见上一篇文章)在注入到调用者的工厂中使用我们的工厂了

priceAfterDiscount = _factory.GetAccountDiscountCalculator(accountStatus).ApplyDiscount(price);

优点

  • 很简单
  • 强类型配置 – 配置错误(类型定义错误)将在编译时导致错误
  • 从工厂返回对象非常快 – 它们已经创建好了

缺点

  • 在返回的对象具有状态的多线程环境中不起作用

在我看来,这是最适合我们正在考虑的功能的版本 – 简单折扣计算器示例。

但是,我想展示一下我们如何处理不同的情况…

延迟版本

现在想象一下,我们的计算器的创建非常昂贵。你需要加载大量文件,然后将其保存在内存中。但是…我们只需要一个计算器实例用于应用程序实例…

这怎么可能?

例如,每个用户运行一个独立的 Windows Form 应用程序实例,并在特定账户状态的客户上执行折扣计算。假设 Mary 在计算简单客户的折扣,Ted 在计算重要客户的折扣。那么就没有必要在每个应用程序实例中创建所有计算器。

为了解决这个问题,我们可以通过使用 C# 中的 Lazy 类型来改进我们的工厂,它实现了延迟加载模式。

Lazy 类型是在 .NET Framework 4 版本中引入的。如果你使用的是旧版本的 .NET Framework,你需要手动实现延迟加载。

 


我们的工厂实现现在看起来像这样

public class DictionarableLazyAccountDiscountCalculatorFactory : 
                                             IAccountDiscountCalculatorFactory
{
    private readonly Dictionary<AccountStatus, Lazy<IAccountDiscountCalculator>> 
                               _discountsDictionary;
    public DictionarableLazyAccountDiscountCalculatorFactory
    (Dictionary<AccountStatus, Lazy<IAccountDiscountCalculator>> discountsDictionary)
    {
        _discountsDictionary = discountsDictionary;
    }

    public IAccountDiscountCalculator 
           GetAccountDiscountCalculator(AccountStatus accountStatus)
    {
        Lazy<IAccountDiscountCalculator> calculator;

        if (!_discountsDictionary.TryGetValue(accountStatus, out calculator))
        {
            throw new NotImplementedException
            ("There is no implementation of IAccountDiscountCalculatorFactory interface 
            for given Account Status");
        }

        return calculator.Value;
    }
}

工厂配置也将稍微改变一下

var lazyDiscountsDictionary = new Dictionary<AccountStatus, Lazy<IAccountDiscountCalculator>>
  {
    {AccountStatus.NotRegistered, new Lazy<IAccountDiscountCalculator>(() => 
                                  new NotRegisteredDiscountCalculator()) },
    {AccountStatus.SimpleCustomer, new Lazy<IAccountDiscountCalculator>(() => 
                                   new SimpleCustomerDiscountCalculator())},
    {AccountStatus.ValuableCustomer, new Lazy<IAccountDiscountCalculator>(() => 
                                     new ValuableCustomerDiscountCalculator())},
    {AccountStatus.MostValuableCustomer, new Lazy<IAccountDiscountCalculator>(() => 
                                         new MostValuableCustomerDiscountCalculator())}
  };
var factory = new DictionarableLazyAccountDiscountCalculatorFactory(lazyDiscountsDictionary);

关于我们所做的事情…
我们的延迟工厂的构造函数现在接受 Dictionary<AccountStatus, Lazy<IAccountDiscountCalculator>> 类型作为参数,并将其存储在 private 字段中。
字典现在将使用延迟的 IAccountDiscountCalculator 实现 :)
所以,在创建 lazyDiscountsDictionary(工厂的配置)时,我使用的是以下语法来设置每个项目的值

new Lazy<IAccountDiscountCalculator>(() => new NotRegisteredDiscountCalculator())

我们使用了 Lazy 类的构造函数,它接受一个 Func 委托作为参数。当我们第一次尝试访问我们字典中存储的值(Lazy 类型)的 Value 属性时,将执行此委托,并创建具体的计算器实现。

其余部分将与第一个示例完全相同。

现在,具体的计算器实现将在第一次请求执行后创建。每次对同一实现的后续调用都将返回相同的(第一次调用时创建的)对象。

请注意,我们仍然不需要更改接口 IAccountDiscountCalculator。所以我们可以在调用者类中使用它,就像对待基本版本一样。

priceAfterDiscount = _factory.GetAccountDiscountCalculator(accountStatus).ApplyDiscount(price);

重要的是,你应该决定是否真的需要使用延迟加载。有时,基本版本可能更好 – 例如,当你能接受应用程序启动时间较长,并且不想延迟从工厂返回对象的情况。

优点

  • 更快的应用程序启动
  • 直到真正需要时才将对象保留在内存中
  • 强类型配置 – 配置错误(类型定义错误)将在编译时导致错误

缺点

  • 从工厂第一次返回对象会比较慢
  • 在返回的对象具有状态的多线程环境中不起作用

代码外的配置

如果你需要或更喜欢将工厂的配置(将实现分配给 enum 值)保留在代码库之外 – 这个版本将适合你。

我们可以将配置存储在数据库表中,或配置文件中,以我们认为更合适的方式。

我将展示一个配置存储在数据库表中的示例,并使用 Entity Framework Code First 从数据库中获取此配置并将其注入工厂。

我们的工厂实现现在看起来像这样

public class ConfigurableAccountDiscountCalculatorFactory : IAccountDiscountCalculatorFactory
{
    private readonly Dictionary<AccountStatus, 
            IAccountDiscountCalculator> _discountsDictionary;
    public ConfigurableAccountDiscountCalculatorFactory
           (Dictionary<AccountStatus, string> discountsDictionary)
    {
        _discountsDictionary = ConvertStringsDictToObjectsDict(discountsDictionary);
    }

    public IAccountDiscountCalculator 
           GetAccountDiscountCalculator(AccountStatus accountStatus)
    {
        IAccountDiscountCalculator calculator;

        if (!_discountsDictionary.TryGetValue(accountStatus, out calculator))
        {
            throw new NotImplementedException("There is no implementation of 
            IAccountDiscountCalculatorFactory interface for given Account Status");
        }

        return calculator;
    }

    private Dictionary<AccountStatus, IAccountDiscountCalculator> 
                       ConvertStringsDictToObjectsDict(
        Dictionary<AccountStatus, string> dict)
    {
        return dict.ToDictionary(x => x.Key,
            x => (IAccountDiscountCalculator)Activator.CreateInstance
                 (Type.GetType(x.Value)));                
    }
}

你可以注意到,工厂的构造函数现在以 string 值字典作为参数

Dictionary<AccountStatus,string>

并使用 private 方法将其转换为 IAccountDiscountCalculator 实现的 dictionary

Dictionary<AccountStatus,IAccountDiscountCalculator>

使用 private 方法

private Dictionary<AccountStatus, IAccountDiscountCalculator> ConvertStringsDictToObjectsDict(
    Dictionary<AccountStatus, string> dict)
{
    return dict.ToDictionary(x => x.Key,
        x => (IAccountDiscountCalculator)Activator.CreateInstance(Type.GetType(x.Value)));  
}

存储在数据库表中的配置将如下所示

string 值的约定遵循以下模式

[Namespace].[ClassName], [AssemblyName]

请注意,我将 Account Statuses 保留为整数值,因为显然数据库不支持 enum 值。

你也可以将你的配置存储在config文件中(使用相同的表示法)。

现在我们需要使用数据库中的配置来创建我们的工厂

var discountsDictionary = _repository.GetDiscountCalculatorConfiguration();
 
var factory = new ConfigurableAccountDiscountCalculatorFactory(discountsDictionary);

其中我的 Repository 类中的 GetDiscountCalculatorConfiguration 方法如下所示

public Dictionary<AccountStatus, string> GetDiscountCalculatorConfiguration()
{
    return _context.DiscountCalculatorConfigurationItems.ToDictionary
                    (x => x.AccountStatus, x => x.Implementation);
}

DiscountCalculatorConfigurationItem poco 类将如下所示

public class DiscountCalculatorConfigurationItem
{
    [Key]
    public AccountStatus AccountStatus { get; set; }
    public string Implementation { get; set; }
}

重要的是,我不需要直接将数据库中的整数值映射到 enum 类型!Entity Framework 将为我完成!这难道不棒吗?确实如此!

请注意,EntityFramework 支持 5 版本以上的 enums

如果你使用的 EF 版本低于 5(旧 EFADO.NET、从 config 文件读取),你必须为每个项目映射 整数enum 值。但是这是一个非常简单的转换

(YourEnum)yourIntVariable

我会在工厂的 private 方法中这样做

ConvertStringsDictToObjectsDict 

在将输入字典转换为 private 字典时。

请注意,我们仍然像以前一样在调用者中使用我们的工厂。

重要:即使没有配置计算器的实现,当你尝试创建工厂时,应用程序也会崩溃,因为构造函数正在尝试实例化所有计算器。
所以,如果你的配置中有错误,你将无法运行应用程序。

这种方法在什么情况下有用?看看我们的例子。

想象一下我们有一个排队系统。我们将消息发送到队列。消息包含计算折扣所需的信息(账户状态等)。我们还有一个组件订阅队列,获取消息并计算折扣。

我们的系统中具有以下账户状态

public enum AccountStatus
{
  NotRegistered = 1,
  SimpleCustomer = 2,
  ValuableCustomer = 3,
  MostValuableCustomer = 4,
  SimpleCustomerExtended = 5,
  ValuableCustomerExtended = 6,
  MostValuableCustomerExtended = 7
}

我们还有四种 IAccountDiscountCalculator 接口的实现

  • NotRegisteredDiscountCalculator
  • SimpleCustomerDiscountCalculator
  • ValuableCustomerDiscountCalculator
  • MostValuableCustomerDiscountCalculator

要求是,我们的消息(带有账户状态)最初应该由如下计算器处理

  • NotRegisteredNotRegisteredDiscountCalculator
  • SimpleCustomer - SimpleCustomerDiscountCalculator
  • ValuableCustomer - ValuableCustomerDiscountCalculator
  • MostValuableCustomer - MostValuableCustomerDiscountCalculator

但是过一段时间,我们需要逐步添加对以下状态的支持

  • 迭代 1: SimpleCustomerExtendedSimpleCustomerDiscountCalculator
  • 迭代 2: ValuableCustomerExtendedValuableCustomerDiscountCalculator
  • 迭代 3: MostValuableCustomerExtended - MostValuableCustomerDiscountCalculator

部署过程非常耗时,因为生产服务器运行在多个节点上。我们不想在每次迭代中修改应用程序代码库。

如果我们把工厂配置放在代码外面,我们只需要更改数据库表或配置文件然后重启服务。现在逐个添加对这三个账户状态的支持将更容易、更快。

优点

  • 配置更改不意味着代码库更改
  • 可以通过配置切换 AccountStatusIAccountDiscountCalculatorFactory 实现的赋值

缺点

  • 弱类型配置 – 配置错误(类型定义错误)将在运行时导致错误
  • 在返回的对象具有状态的多线程环境中不起作用
  • 你可能会允许部署团队更改你的代码行为

配置在单独的源中 – 延迟版本

如果你需要两者:代码库外的配置 + 延迟加载 – 这个版本适合你!

public class ConfigurableLazyAccountDiscountCalculatorFactory : 
                                    IAccountDiscountCalculatorFactory
{
    private readonly Dictionary<AccountStatus, Lazy<IAccountDiscountCalculator>> 
                                               _discountsDictionary;
    public ConfigurableLazyAccountDiscountCalculatorFactory
                           (Dictionary<AccountStatus, Type> discountsDictionary)
    {
        _discountsDictionary = ConvertStringsDictToObjectsDict(discountsDictionary);
    }

    public IAccountDiscountCalculator 
           GetAccountDiscountCalculator(AccountStatus accountStatus)
    {
        Lazy<IAccountDiscountCalculator> calculator;

        if (!_discountsDictionary.TryGetValue(accountStatus, out calculator))
        {
            throw new NotImplementedException
            ("There is no implementation of IAccountDiscountCalculatorFactory 
            interface for given Account Status");
        }

        return calculator.Value;
    }

    private Dictionary<AccountStatus, Lazy<IAccountDiscountCalculator>> 
                                      ConvertStringsDictToObjectsDict(
        Dictionary<AccountStatus, Type> dict)
    {
      return dict.ToDictionary(x => x.Key,
          x => new Lazy<IAccountDiscountCalculator>(() => 
               (IAccountDiscountCalculator)Activator.CreateInstance(x.Value)));                
    }
}

如上段代码所示,我对前一个版本的可配置工厂做了一些小的改动。

private 字典的类型已从

Dictionary<AccountStatus, IAccountDiscountCalculator>

to

Dictionary<AccountStatus, Lazy<IAccountDiscountCalculator>>

另一个不同之处在于,工厂的构造函数现在接受类型为

Dictionary<AccountStatus, Type>

因此,工厂的配置现在可以这样

var discountsDictionary = _repository.GetDiscountCalculatorConfiguration().ToDictionary
                          (x=> x.Key, x => Type.GetType(x.Value));

多亏了这种转换

Type.GetType(x.Value)

如果我们犯了实现类型定义的错误,在工厂创建之前就会发生错误。公平竞争!

最重要的是 – ConvertStringsDictToObjectsDict 方法现在正在创建计算器的延迟实例

new Lazy<IAccountDiscountCalculator>(() => 
    (IAccountDiscountCalculator)Activator.CreateInstance(x.Value))

GetAccountDiscountCalculator 方法现在返回字典值(Lazy 类型)的 Value 属性

return calculator.Value;

优点

  • 配置更改不意味着代码库更改
  • 更快的应用程序启动
  • 直到真正需要时才将对象保留在内存中
  • 可以通过配置切换 AccountStatusIAccountDiscountCalculatorFactory 实现的赋值

缺点

  • 弱类型配置 – 配置错误(类型定义错误)将在运行时导致错误
  • 在返回的对象具有状态的多线程环境中不起作用
  • 你可能会允许部署团队更改你的代码行为
  • 从工厂第一次返回对象会比较慢

每次调用返回新实例

在这个解决方案组中,工厂对 IAccountDiscountCalculator 的具体实现的每一次调用,例如

_factory.GetAccountDiscountCalculator(AccountStatus.SimpleCustomer);

都将返回一个新的对象。

基本版本

这个组的第一个工厂版本如下

public class DictionarableAccountDiscountCalculatorFactory : IAccountDiscountCalculatorFactory
{
  private readonly Dictionary<AccountStatus, Type> _discountsDictionary;
  public DictionarableAccountDiscountCalculatorFactory
         (Dictionary<AccountStatus, Type> discountsDictionary)
  {       
    _discountsDictionary = discountsDictionary;
    CheckIfAllValuesFromDictImplementsProperInterface();
  }

  public IAccountDiscountCalculator GetAccountDiscountCalculator(AccountStatus accountStatus)
  {
    Type calculator;

    if (!_discountsDictionary.TryGetValue(accountStatus, out calculator))
    {
      throw new NotImplementedException("There is no implementation of 
            IAccountDiscountCalculatorFactory interface for given Account Status");
    }

    return (IAccountDiscountCalculator)Activator.CreateInstance(calculator);
  }

  private void CheckIfAllValuesFromDictImplementsProperInterface()
  {
    foreach (var item in _discountsDictionary)
    {
      if (!typeof(IAccountDiscountCalculator).IsAssignableFrom(item.Value))
      {
        throw new ArgumentException("The type: " + item.Value.FullName + 
              "does not implement IAccountDiscountCalculatorFactory interface!");
      }
    }
  }
}

你可以注意到,我们的 private dictionary 现在将 Type 类型作为值存储

private readonly Dictionary<AccountStatus, Type> _discountsDictionary;

为什么?

因为我们想在 GetAccountDiscountCalculator 方法中实例化分配给特定 AccountStatus 的类型

return (IAccountDiscountCalculator)Activator.CreateInstance(calculator);

Type 类型允许我们存储应用程序中存在的任何类型,在我们的例子中 – IAccountDiscountCalculator 的实现。

这里,我使用 C# 的 Activator 类来从 Type 类型的变量创建一个新对象。

你还可以在我们的工厂实现中看到另一件事

private void CheckIfAllValuesFromDictImplementsProperInterface()
{
  foreach (var item in _discountsDictionary)
  {
    if (!typeof(IAccountDiscountCalculator).IsAssignableFrom(item.Value))
    {
      throw new ArgumentException("The type: " + item.Value.FullName + 
      "does not implement IAccountDiscountCalculatorFactory interface!");
    }
  }
}

该方法在工厂创建时从构造函数中执行。

我将此检查添加到代码中,因为我们的工厂正在尝试实例化每个计算器实现,并将其强制转换为 IAccountDiscountCalculator 接口。如果我们配置了一个不实现 IAccountDiscountCalculator 的计算器实现,当工厂尝试在返回对象时将该计算器实例强制转换为接口时,我们将收到错误。我们不希望那样!现在,如果发生这种情况,我们将在工厂创建时收到通知。

最后是注入字典的配置

var discountsDictionary = new Dictionary<AccountStatus, Type>
            {
              {AccountStatus.NotRegistered, typeof(NotRegisteredDiscountCalculator)},
              {AccountStatus.SimpleCustomer, typeof(SimpleCustomerDiscountCalculator)},
              {AccountStatus.ValuableCustomer, typeof(ValuableCustomerDiscountCalculator)},
              {AccountStatus.MostValuableCustomer, 
               typeof(MostValuableCustomerDiscountCalculator)}
            };

总而言之,每次调用 GetAccountDiscountCalculator 方法时,工厂将返回一个新对象,该对象在(注入的字典中)分配给 AccountStatus

优点

  • 很简单
  • 强类型配置 – 配置错误(类型定义错误)将在编译时导致错误
  • 在返回的对象具有状态的多线程环境中工作良好

缺点

  • 对象从工厂返回的速度较慢 – 每次调用工厂以获取对象都会创建一个新实例
  • 如果给定的配置类型不实现 IAccountDiscountCalculator,将在运行时发生错误
  • 调用者负责管理工厂返回对象后的生命周期

代码外的配置

此版本展示了一个工厂,它将为每次调用返回一个新对象,并且其配置存储在代码之外 – 这次是在配置文件中。

这次我们从配置文件开始

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="DiscountCalculatorsConfiguration" type=
             "System.Configuration.NameValueSectionHandler" />
  </configSections>
  <DiscountCalculatorsConfiguration>
    <add key="NotRegistered" 
         value="Calculators.NotRegisteredDiscountCalculator, Calculators" />
    <add key="SimpleCustomer" 
         value="Calculators.SimpleCustomerDiscountCalculator, Calculators" />
    <add key="ValuableCustomer" 
         value="Calculators.ValuableCustomerDiscountCalculator, Calculators" />
    <add key="MostValuableCustomer" 
         value="Calculators.MostValuableCustomerDiscountCalculator, Calculators" />
  </DiscountCalculatorsConfiguration>
</configuration>

我创建了一个自定义节并将其命名为 DiscountCalculatorsConfiguration
在该节内,我们有一个键值对的集合 – 我们工厂的定义(与存储在数据库中的配置遵循相同的约定)。
现在我们只需要在创建工厂之前运行此代码

var collection = ConfigurationManager.GetSection("DiscountCalculatorsConfiguration") 
                 as NameValueCollection;
var discountsDictionary = collection.AllKeys.ToDictionary(k => k, k => collection[k]);

并将创建的字典注入到我们新工厂中

public class ConfigurableAccountDiscountCalculatorFactory : IAccountDiscountCalculatorFactory
{
  private readonly Dictionary<AccountStatus, Type> _discountsDictionary;
  public ConfigurableAccountDiscountCalculatorFactory(Dictionary<string, string> 
                                                      discountsDictionary)
  {
    _discountsDictionary = ConvertStringsDictToDictOfTypes(discountsDictionary);
    CheckIfAllValuesFromDictImplementsProperInterface();
  }

  public IAccountDiscountCalculator GetAccountDiscountCalculator(AccountStatus accountStatus)
  {
    Type calculator;

    if (!_discountsDictionary.TryGetValue(accountStatus, out calculator))
    {
        throw new NotImplementedException("There is no implementation of 
              IAccountDiscountCalculatorFactory interface for given Account Status");
    }

    return (IAccountDiscountCalculator)Activator.CreateInstance(calculator);
  }

  private void CheckIfAllValuesFromDictImplementsProperInterface()
  {
    foreach (var item in _discountsDictionary)
    {
      if (!typeof(IAccountDiscountCalculator).IsAssignableFrom(item.Value))
      {
        throw new ArgumentException("The type: " + item.Value.FullName + 
              " does not implement IAccountDiscountCalculatorFactory interface!");
      }
    }
  }

  private Dictionary<AccountStatus, Type> ConvertStringsDictToDictOfTypes(
      Dictionary<string, string> dict)
  {
    return dict.ToDictionary(x => (AccountStatus)Enum.Parse
                            (typeof(AccountStatus), x.Key, true),
        x => Type.GetType(x.Value));
  }
}

请注意,为了处理从配置文件创建的字典,我们必须准备我们的工厂以接受…

Dictionary<string, string>

…作为参数,并将其转换为

Dictionary<AccountStatus, Type>

in

private Dictionary<AccountStatus, Type> ConvertStringsDictToDictOfTypes(
    Dictionary<string, string> dict)
{
  return dict.ToDictionary(x => (AccountStatus)Enum.Parse(typeof(AccountStatus), x.Key, true),
      x => Type.GetType(x.Value));
}

方法的新参数。

GetAccountDiscountCalculator 方法与前一个版本相同。

请注意,即使我们在配置中犯了错误(在定义实现类型时),错误也会在工厂创建时发生(在 ConvertStringsDictToDictOfTypes 方法中)

Type.GetType(x.Value)

如果配置的计算器实现不实现 IAccountDiscountCalculator 接口,那么在工厂创建时也会发生错误 – 请参见 CheckIfAllValuesFromDictImplementsProperInterface 方法。

优点

  • 配置更改不意味着代码库更改
  • 可以通过配置切换 AccountStatusIAccountDiscountCalculatorFactory 实现的赋值
  • 在返回的对象具有状态的多线程环境中工作良好

缺点

  • 弱类型配置 – 配置错误将在运行时导致错误
  • 你可能会允许部署团队更改你的代码行为
  • 对象从工厂返回的速度较慢 – 每次调用工厂以获取对象都会创建一个新实例
  • 调用者负责管理工厂返回对象后的生命周期

结论

在本文中,我介绍了如何解决我上一篇文章中存在的问题 – 工厂违反了以下原则

  • 开闭原则
  • 控制反转原则

问题是由使用 switch-case 语句引起的。在本文中,我使用 dictionary 方法替换了它。
我展示了工厂实现的六种版本,它们涵盖了你在作为开发者工作时会遇到(或已经遇到)的许多不同情况。

非常重要的一点是,得益于“面向接口编程”的方法,我们无需修改除工厂实现之外的任何内容,因为工厂的接口仍然是相同的,它的调用者可以像以前(在上一篇文章中)一样使用它。

总而言之,我们最终得到一个完全可配置的系统,该系统基于代码、数据库或配置文件的配置。我们现在可以轻松地在工厂实现和具体计算器实现之间切换。我们还可以添加对新账户状态的支持,并添加新的计算器实现,而无需修改现有类。

如果您对本文有任何疑问,请随时与我联系。

历史

  • 2016年5月3日:初始版本
© . All rights reserved.