策略模式
定义:
策略模式(Strategy Design Pattern),在GoF的《设计模式》一书中,它的定义是这样的:
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
翻译成中文就是:定义一族算法类,将每个算法分别封装起来,让他们可以相互替换。策略模式可以使算法那的变化独立于使用他们的客户端(这里的客户端指使用算法的代码)。
我们知道,工厂模式是解耦对象的创建和使用,观察者模式是解耦观察者和被观察者。策略模式跟两者类似,也能起到解耦的作用,不过,它解耦的是策略的定义、创建、使用这三部分。
- 策略类的定义比较简单,包含一个策略接口和一组实现了这个接口的策略类
- 策略的创建由工厂类来完成,封装策略创建的细节
- 客户端运行时动态地确定使用哪种策略
今天主要通过一步步地分析、重构,展示一个设计模式是如何“创造”出来的。看完之后你会发现,设计原则和思想其实比设计模式更加普适和重要,掌握了代码的设计原则和思想,我们甚至可以自己创造出来新的设计模式。
话不多说,我们开始吧!
问题与解决思路
假设有这样一个需求,希望写一个程序,实现对一个文件进行排序的功能。文件中只包含整型数,并且,相邻的数字都是通过逗号来分隔。如果让你来写这个程序,你会如何实现呢?
你可能会说,这不是很简单吗,只需要将文件内容读取出来,并且通过逗号分隔成一个一个的数字,放到内存数组中,然后编写某种排序算法(比如快排),或者直接使用编程语言提供的排序函数,对数组进行排序,最后再将数组中的数据写入文件就可以了。
但是,如果文件很大呢,比如10GB大小,因为内存有限(比如只有8GB),我们没有办法一次性加载文件中的所有数据到内存中,这个时候,我们就要利用外部排序算法了。
如果文件更大,比如100GB,我们为了利用CPU多核的优势,可以在外部排序的基础上进行优化,加入多线程并发排序功能,这就有点类似单机版的“MapReduce”。
如果文件非常大,比如1TB大小,即使是单机多线程排序,这也很慢了,这个时候我们可以使用真正的MapReduce框架,利用多机的处理能力,提高排序的效率。
代码实现与分析
思路讲完了,不难理解,接下来,我们看一下,如何将解决思路翻译成代码实现。
我先用最简单的方法将他实现出来。具体代码贴在下面了,因为现在是在讲设计模式,不是讲算法,所以,在下面的代码实现中,只给出了跟设计模式相关的骨架代码,并没有给出每种排序算法的具体代码实现
1 | class SortFile { |
“编码规范”一般规定,函数的行数不能过多,最好不要超过一屏的大小。所以,为了避免 sortFile() 函数过长,我们把每种排序算法从 sortFile() 函数中抽离出来,拆分成 4 个独立的排序函数。
如果只是开发一个简单的工具,那上面的代码实现就足够了。毕竟,代码不多,后续修改、扩展的需求也不多,怎么写都不会导致代码不可维护。但是,如果我们是在开发一个大型项目,排序文件只是其中的一个功能模块,那我们就要在代码设计、代码质量上下点儿功夫了。只有每个小的功能模块都写好,整个项目的代码才能不差。
在刚刚的代码中,我们并没有给出每种排序算法的代码实现。实际上,如果自己实现一下的话,你会发现,每种排序算法的实现逻辑都比较复杂,代码行数都比较多。所有排序算法的代码实现都堆在 SortFile 一个类中,这就会导致这个类的代码很多。一个类的代码太多也会影响到可读性、可维护性。除此之外,所有的排序算法都设计成 SortFile 的私有函数,也会影响代码的可复用性。
代码优化与重构
只要掌握了我们之前讲过的设计原则和思想,针对上面的问题,即便我们想不到该用什么设计模式来重构,也应该能知道该如何解决,那就是将 SortFile 类中的某些代码拆分出来,独立成职责更加单一的小类。实际上,拆分是应对类或者函数代码过多、应对代码复杂性的一个常用手段。按照这个解决思路,我们对代码进行重构。重构之后的代码如下所示:
1 | interface ISort { |
经过拆分之后,每个类的代码都不会太多,每个类的逻辑都不会太复杂,代码的可读性、可维护性提高了。除此之外,我们将排序算法设计成独立的类,跟具体的业务逻辑(代码中的 if-else 那部分逻辑)解耦,也让排序算法能够复用。这一步实际上就是策略模式的第一步,也就是将策略的定义分离出来。
实际上,上面的代码还可以继续优化。我们可以使用工厂模式对对象的创建进行封装。按照这个思路,我们对代码进行重构。重构之后的代码如下所示:
1 | class SortAlgFactory { |
经过上面两次重构之后,现在的代码实际上已经符合策略模式的代码结构了。我们通过策略模式将策略的定义、创建、使用解耦,让每一部分都不至于太复杂。
一提到 if-else 分支判断,有人就觉得它是烂代码。如果 if-else 分支判断不复杂、代码不多,这并没有任何问题,毕竟 if-else 分支判断几乎是所有编程语言都会提供的语法,存在即有理由。遵循 KISS 原则,怎么简单怎么来,就是最好的设计。非得用策略模式,搞出 n 多类,反倒是一种过度设计。
一提到策略模式,有人就觉得,它的作用是避免 if-else 分支判断逻辑。实际上,这种认识是很片面的。策略模式主要的作用还是解耦策略的定义、创建和使用,控制代码的复杂度,让每个部分都不至于过于复杂、代码量过多。除此之外,对于复杂代码来说,策略模式还能让其满足开闭原则,添加新策略的时候,最小化、集中化代码改动,减少引入 bug 的风险。实际上,设计原则和思想比设计模式更加普适和重要。掌握了代码的设计原则和思想,我们能更清楚的了解,为什么要用某种设计模式,就能更恰到好处地应用设计模式。