策略模式
意图
定义一系列的算法,将它们一个个封装起来,并且使得它们可以相互替换。策略模式使得算法可以独立使用与它的用户而变化。
举例
在我们使用的许多编辑器乃至编译器中,我们通常可以看到一些不同的换行符算法,有windows的\r\n,linux的\n,Mac用的\r等。将这些个换行符算法硬编码进我们使用的编辑器或者编译器软件中是不可取的。实际你也可以看到,基本所有的编辑器和编译器都有自己的切换换行符的功能。那么,原因是什么呢?
原因
- 复杂性增加
- 如果将换行算法直接嵌入到需要换行功能的程序中,会导致代码臃肿且难以维护。
- 支持多种换行算法的需求进一步加剧了这一问题。
- 灵活性受限
- 如果需要更改换行算法,不希望仍然支持之前的算法。例如,从 Windows 换行标准切换到 Linux 或 macOS 的标准,可能希望只支持新的规则。
- 继续支持不需要的旧规则可能导致解析问题。
- 扩展性受限
- 当功能被硬编码后,添加新算法或修改现有算法变得十分困难。
分析
也就是说,我们策略模式出现的动机就是为了解决上诉这种硬编码会导致的缺陷。所谓策略,就是在一个人面对不同事情时选择的不同应对方法,就比如你在window平台上设计时需要去使用window的换行符,你在linux平台开发时需要去使用linux的换行符。这些都是一种策略模式的体现。
也就是说,策略模式最本质的作用,就是通过一种策略来代替一种硬编码的步骤,使得用户能够自己选择当前要使用的方法,或者程序自己选择要使用的方法以取代全部的一种硬编码程序。也就是说,这是一种对于程序的进一步的解耦。
通过解耦算法的选择与其具体实现来提高程序的灵活性和可维护性。
就拿上面那个换行符算法举例,当我们使用策略模式来设计我们的编辑器后,我们能够在编辑器中去自由选择我们当前想要使用的换行符版本,这种动态选择提供了很高的灵活性,显著的提升了我们使用时的灵活性,将这个扩展到我们的类的设计中也是如此。
而且我们在考虑一下,如果我们使用策略模式来设计我们的换行符算法,那就意味着如果我们后续还需要对这个换行符算法进行添加或者修改的话,我们只需要去修改对应的区域或者添加新的算法即可,不必去修改现有的选择模块,这也是一种策略模式下的原则遵守和优势。
UML类图
百闻不如一见,UML类图,启动
首先,策略模式的UML类图是相对简单的,毕竟从本质来看,它关注的其实主要就来个模块,一个模块时选择模块,另一个是算法模块,选择模块时面向用户层的接口,通过这个模块隔离了用户层与算法实现,同时提供了用户选择算法的方式。
至于算法层,只需要关注自身的实现,关注与选择模块的接口对接即可。
1 | +----------------+ +----------------+ |
更为简单的一个UML类图。虽然有点糙,凑活这看吧
类图要点:
Context
(选择模块):持有Strategy
的引用,用于调用具体算法。Strategy
(算法模块):定义算法的统一接口,提供一个抽象的算法约束。ConcreteStrategyA
、ConcreteStrategyB
(具体算法实现):实现Strategy
接口,分别提供具体的算法逻辑。
在这里我们在选择模块中没有列出引用这个属性,但是我们使用这个聚合关系来表示了。不对,你都看到这里了,不应该不知道这个啊,我还写个蛋啊。
在这个类图中其实没有显示出到底是一种什么个选择的逻辑,这个是等下我们的代码的工作。在这个类图中,我们主要来分析下我们设计的解耦性。
设计的解耦性
对用户:
用户只需与
Context
交互,完全无需了解具体算法的实现逻辑。Context
提供了灵活的接口,让用户能够动态选择策略或依赖默认策略。这种灵活的选择可能是用户自己的选择也可能是程序自己的旋转,但是一般来说,这种选择都不是有if-else实现的,毕竟你策略模式的一种存在意义就是代替这种令人反感的长嵌套语句你可能会疑惑那得怎么动态选择,别急,接着看
对算法:
- 各种算法被封装为独立类,彼此互不干扰。它们只需实现
Strategy
接口,与Context
保持对接即可。 - 新增算法时,只需增加一个新的具体策略类,无需修改
Context
和其他策略。
- 各种算法被封装为独立类,彼此互不干扰。它们只需实现
代码实例
实践才是检验真理的唯一方法
其实这里给的代码实例大部分还是为了写而写的,所以其实并没有什么实际应用的价值。而这些存在的意义是给你阐述这些的存在,并在实际的应用中去尝试使用,就比如,我学完策略模式将会把我课设中的数据模块再次进行解耦,将其分割为可以单步执行渲染和一直渲染到底的俩种策略模式,不过那都是后话了,我们先来看代码。
对比示例
使用 if-else
的实现(不推荐)
1 |
|
缺点:
printLineBreak
方法中耦合了换行逻辑的选择和实现,扩展性差,破坏了单一职责原则。增加新的操作系统时,必须修改方法内部的条件分支,破坏了开放封闭原则。
我们在修改时必须对所有的算法都进行维护,可能需要进行全部的实例化占用空间,也可能会使用懒加载机制来复杂使用逻辑。
使用策略模式的实现(推荐)
1 |
|
优势:
- 避免了
if-else
的分支判断,代码简洁清晰。 - 新增策略只需添加新的策略类,无需修改现有代码。
- 动态选择和切换策略,灵活性更强。
那么,代价是什么?
从上面来段代码我们其实可以看到最直观的一点就是,策略模式设计下的程序相对来说会存在更多的子类,这些子类都是对应的子算法类,这样会导致这个程序看起来很臃肿。
同时,无论是使不使用策略模式下的设计,我们其实都会存在一种问题,就是我们如果要选择一种算法,我们就必须知道一种算法的存在,这个其实就跟我们平时使用的很多软件一样,特别是编译器这种集成度很高的软件。
通常来说,为了集成到图形化界面上并提供一些多元化的选择,设计者会将功能的选择绑定到按钮上,而程序中的交互按钮又乱七八糟,很多按钮对很多用户来说只是个黑盒,这无疑还是会提高用户的使用难度。
而且在很多情况下,我们可能会碰到一些策略算法类之间存在部分逻辑重叠的情况,这会导致代码的重复,不过这种也可以通过基类的方法提取来解决。
我为什么提出这一点,其实是为了说明在我们提出一个模式的缺点后,我们应该做的不是去记住这些缺点然后接受它,而是应该通过我们已有的或者将来可能会有的知识来进行我们现有的缺点的一些个隐藏,这个才是举出缺点的意义。而不是,好,这个模式有这些缺点,那我就记住它,当我被问到这些时,我一五一十的去吧这些缺点说出来,这是没有意义的。
但是,总的来说,策略模式的这种优化,还是利大于弊的。因为设计模式关注的过程大多还是程序开发的这个过程,策略模式的设计思想本质就是让每种策略自成一体,通过接口与上下文对接,从而取代臃肿的条件分支语句,使代码更加模块化、可扩展和易维护。
总结
策略模式的核心思想是通过封装变化和动态选择策略,来提升代码的灵活性和可维护性。尽管它可能会增加类的数量和一定的设计复杂度,但其在算法扩展性和代码清晰度上的优势使得它在实际开发中广受欢迎。
通过合理设计和优化,策略模式能够很好地实现高内聚、低耦合的设计目标,帮助我们构建更易维护、更具扩展性的系统。