Answers:
一方面,很难测试if/else
大块的块。每个新的“分支”都会添加另一个执行路径,从而增加循环复杂性。如果要彻底测试代码,则必须涵盖所有执行路径,并且每种情况都将要求您至少再编写一个测试(假设您编写了集中的小型测试)。另一方面,实现策略的类通常仅公开一种公共方法,该方法易于测试。
所以,与嵌套 if/else
您将对代码的单个部分进行许多测试,而使用Strategy时,针对多个较简单的策略中的每种策略,您将仅有很少的测试。有了后者,就很容易获得更好的覆盖范围,因为很难错过执行路径。
至于可扩展性,假设您正在编写一个框架,假定用户应该能够注入自己的行为。例如,您要创建某种税收计算框架,并希望支持不同国家的税收系统。无需实现所有功能,您只是想给框架用户提供实现如何计算某些特定税金的机会。
这是策略模式:
TaxCalculation
,并且您的框架接受此类型的实例来计算税金您不能对进行相同的操作if/else
,因为这将需要更改框架的代码,在这种情况下,它将不再是框架。由于框架通常以编译形式分发,因此这可能是唯一的选择。
尽管如此,即使您只是编写一些常规代码,策略也是有益的,因为它使您的意图更加清晰。它说“此逻辑是可插入的,并且是有条件的”,即可以有多种实现,具体取决于用户的操作,配置甚至平台。
使用策略模式可以提高可读性,因为虽然实现某些特定策略的类通常应具有描述性名称,例如USAIncomeTaxCalculator
,if/else
块是“无名的”,在最佳情况下仅是注释过的,并且可能存在注释。另外,从我个人的喜好来看if/else
,连续读取超过3 个块是不可读的,而嵌套块会变得非常糟糕。
“ 打开/关闭”原理也非常相关,因为,正如我在上面的示例中所描述的,“策略”允许您在代码的某些部分(“扩展为扩展”)中扩展逻辑,而无需重写这些部分(“为修改而闭合”) )。
if/else
块还降低了代码的可读性。至于战略模式,开放/封闭原则值得一提。
if
,你得了,通过您的代码存在多个可能的路径,更多的测试,你必须写多的方式为这个方法失败。如果我要引用已故的瑜伽士贝拉(Yogi Berra):“如果您在路上遇到叉子,那就把它拿走。” 这非常适用于单元测试。此外,许多if
语句意味着您可能会重复这些条件的逻辑,从而进一步增加测试负载并增加出现错误的风险。
if/else
来调用它们(或者在它们内部,以确定是否应该执行某些操作),因此它没有多大帮助,除了可能更具可读性的代码外。然后,您的假设框架的用户也无法扩展。
如果仅在if / then情况下编写代码,使用策略模式为什么有好处?
有时您应该只使用if / then。这是简单易懂的代码。
简单的if / then代码的两个关键问题是它可能违反开放封闭原则。如果您必须进入并添加或更改条件,则可以修改此代码。如果您希望拥有更多条件,则仅添加新策略会更简单/更干净/更不可能被破坏。
另一个问题是耦合。通过使用if / then,所有实现都与该实现相关联,从而使它们将来很难更改。通过使用该策略,唯一的耦合是与策略的接口。
当if/then
条件基于类型时,策略很有用,如http://www.refactoring.com/catalog/replaceConditionalWithPolymorphism.html中所述
类型检查条件通常不会具有很高的圈复杂度,因此我不会说Strategy可以改善那里的情况。
GoF书第316页中介绍了该模式,解释了采用“策略”的主要原因:
在以下情况下使用策略模式
...
- 一个类定义了许多行为,这些行为在其操作中显示为多个条件语句。代替许多条件,将相关的条件分支移到其自己的Strategy类中。
如其他答案中所述,如果适当地应用策略模式,则可以添加新的扩展名(具体策略),而不必修改其余代码。这就是所谓的开闭原则或受保护的变化原则。当然,您仍然必须编写新的具体策略,并且客户端代码必须将这些策略识别为插件(这并非易事)。
对于if/then
条件语句,必须更改包含条件逻辑的类的代码。如其他答案所述,当您不想增加复杂性来支持添加新功能(插件)而无需重新编译时,有时可以这样做。
[...]是否可以在if / then情况下编写代码?
这恰恰是战略模式的最大好处。没有条件。
您希望您的类/方法/函数尽可能简单和简短。短代码非常容易测试和阅读。
条件(if
/ elseif
/ else
)使您的类/方法/函数变长,因为通常一个决策所评估的代码与该决策所评估的代码true
是不同的false
。
策略模式的另一个巨大好处是,它可以在整个项目中重复使用。
当使用策略设计模式时,您很可能拥有某种IoC容器,您可以从中获得所需的接口实现,也许是通过一种getById(int id)
方法(id
可能是枚举成员)获得的。
这意味着,实现的创建仅在代码的一个位置。
如果您想添加更多的实现,则可以将新的实现添加到getById
方法中,并且此更改会在调用它的代码中随处可见。
随着if
/ elseif
/ else
这是不可能做到的。通过添加一个新的实现,您必须添加一个新的elseif
块,并在使用实现的所有位置进行操作,否则最终可能会得到无效的代码,因为您忘记了将实现添加到其结构中。
另外,算法在运行时更改意味着什么?
在我的示例中,id
可以是根据用户输入填充的变量。如果用户点击一个按钮A,那么id = 2
,如果他点击按钮B,然后上id = 8
。
由于id
值不同,因此从IoC容器获得了接口的不同实现,并且代码执行了不同的操作。
if
/ elseif
/ else
状态。和以前一样,只是在不同的地方。
id
在getById
方法中打开变量,这将返回特定的实现。每当您需要接口的实现时,您都将要求IoC容器将其交付给您。
getSortByEnumType(SortEnum type)
返回一个Sort
接口的实现,有一个方法getSortType
返回一个SortEnum
变量并将一个集合作为参数,并且该getSortByEnumType
方法将再次包含一个type
参数开关,以返回正确的排序算法。如果需要添加新的排序算法,则只需编辑枚举和一个方法。而你被设置。
如果仅在if / then情况下编写代码,使用策略模式为什么有好处?
策略模式使您可以将算法(详细信息)与业务逻辑(高级策略)分开。这两件事混合在一起时不仅令人困惑,而且改变的原因也大不相同。
这里还有一个主要的团队可伸缩性因素。设想一个庞大的编程团队,其中许多人都在处理此会计程序包。如果计税算法全部在TaxPayer类或模块中,则合并冲突可能会发生。合并冲突非常耗时且易于出错。这段时间的浪费降低了团队的生产力,而不良合并带来的错误会损害客户的信誉。
另外,算法在运行时更改意味着什么?
一种在运行时更改的算法,其行为由配置或上下文确定。现有的if / then方法无法有效地启用此功能,因为它涉及重新加载现有的活动使用的类。使用策略模式,可以在使用时构造实现每种算法的策略对象。结果,可以对这些算法进行更改(错误修复或增强),并在运行时重新加载。该方法可用于实现连续可用性和零停机时间释放。
if/else
本身没有错。在许多情况下,这if/else
是表达逻辑的最简单,最易读的方式。因此,您描述的方法在许多情况下都是完全有效的。(它也是完全可测试的,因此这不是问题。)
但是在某些特殊情况下,策略模式可以提高整个代码的可维护性。例如:
为了使策略模式有意义,核心逻辑和税费计算算法之间的接口应该比单个组件更稳定。如果需求变更很可能导致界面变更,那么策略模式实际上可能是一种责任。
一切都取决于“税收计算算法”是否可以与调用它的核心逻辑完全分开。与相比,策略模式会有一些开销if/else
,因此您必须根据具体情况确定投资是否值得。