目录
介绍
这篇文章的目标
Switch case与字典模式
第一个问题
第二个问题
更有力的例子
了解对象的生命周期
有用的字典
每次调用相同的实例
基础(Basic)版本
代码外的配置
在单独的源中配置——延迟(Lazy)版本
每次调用的新实例
基本(Basic)版
代码外的配置
结论
介绍大家好!这篇文章是我之前文章的延续:
C#坏习惯:通过不好的例子学习如何制作好的代码——第1部分
我强烈建议您在开始阅读本文之前阅读它(我会多次参考)。
我注意到很多人发现我的第一篇文章很有帮助,所以我决定写第二篇文章。
那么......只是简单地回顾一下我的第一篇文章是关于什么的。我展示了一些可以应用于代码的技术:
- 更具可读性
- 更好的可维护性
- 可扩展
我已经用极其简化的方法展示了它——为了避免在文章中放置大量的代码行(很多人——请注意——没有理解这篇文章,他们认为我正试图用几行代码重构极为简化的方法),因为我认为它会使文章完全不可读。
总而言之,我展示了如何使用一些技术和设计模式,这些模式可以使您和您的同事从公司生活中变得更加容易,当您必须实现复杂的应用程序,这些应用程序可能会长期扩展和维护。
我在一个简化的例子中展示了它,它展示了真实世界功能的实现——折扣计算器。
这篇文章的目标在上一篇文章中,我最终得到了一个干净且可维护的解决方案。
然而,正如许多聪明人注意到的,工厂中的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 类或其他调用者(如果需要)中。
是不是“接口编程”的方法很棒?? !! 当然如此!
为什么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语句将是庞大且难以理解的。两次添加同一个国家会有风险,依此类推。
了解对象的生命周期在我开始为我们的问题展示解决方案之前,我想谈谈我们将要创建的对象的生命周期。
在实现实际解决方案时,我们始终必须考虑每个对象的生命周期应如何与整个应用程序相似。
由于我们不需要工厂的多个实例(一个实例可以为每个线程的每次调用返回对象),我们应该为每个应用程序只创建一个实例。
我们可以通过使用IOC容器来实现它,并以每次调用我们工厂将返回相同对象的方式对其进行配置。对于我们使用AutoFac库的实例,它的配置如下所示:
var builder = new ContainerBuilder();
builder.RegisterType().As().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类的新实例。
我将我的解决方案分为两组:
- 工厂为每个调用返回类 的相同实例
- 工厂为每个调用返回类的 一个新实例
在下面提出的所有解决方案中,我将使用C#Dictionary类。但是我将以不同的变量使用它。
我们现在进入具体的实现..
每次调用相同的实例
在这组解决方案中,每次调用工厂以获取实例的IAccountDiscountCalculator的具体实现:
_factory.GetAccountDiscountCalculator(AccountStatus.SimpleCustomer);
将返回相同的对象。
基础(Basic)版本我的解决方案的第一个版本是一个简单的工厂
public class DictionarableAccountDiscountCalculatorFactory : IAccountDiscountCalculatorFactory
{
private readonly Dictionary _discountsDictionary;
public DictionarableAccountDiscountCalculatorFactory(Dictionary 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.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().As()
.WithParameter("discountsDictionary", discountsDictionary)
.SingleInstance();
现在,我们可以像以前一样使用我们的工厂注入调用者(参见上一篇文章):
priceAfterDiscount = _factory.GetAccountDiscountCalculator(accountStatus).ApplyDiscount(price);
好处:
- 这很简单
- 强类型配置——配置错误(错误的类型定义)将在编译时导致错误
- 从工厂返回对象非常快——他们已经创建了
缺点:
- 当返回的对象具有状态时,将无法在多线程环境中工作
在我看来,这是我们正在考虑的最合适的功能版本——简单折扣计算器示例。
但是,我想展示我们如何处理不同的情况......
延迟(Lazy)版
现在想象一下,我们的计算器的制作非常昂贵。您必须加载繁重的文件,然后将其保存在内存中。但是..我们只需要为应用程序实例使用一个计算器......
这怎么可能?
例如,每个用户都运行单独的Windows窗体应用程序实例,并对具有一个特定帐户状态的客户执行折扣计算。假设Mary正在对简单客户进行计算,而Ted正在对最有价值客户进行计算。然后,不需要在每个应用程序实例中创建所有计算器。
要解决此问题,我们可以通过在C#中使用Lazy类型来实现Lazy Loading模式来改进我们的工厂。
Lazy类型是在.NET Framework 4版本中引入的。如果在.NET Framework上使用旧版本,则必须手动实现延迟加载。
我们工厂的实施现在如下:
public class DictionarableLazyAccountDiscountCalculatorFactory : IAccountDiscountCalculatorFactory
{
private readonly Dictionary _discountsDictionary;
public DictionarableLazyAccountDiscountCalculatorFactory(Dictionary discountsDictionary)
{
_discountsDictionary = discountsDictionary;
}
public IAccountDiscountCalculator GetAccountDiscountCalculator(AccountStatus accountStatus)
{
Lazy 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.NotRegistered, new Lazy(() => new NotRegisteredDiscountCalculator()) },
{AccountStatus.SimpleCustomer, new Lazy(() => new SimpleCustomerDiscountCalculator())},
{AccountStatus.ValuableCustomer, new Lazy(() => new ValuableCustomerDiscountCalculator())},
{AccountStatus.MostValuableCustomer, new Lazy(() => new MostValuableCustomerDiscountCalculator())}
};
var factory = new DictionarableLazyAccountDiscountCalculatorFactory(lazyDiscountsDictionary);
我们在这里做的很少。
我们的延迟工厂的构造函数现在作为参数Dictionary 类型并将其存储在私有字段中。该字典现在将使用延迟IAccountDiscountCalculator实现:) 所以在创建lazyDiscountsDictionary(工厂的配置)时,设置我正在使用的每个项目的值,语法如下:
new Lazy(() => new NotRegisteredDiscountCalculator())
我们使用的是Lazy类的构造函数 ,它将 Func委托作为参数。当我们第一次尝试访问存储在我们的字典valuen(Lazy类型)中的 Value属性时,将执行该委托并创建具体的计算器实现。
其余部分与第一个示例相同。
现在,具体的计算器实现将在第一次请求执行后创建。对于相同实现的每个下一次调用将返回相同的(在第一次调用时创建)对象。
请注意,我们仍然不必更改接口IAccountDiscountCalculator。因此我们可以在调用者类中以与基础(Basic)版本相同的方式使用它。
priceAfterDiscount = _factory.GetAccountDiscountCalculator(accountStatus).ApplyDiscount(price);
重要的是你应该决定是否真的需要使用延迟加载。有时基本版本可能会更好——例如,当您可以接受这样的情况时,应用程序启动将花费更多的时间,并且不希望延迟从工厂返回对象。
好处:
- 更快的应用程序启动
- 在您真正需要之前,不要将对象保留在内存中
- 强类型配置——配置错误(错误的类型定义)将在编译时导致错误
缺点:
- 从工厂返回的第一个对象将更慢
- 当返回的对象具有状态时,将无法在多线程环境中工作
如果您需要或更喜欢在代码库之外保持工厂配置(将实现分配给枚举值)——此版本将适合您。
我们可以将配置存储在数据库表或配置文件中,以便更好地适合我们。
我将提供一个示例,当配置存储在数据库表中时,我将使用Entity Framework Code First从数据库中获取此配置并将其注入工厂。
我们的工厂实现如下:
public class ConfigurableAccountDiscountCalculatorFactory : IAccountDiscountCalculatorFactory
{
private readonly Dictionary _discountsDictionary;
public ConfigurableAccountDiscountCalculatorFactory(Dictionary 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 ConvertStringsDictToObjectsDict(
Dictionary dict)
{
return dict.ToDictionary(x => x.Key,
x => (IAccountDiscountCalculator)Activator.CreateInstance(Type.GetType(x.Value)));
}
}
您可以注意到工厂的构造函数现在将字符串值的字典作为参数:
Dictionary
并将其转换为IAccountDiscountCalculator实现的字典:
Dictionary
使用私有方法:
private Dictionary ConvertStringsDictToObjectsDict(
Dictionary dict)
{
return dict.ToDictionary(x => x.Key,
x => (IAccountDiscountCalculator)Activator.CreateInstance(Type.GetType(x.Value)));
}
存储在数据库表中的配置如下所示:
字符串值的约定遵循以下模式:
[Namespace].[ClassName], [AssemblyName]
请注意,我将帐户状态保持为整数值,因为数据库显然不支持枚举值。
您还可以将配置存储在配置文件中(使用相同的表示法)。
现在我们需要使用数据库中的配置来创建工厂:
var discountsDictionary = _repository.GetDiscountCalculatorConfiguration();
var factory = new ConfigurableAccountDiscountCalculatorFactory(discountsDictionary);
我的Repository类中的GetDiscountCalculatorConfiguration方法如下所示:
public Dictionary 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; }
}
重要的是我不必将数据库中的整数值直接映射到枚举类型!实体框架将为我做!不是很棒吗?是的!
请注意,EntityFramework 5版本支持枚举!
如果在版本5下不适用EF(旧的EF,ADO.NET,从配置文件中读取),则必须将整数映射到每个项目的枚举值。然而,这是一个非常简单的转换:
(YourEnum)yourIntVariable
我会在工厂的私人方法中做到这一点:
ConvertStringsDictToObjectsDict
将输入字典转换为私有字典时。请注意,我们仍然以与以前相同的方式在调用者中使用我们的工厂。
重要提示:即使没有配置计算器的实现,当您尝试创建工厂时,应用程序也会中断,因为构造函数正在尝试实例化所有计算器。因此,如果配置中存在错误,您将无法运行该应用程序。
这种方法有用吗?看看我们的例子。
想象一下,我们有排队系统。我们正在向队列发送消息。该消息包含足够的信息来计算折扣(其他人的帐户状态)。我们还有订阅队列,获取消息和计算折扣的组件。
我们的系统中有以下帐户状态:
public enum AccountStatus
{
NotRegistered = 1,
SimpleCustomer = 2,
ValuableCustomer = 3,
MostValuableCustomer = 4,
SimpleCustomerExtended = 5,
ValuableCustomerExtended = 6,
MostValuableCustomerExtended = 7
}
我们还有4个IAccountDiscountCalculator接口的实现:
- NotRegisteredDiscountCalculator
- SimpleCustomerDiscountCalculator
- ValuableCustomerDiscountCalculator
- MostValuableCustomerDiscountCalculator
要求是我们的消息(具有帐户状态)初始应由计算器处理,如下所示:
- NotRegistered - NotRegisteredDiscountCalculator
- SimpleCustomer - SimpleCustomerDiscountCalculator
- ValuableCustomer - ValuableCustomerDiscountCalculator
- MostValuableCustomer - MostValuableCustomerDiscountCalculator
但过了一段时间后,我们需要逐步增加对下一个状态的支持:迭代1:SimpleCustomerExtended - SimpleCustomerDiscountCalculator 迭代2:ValuableCustomerExtended - ValuableCustomerDiscountCalculator 迭代3:MostValuableCustomerExtended - MostValuableCustomerDiscountCalculator
由于生产服务器在几个节点下运行,因此部署过程非常耗时。我们不希望在每次迭代中修改应用程序代码库。
如果我们将在代码之外进行工厂配置,我们只需要更改数据库表或配置文件并重新启动服务。现在逐个添加对这3个帐户状态的支持将更容易,更快。
好处:
- 配置更改并不意味着代码库中的更改
- 切换分配AccountStatus - IAccountDiscountCalculatorFactory实现可以通过配置完成
缺点:
- 弱类型配置——配置中的错误(错误的类型定义)将导致运行时出错
- 当返回的对象具有状态时,将无法在多线程环境中工作
- 您可能需要允许部署团队更改代码的行为
如果你都需要:代码库外的配置+延迟加载——这个版本适合你!
public class ConfigurableLazyAccountDiscountCalculatorFactory : IAccountDiscountCalculatorFactory
{
private readonly Dictionary _discountsDictionary;
public ConfigurableLazyAccountDiscountCalculatorFactory(Dictionary discountsDictionary)
{
_discountsDictionary = ConvertStringsDictToObjectsDict(discountsDictionary);
}
public IAccountDiscountCalculator GetAccountDiscountCalculator(AccountStatus accountStatus)
{
Lazy 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 ConvertStringsDictToObjectsDict(
Dictionary dict)
{
return dict.ToDictionary(x => x.Key,
x => new Lazy(() => (IAccountDiscountCalculator)Activator.CreateInstance(x.Value)));
}
}
正如您在上面的代码中看到的那样,我已经将可配置工厂从之前的版本中稍微改了一下。
私人字典的类型已更改,从:
Dictionary
至:
Dictionary
另一个区别是我们工厂的构造函数现在采用类型的参数:
Dictionary
所以工厂的配置现在看起来像这样:
var discountsDictionary = _repository.GetDiscountCalculatorConfiguration().ToDictionary(x=> x.Key, x => Type.GetType(x.Value));
感谢这个转换:
Type.GetType(x.Value)
如果我们在实现类型定义中出错,则在创建工厂之前会发生错误。公平竞争!
最重要的是——ConvertStringsDictToObjectsDict方法现在正在创建计算器的延迟实例:
new Lazy(() => (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 _discountsDictionary;
public DictionarableAccountDiscountCalculatorFactory(Dictionary 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 _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.NotRegistered, typeof(NotRegisteredDiscountCalculator)},
{AccountStatus.SimpleCustomer, typeof(SimpleCustomerDiscountCalculator)},
{AccountStatus.ValuableCustomer, typeof(ValuableCustomerDiscountCalculator)},
{AccountStatus.MostValuableCustomer, typeof(MostValuableCustomerDiscountCalculator)}
};
总而言之,每次我们调用GetAccountDiscountCalculator方法时,工厂都会返回一个新的对象(在注入的字典中)分配给AccountStatus。
好处:
- 这很简单
- 强类型配置——配置错误(错误的类型定义)将在编译时导致错误
- 当返回的对象具有状态时,在多线程环境中工作起来就像是一种魅力
缺点:
- 对象从工厂返回的速度较慢——每次对工厂的调用都会创建一个新实例
- 如果给定的配置类型未实现IAccountDiscountCalculator,则运行时将发生错误
- 调用者负责在工厂返回后管理对象的生命周期
此版本提供了一个工厂,它将为每个调用返回一个新对象,并将配置存储在代码之外——这次是在配置文件中。
让我们从配置文件开始:
我创建了一个自定义部分,并将其命名为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 _discountsDictionary;
public ConfigurableAccountDiscountCalculatorFactory(Dictionary 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 ConvertStringsDictToDictOfTypes(
Dictionary dict)
{
return dict.ToDictionary(x => (AccountStatus)Enum.Parse(typeof(AccountStatus), x.Key, true),
x => Type.GetType(x.Value));
}
}
请注意,要处理从配置文件字典创建的句柄,我们必须准备我们的工厂
Dictionary
作为参数并将其转换为:
Dictionary
在:
private Dictionary ConvertStringsDictToDictOfTypes(
Dictionary 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实现可以通过配置完成
- 当返回的对象具有状态时,在多线程环境中工作起来就像是一种魅力
缺点:
- 弱类型配置——配置中的错误将导致运行时出错
- 您可能需要允许部署团队更改代码的行为
- 对象从工厂返回的速度较慢——每次对工厂的调用都会创建一个新实例
- 调用者负责在工厂返回后管理对象的生命周期
在本文中,我介绍了我如何看待上一篇文章中存在的问题的解决方案——工厂违反了以下原则:
- 开放/封闭原则
- 反转控制原理
该问题是由使用switch-case语句引起的。在本文中,我使用字典方法替换它。 我已经介绍了工厂实现的6个版本,它们涵盖了作为开发人员工作时遇到的(或已经做过的)许多不同情况。
非常重要的是,感谢“接口编程”的方法,我们没有必要修改工厂实现以外的任何东西,因为工厂的接口仍然是相同的,其调用者可以像以前一样使用它(在上一篇文章中)。
总而言之,我们最终得到了完全可配置的系统,该系统基于代码,数据库或配置文件中的配置。我们现在可以轻松地在工厂实现和具体计算器实现之间切换。我们还可以添加对新帐户状态的支持,并添加新的计算器实现,而无需修改现有类。
下一篇:C#坏习惯:通过不好的例子学习如何制作好的代码——第3部分
原文地址:https://www.codeproject.com/Articles/1097145/Csharp-BAD-PRACTICES-Learn-how-to-make-a-good-co