C#坏习惯:通过不好的例子学习如何制作好的代码——第2部分

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/mzl87/article/details/86556352

目录

介绍

这篇文章的目标

Switch case与字典模式

第一个问题

第二个问题

更有力的例子

了解对象的生命周期

有用的字典

每次调用相同的实例

基础(Basic)版本

代码外的配置

在单独的源中配置——延迟(Lazy)版本

每次调用的新实例

基本(Basic)版

代码外的配置

结论


介绍

大家好!这篇文章是我之前文章的延续:

C#坏习惯:通过不好的例子学习如何制作好的代码——第1部分

我强烈建议您在开始阅读本文之前阅读它(我会多次参考)。

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

那么......只是简单地回顾一下我的第一篇文章是关于什么的。我展示了一些可以应用于代码的技术:

  • 更具可读性
  • 更好的可维护性
  • 可扩展

我已经用极其简化的方法展示了它——为了避免在文章中放置大量的代码行(很多人——请注意——没有理解这篇文章,他们认为我正试图用几行代码重构极为简化的方法),因为我认为它会使文章完全不可读。

总而言之,我展示了如何使用一些技术和设计模式,这些模式可以使您和您的同事从公司生活中变得更加容易,当您必须实现复杂的应用程序,这些应用程序可能会长期扩展和维护。

我在一个简化的例子中展示了它,它展示了真实世界功能的实现——折扣计算器。

这篇文章的目标

https://www.clker.com/cliparts/l/7/v/Z/Z/Q/soccer-dog-goal-hi.png

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

然而,正如许多聪明人注意到的,工厂中的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语句。

在我看来,没有一个银弹可以解决每种情况下的这个问题,我决定展示几个版本的实现,并描述每种实现的优缺点。

工厂代码将是本文的基本代码。

感谢隐藏(在上一篇文章中)在抽象(接口)背后的工厂实现:

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

我们将能够在不触及任何其他类的情况下切换到工厂的新实现。

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

是不是接口编程的方法很棒?? !! 当然如此!

https://openclipart.org/image/800px/svg_to_png/101707/happy-pencil.png

Switch case与字典模式

为什么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;
  }
}

我们现在有4个帐户状态。现在想象一下,我们公司正计划增加更多账户。

第一个问题

这意味着每次我们添加对新状态的支持时,我们将不得不通过添加新的case块来修改我们的工厂。这意味着每次更改都可能在我们的类中引入错误,或者可能会破坏现有的单元测试。

第二个问题

我们的工厂也与具体实施紧密结合——它违反了控制反转原责。如果没有工厂修改,我们将无法使用ExtendedSimpleCustomerDiscountCalculator替换SimpleCustomerDiscountCalculator的实现。

更有力的例子

我们可以想象这些问题会更明显的情况。如果我们将有一个巨大的switch-case语句并且每个case块将返回包含给定国家的特定业务逻辑的类的实例,那么我们可以想象,每当我们想要处理一个新的switch-case语句时,我们的switch-case语句都将被扩展一次世界上大约200个国家中的一个)。过了一段时间,我们的switch-case语句将是庞大且难以理解的。两次添加同一个国家会有风险,依此类推。

了解对象的生命周期

https://openclipart.org/image/800px/svg_to_png/213681/shoulder-shrug.png

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

在实现实际解​​决方案时,我们始终必须考虑每个对象的生命周期应如何与整个应用程序相似。

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

我们可以通过使用IOC容器来实现它,并以每次调用我们工厂将返回相同对象的方式对其进行配置。对于我们使用AutoFac库的实例,它的配置如下所示:

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

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

对于我们的计算器实现,如下:

  • NotRegisteredDiscountCalculator
  • SimpleCustomerDiscountCalculator
  • 等等

答案并不明显。

                                         https://www.clker.com/cliparts/2/g/Y/a/7/t/2-way-hi.png

可以有两种管理方式:

  • 我们希望我们的工厂为每次调用返回  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类的新实例。

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

  • 工厂为每个调用返回类  的相同实例
  • 工厂为每个调用返回类的  一个新实例

有用的字典

在下面提出的所有解决方案中,我将使用CDictionary类。但是我将以不同的变量使用它。

https://openclipart.org/image/800px/svg_to_png/185246/dictionary-pictogram.png

我们现在进入具体的实现..

https://openclipart.org/image/800px/svg_to_png/211213/Turn-coffee-into-code.png

 

每次调用相同的实例

在这组解决方案中,每次调用工厂以获取实例的IAccountDiscountCalculator的具体实现:

_factory.GetAccountDiscountCalculator(AccountStatus.SimpleCustomer);

将返回相同的对象。 

基础(Basic)版本

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

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枚举值的实现。

为了使其工作,我们必须在创建它时配置工厂。正如我之前提到的,我们只需要它的一个实例,我们将在应用程序根目录中创建此实例(如果我们想手动注入它)或者在创建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);

 好处:

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

缺点:

  • 当返回的对象具有状态时,将无法在多线程环境中工作

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

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

延迟(Lazy)

https://openclipart.org/image/800px/svg_to_png/195074/Man-Hammock-Colored.png

现在想象一下,我们的计算器的制作非常昂贵。您必须加载繁重的文件,然后将其保存在内存中。但是..我们只需要为应用程序实例使用一个计算器......

这怎么可能?

例如,每个用户都运行单独的Windows窗体应用程序实例,并对具有一个特定帐户状态的客户执行折扣计算。假设Mary正在对简单客户进行计算,而Ted正在对最有价值客户进行计算。然后,不需要在每个应用程序实例中创建所有计算器。

要解决此问题,我们可以通过在C#中使用Lazy类型来实现Lazy Loading模式来改进我们的工厂。 

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 <AccountStatusLazy <IAccountDiscountCalculator >>类型并将其存储在私有字段中。该字典现在将使用延迟IAccountDiscountCalculator实现:) 所以在创建lazyDiscountsDictionary(工厂的配置)时,设置我正在使用的每个项目的值,语法如下:

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

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

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

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

请注意,我们仍然不必更改接口IAccountDiscountCalculator。因此我们可以在调用者类中以与基础(Basic)版本相同的方式使用它。

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

重要的是你应该决定是否真的需要使用延迟加载。有时基本版本可能会更好——例如,当您可以接受这样的情况时,应用程序启动将花费更多的时间,并且不希望延迟从工厂返回对象。 

好处:

  • 更快的应用程序启动
  • 在您真正需要之前,不要将对象保留在内存中
  • 强类型配置——配置错误(错误的类型定义)将在编译时导致错误

缺点:

  • 从工厂返回的第一个对象将更慢
  • 当返回的对象具有状态时,将无法在多线程环境中工作

代码外的配置

如果您需要或更喜欢在代码库之外保持工厂配置(将实现分配给枚举值)——此版本将适合您。

我们可以将配置存储在数据库表或配置文件中,以便更好地适合我们。

我将提供一个示例,当配置存储在数据库表中时,我将使用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)));                
    }
}

您可以注意到工厂的构造函数现在将字符串值的字典作为参数:

Dictionary<AccountStatus,string>

并将其转换为IAccountDiscountCalculator实现的字典:

Dictionary<AccountStatus,IAccountDiscountCalculator>

使用私有方法:

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

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

https://www.codeproject.com/KB/cs/1097145/dbtable.png

字符串值的约定遵循以下模式:

[Namespace].[ClassName], [AssemblyName]

请注意,我将帐户状态保持为整数值,因为数据库显然不支持枚举值。

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

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

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

重要的是我不必将数据库中的数值直接映射到枚举类型!实体框架将为我做!不是很棒吗?是的!

https://openclipart.org/image/800px/svg_to_png/184718/Cool.png

 请注意,EntityFramework 5版本支持枚举 

如果在版本5下不适用EF(旧的EFADO.NET,从配置文件中读取),则必须将整数映射到每个项目的枚举值。然而,这是一个非常简单的转换: 

(YourEnum)yourIntVariable

我会在工厂的私人方法中做到这一点:

ConvertStringsDictToObjectsDict

将输入字典转换为私有字典时。
请注意,我们仍然以与以前相同的方式在调用者中使用我们的工厂。 

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

这种方法有用吗?看看我们的例子。

想象一下,我们有排队系统。我们正在向队列发送消息。该消息包含足够的信息来计算折扣(其他人的帐户状态)。我们还有订阅队列,获取消息和计算折扣的组件。

我们的系统中有以下帐户状态:

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

我们还有4IAccountDiscountCalculator接口的实现:

  • NotRegisteredDiscountCalculator
  • SimpleCustomerDiscountCalculator
  • ValuableCustomerDiscountCalculator
  • MostValuableCustomerDiscountCalculator

要求是我们的消息(具有帐户状态)初始应由计算器处理,如下所示:

  • NotRegistered - NotRegisteredDiscountCalculator
  • SimpleCustomer - SimpleCustomerDiscountCalculator
  • ValuableCustomer - ValuableCustomerDiscountCalculator
  • MostValuableCustomer - MostValuableCustomerDiscountCalculator

但过了一段时间后,我们需要逐步增加对下一个状态的支持:
迭代1
SimpleCustomerExtended - SimpleCustomerDiscountCalculator 
迭代2
ValuableCustomerExtended - ValuableCustomerDiscountCalculator 
迭代3
MostValuableCustomerExtended - MostValuableCustomerDiscountCalculator

由于生产服务器在几个节点下运行,因此部署过程非常耗时。我们不希望在每次迭代中修改应用程序代码库。

如果我们将在代码之外进行工厂配置,我们只需要更改数据库表或配置文件并重新启动服务。现在逐个添加对这3个帐户状态的支持将更容易,更快。

好处:

  • 配置更改并不意味着代码库中的更改
  • 切换分配AccountStatus - IAccountDiscountCalculatorFactory实现可以通过配置完成

缺点:

  • 弱类型配置——配置中的错误(错误的类型定义)将导致运行时出错
  • 当返回的对象具有状态时,将无法在多线程环境中工作
  • 您可能需要允许部署团队更改代码的行为

在单独的源中配置——延迟(Lazy)版本

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

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

正如您在上面的代码中看到的那样,我已经将可配置工厂从之前的版本中稍微改了一下。

私人字典的类型已更改,从:

Dictionary<AccountStatus, IAccountDiscountCalculator>

至:

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型):

return calculator.Value;

 好处:

  • 配置更改并不意味着代码库中的更改
  • 更快的应用程序启动
  • 在您真正需要之前,不要将对象保留在内存中
  • 切换分配AccountStatus - IAccountDiscountCalculatorFactory实现可以通过配置完成

缺点:

  • 弱类型配置——配置中的错误(错误的类型定义)将导致运行时出错
  • 当返回的对象具有状态时,将无法在多线程环境中工作
  • 您可能需要允许部署团队更改代码的行为
  • 从工厂返回的第一个对象将更慢

每次调用的新实例

在这组解决方案中,每个工厂都会调用IAccountDiscountCalculator的具体实现——例如:

_factory.GetAccountDiscountCalculator(AccountStatus.SimpleCustomer);

将返回一个新对象。

基本(Basic)版

该组中工厂的第一个版本如下所示:

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!");
      }
    }
  }
}

您可以注意到我们的私有字典现在将Type类型保留为值:

private readonly Dictionary<AccountStatus, Type> _discountsDictionary;

为什么?

https://openclipart.org/image/800px/svg_to_png/191766/Question-Guy.png

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

return (IAccountDiscountCalculator)Activator.CreateInstance(calculator);

Type 类型使我们能够保持任何类型存在于应用程序中,在我们的例子中,任何类型是——IAccountDiscountCalculator的实现

我在这里使用CActivator类从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>

在:

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方法。

 好处:

  • 配置更改并不意味着代码库中的更改
  • 切换分配AccountStatus - IAccountDiscountCalculatorFactory实现可以通过配置完成
  • 当返回的对象具有状态时,在多线程环境中工作起来就像是一种魅力

缺点:

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

结论

https://openclipart.org/image/800px/svg_to_png/220970/Checkered-Racing-Flags.png

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

  • 开放/封闭原则
  • 反转控制原理

该问题是由使用switch-case语句引起的。在本文中,我使用字典方法替换它。 
我已经介绍了工厂实现的6个版本,它们涵盖了作为开发人员工作时遇到的(或已经做过的)许多不同情况。

非常重要的是,感谢接口编程的方法,我们没有必要修改工厂实现以外的任何东西,因为工厂的接口仍然是相同的,其调用者可以像以前一样使用它(在上一篇文章中)。

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

下一篇:C#坏习惯:通过不好的例子学习如何制作好的代码——第3部分  

 

原文地址:https://www.codeproject.com/Articles/1097145/Csharp-BAD-PRACTICES-Learn-how-to-make-a-good-co

猜你喜欢

转载自blog.csdn.net/mzl87/article/details/86556352