17.3 模 式 讲 解

17.3.1 认识策略模式

1.策略模式的功能

策略模式的功能是把具体的算法实现从具体的业务处理中独立出来,把它们实现成为单独的算法类,从而形成一系列的算法,并让这些算法可以相互替换。

提示

策略模式的重心不是如何来实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。

2.策略模式和if-else语句

看了前面的示例,很多朋友会发现,每个策略算法具体实现的功能,就是原来在if-else结构中的具体实现。

没错,其实多个if-elseif语句表达的就是一个平等的功能结构,你要么执行if,要么执行else,或者是elseif,这个时候,if块中的实现和else块中的实现从运行地位上来讲是平等的。

而策略模式就是把各个平等的具体实现封装到单独的策略实现类了,然后通过上下文来与具体的策略类进行交互。

因此多个if-else语句可以考虑使用策略模式。

3.算法的平等性

策略模式一个很大的特点就是各个策略算法的平等性。对于一系列具体的策略算法,大家的地位是完全一样的,正是因为这个平等性,才能实现算法之间可以相互替换。

所有的策略算法在实现上也是相互独立的,相互之间是没有依赖的。

所以可以这样描述这一系列策略算法:策略算法是相同行为的不同实现。

4.谁来选择具体的策略算法

在策略模式中,可以在两个地方来进行具体策略的选择。

一个是在客户端,当使用上下文的时候,由客户端来选择具体的策略算法,然后把这个策略算法设置给上下文。前面的示例就是这种情况。

还有一个是客户端不管,由上下文来选择具体的策略算法,这个在后面介绍容错恢复的时候给大家演示一下。

5.Strategy的实现方式

在前面的示例中,Strategy都是使用接口来定义的,这也是常见的实现方式。但是如果多个算法具有公共功能的话,可以把Strategy实现成为抽象类,然后把多个算法的公共功能实现到Strategy中。

6.运行时策略的唯一性

运行期间,策略模式在每一个时刻只能使用一个具体的策略实现对象,虽然可以动态地在不同的策略实现中切换,但是同时只能使用一个。

7.增加新的策略

在前面的示例中,体会到了策略模式中切换算法的方便,但是增加一个新的算法会怎样呢?比如现在要实现如下的功能,对于公司的“战略合作客户”,统一8折。

其实很简单,策略模式可以让你很灵活地扩展新的算法。具体的做法是,先写一个策略算法类来实现新的要求,然后在客户端使用的时候指定使用新的策略算法类就可以了。

还是通过示例来说明。先添加一个实现要求的策略类。示例代码如下:

c2b0554da471480fae0d181f95f4357b

然后在客户端指定使用策略的时候指定新的策略算法实现。示例代码如下:

c9da98629c434053bcfd9c40d8d757e2

834c070f73464e27bb964943dd913e1b

运行客户端,测试看看,好好体会一下。

除了客户端发生变化外,已有的上下文、策略接口定义和策略的已有实现,都不需要做任何的修改,可见能很方便地扩展新的策略算法。

8.策略模式的调用顺序示意图

策略模式的调用顺序,有两种常见的情况,一种如同前面的示例,具体如下:

(1)先是客户端来选择并创建具体的策略对象。

(2)然后客户端创建上下文。

(3)接下来客户端就可以调用上下文的方法来执行功能了,在调用的时候,从客户端传入算法需要的参数。

(4)上下文接到客户的调用请求,会把这个请求转发给它持有的Strategy。

这种情况的调用顺序如图17.3所示。

图片

图17.3 策略模式调用顺序示意图一

策略模式的调用还有一种情况,就是把Context当作参数来传递给Strategy,这种方式的调用顺序图,在介绍具体的Context和Strategy的关系时再给出。

17.3.2 Context和Strategy的关系

在策略模式中,通常是上下文使用具体的策略实现对象。反过来,策略实现对象也可以从上下文获取所需要的数据。因此可以将上下文当作参数传递给策略实现对象,这种情况下上下文和策略实现对象是紧密耦合的。

在这种情况下,上下文封装着具体策略对象进行算法运算所需要的数据,具体策略对象通过回调上下文的方法来获取这些数据。

甚至在某些情况下,策略实现对象还可以回调上下文的方法来实现一定的功能,这种使用场景下,上下文变相充当了多个策略算法实现的公共接口。在上下文定义的方法可以当作是所有或者是部分策略算法使用的公共功能。

注意

但是请注意,由于所有的策略实现对象都实现同一个策略接口,传入同一个上下文,可能会造成传入的上下文数据的浪费,因为有的算法会使用这些数据,而有的算法不会使用,但是上下文和策略对象之间交互的开销是存在的。

还是通过例子来说明。

1.工资支付的实现思路

考虑这样一个功能:工资支付方式的问题。很多企业的工资支付方式是很灵活的,可支付方式是比较多的,比如,人民币现金支付、美元现金支付、银行转账到工资账户、银行转账到工资卡;一些创业型的企业为了留住骨干员工,还可能有工资转股权等方式。总之一句话,工资支付方式很多。

随着公司的发展,会不断有新的工资支付方式出现,这就要求能方便地扩展;另外工资支付方式不是固定的,是由公司和员工协商确定的,也就是说可能不同的员工采用的是不同的支付方式,甚至同一个员工,不同时间采用的支付方式也可能会不同,这就要求能很方便地切换具体的支付方式。

要实现这样的功能,策略模式是一个很好的选择。在实现这个功能的时候,不同的策略算法需要的数据是不一样,比如,现金支付就不需要银行账号,而银行转账就需要账号。这就导致在设计策略接口中的方法时,不太好确定参数的个数,而且,就算现在把所有的参数都列上了,今后扩展呢?难道再来修改策略接口吗?如果这样做,那无异于一场灾难,加入一个新策略,就需要修改接口,然后修改所有已有的实现,不疯掉才怪!那么到底如何实现,在今后扩展的时候才最方便呢?

解决方案之一,就是把上下文当作参数传递给策略对象。这样一来,如果要扩展新的策略实现,只需要扩展上下文就可以了,已有的实现不需要做任何的修改。

这样是不是能很好地实现功能,并具有很好的扩展性呢?还是通过代码示例来具体看看。假设先实现人民币现金支付和美元现金支付这两种支付方式,然后进行使用测试,再来添加银行转账到工资卡的支付方式,看看是不是能很容易的与已有地实现结合上。

2.实现代码示例

(1)先定义工资支付的策略接口,也就是定义一个支付工资的方法。示例代码如下:

418682851d424548bb9693d7bd68161f

(2)定义好了工资支付的策略接口,该来考虑如何实现这多种支付策略了。

为了演示的简单,这里先简单实现人民币现金支付和美元现金支付方式,当然并不是真地去实现跟银行的交互,只是示意一下。

人民币现金支付的策略实现。示例代码如下:

5094df443fbe40a9baa64b65e7734631

同样地实现美元现金支付的策略。示例代码如下:

7a699fa04ebb45f0bb71890a1d9f0813

(3)该来看支付上下文的实现了,当然这个使用支付策略的上下文,是需要知道具体使用哪一个支付策略的,一般由客户端来确定具体使用哪一个具体的策略,然后上下文负责去真正执行。因此,这个上下文需要持有一个支付策略,而且是由客户端来配置它。示例代码如下:

9628bd9c638741f1b9f43ef8cf01005a

e96cf6e5e97847aaa744efeadd9aaf37

(4)准备好了支付工资的各种策略,下面来看看如何使用这些策略来真正支付工资。很简单,客户端是使用上下文来使用具体的策略的,而且是客户端来确定具体的策略,就是客户端创建哪个策略,最终就运行哪一个策略,各个策略之间是可以动态切换的。示例代码如下:

7b22e121879f48f6a26198a553ae08d8

59386566b6f9408a903043ceed231f1d

运行一下,看看效果。运行结果如下:

现在给小李人民币现金支付5000.0元

现在给Petter美元现金支付8000.0元

3.扩展示例,实现方式一

经过上面的测试可以看出,通过使用策略模式,已经实现好了两种支付方式了。如果现在要增加一种支付方式,要求能支付到银行卡,该怎样扩展最简单呢?

应该新增加一种支付到银行卡的策略实现,然后通过继承来扩展支付上下文,在其中添加新的支付方式需要的新数据,比如银行卡账户,并在客户端使用新的上下文和新的策略实现就可以了,这样已有的实现都不需要改变,完全遵循开一闭原则。

先看看扩展的支付上下文对象的实现。示例代码如下:

e106a70baff34deb8cf988c842f3fac6

8bf2639715e04e59affc6c77906107cd

然后看看新的策略算法的实现。示例代码如下:

ec01263713ff41c2be3f1f4eadc8f183

最后看看客户端怎么使用这个新的策略呢?原有的代码不变,直接添加新的测试就可以了。示例代码如下:

ed6451b6bbf54b95a99b43a1712a3daa

b55859c195d34438b16a28d47726bdf1

再次测试,体会一下。运行结果如下:

图片

4.扩展示例,实现方式二

同样还是实现上面这个功能:现在要增加一种支付方式,要求能支付到银行卡。

(1)上面这种实现方式,是通过扩展上下文对象来准备新的算法需要的数据。还有另外一种方式,那就是通过策略的构造方法来传入新算法需要的数据。这样实现的话,就不需要扩展上下文了,直接添加新的策略算法实现就可以了。示例代码如下:

f9e07cf619e94b50834151520cb5174d

(2)直接在客户端测试就可以了。示例代码如下:

9d4dbb8c070847b4909829060ebec2a0

3b433a99128c4041a341237177af66f8

运行看看,好好体会一下。

(3)现在有这么两种扩展的实现方式,到底使用哪一种呢?或者是哪种实现更好呢?下面来比较一下。

■ 对于扩展上下文的方式:这样实现,所有策略的实现风格更统一,策略需要的数据都统一从上下文来获取,这样在使用方法上也很统一;另外,在上下文中添加新的数据,别的相应算法也可以用得上,可以视为公共的数据。但缺点也很明显,如果这些数据只有一个特定的算法来使用,那么这些数据有些浪费;另外每次添加新的算法都去扩展上下文,容易形成复杂的上下文对象层次,也未见得有必要。

■ 对于在策略算法的实现上添加自己需要的数据的方式:这样实现,比较好想,实现起来简单。但是缺点也很明显,跟其他策略实现的风格不一致,其他策略都是从上下文中来获取数据,而这个策略的实现一部分数据来自上下文,一部分数据来自自己,有些不统一;另外,这样一来,外部使用这些策略算法的时候也不一样了,难于以一个统一的方式来动态切换策略算法。

两种实现各有优劣,至于如何选择,那就具体问题具体分析了。

5.另一种策略模式调用顺序示意图

策略模式调用还有一种情况,就是把Context当作参数来传递给Strategy,也就是本例示范的这种方式,这个时候策略模式的调用顺序如图17.4所示。

图片

图17.4 策略模式调用顺序示意图二

17.3.3 容错恢复机制

容错恢复机制是应用程序开发中常见的功能。那么什么是容错恢复呢?简单点说就是,程序运行的时候,正常情况下应该按照某种方式来做,如果按照某种方式来做发生错误的话,系统并不会崩溃,也不会就此不能继续向下运行了,而是有容忍出错的能力,不但能容忍程序运行出现错误,还提供出现错误后的备用方案,也就是恢复机制,来代替正常执行的功能,使程序继续向下运行。

举个实际点的例子吧,比如在一个系统中,所有对系统的操作都要有日志记录,而且这个日志还需要有管理界面,这种情况下通常会把日志记录在数据库里面,方便后续的管理,但是在记录日志到数据库的时候,可能会发生错误,比如暂时连不上数据库了,那就先记录在文件里面,然后在合适的时候把文件中的记录再转录到数据库中。

对于这样的功能的设计,就可以采用策略模式,把日志记录到数据库和日志记录到文件当作两种记录日志的策略,然后在运行期间根据需要进行动态的切换。

在这个例子的实现中,要示范由上下文来选择具体的策略算法,而前面的例子都是由客户端选择好具体的算法,然后设置到上下文中。

下面还是通过代码来示例一下。

(1)先定义日志策略接口,很简单,就是一个记录日志的方法。示例代码如下:

6e30daaac33c4f9fb89ac6a2a94f1805

(2)实现日志策略接口。

先实现默认的数据库实现,假设如果日志的长度超过长度就出错,制造错误的是一个最常见的运行期错误。示例代码如下:

d5e09187f60144ce8d986bfcbd96a17c

接下来实现记录日志到文件中去。示例代码如下:

9cf2418950d54472bd7cf9fd4d778c3d

(3)下面来定义使用这些策略的上下文。注意这次是在上下文中实现具体策略算法的选择,所以不需要客户端来指定具体的策略算法了。示例代码如下:

0255cdfe460046f2af412e9d386057f4

(4)看看现在的客户端,没有了选择具体实现策略算法的工作,变得非常简单。故意多调用一次,可以看出不同的效果。示例代码如下:

67a69cbf18d5459e9b21b9ac0e4397a5

5e415b0e65254729bcb7cb35ec31c21f

运行结果如下:

图片

(5)小结一下。通过上面的示例,看到策略模式的一种简单应用,也顺便了解了基本容错恢复机制的设计和实现。在实际的应用中,需要设计容错恢复的系统一般要求都比较高,应用也会更加复杂,但是基本的思路是差不多的。

17.3.4 策略模式结合模板方法模式

在实际应用策略模式的过程中,经常会出现这样一种情况,就是发现这一系列算法的实现上存在公共功能,甚至这一系列算法的实现步骤都是一样的,只是在某些局部步骤上有所不同,这个时候,就需要对策略模式进行些许的变化使用了。

对于一系列算法的实现上存在公共功能的情况,策略模式可以有以下三种实现方式。

■ 在上下文当中实现公共功能,让所有具体的策略算法回调这些方法。

■ 将策略的接口改成抽象类,然后在其中实现具体算法的公共功能。

■ 为所有的策略算法定义一个抽象的父类,让这个父类去实现策略的接口,然后在这个父类中去实现公共的功能。

更进一步,如果这个时候发现“一系列算法的实现步骤都是一样的,只是在某些局部步骤上有所不同”的情况,那就可以在这个抽象类里面定义算法实现的骨架,然后让具体的策略算法去实现变化的部分。这样的一个结构自然就变成了策略模式结合模板方法模式了,那个抽象类就成了模板方法模式的模板类。

在第16章我们讨论过模板方法模式结合策略模式的方式,也就是主要的结构是模板方法模式,局部采用策略模式。而这里讨论的是策略模式结合模板方法模式,也就是主要的结构是策略模式,局部实现上采用模板方法模式。通过这个示例也可以看出,模式之间的结合是没有定势的,要具体问题具体分析。

此时策略模式结合模板方法模式的系统结构如图17.5所示。

图片

图17.5 策略模式结合模板方法模式的结构示意图

还是用实际的例子来说吧,比如上面那个记录日志的例子,如果现在需要在所有的消息前面都添加上日志时间,也就是说现在记录日志的步骤变成了:第一步为日志消息添加日志时间;第二步具体记录日志。

那么该怎么实现呢?

(1)记录日志的策略接口没有变化,为了看起来方便,还是示例一下。示例代码如下:

96ea869f17d14497b540def2eabbe4e1

(2)增加一个实现这个策略接口的抽象类,在其中定义记录日志的算法骨架,相当于模板方法模式的模板。示例代码如下:

373b0025ced34037b35944bbc57b19fb

41c905bed10448f49ac94344a58798d7

(3)这个时候那两个具体的日志算法实现也需要做些改变,不再直接实现策略接口了,而是继承模板,实现模板方法。这个时候记录日志到数据库的类。示例代码如下:

37a82b1d582b49a0bec1aa85bb846179

同理实现记录日志到文件的类如下:

b452c953246843ac8cb316b52605fe63

(4)算法实现的改变不影响使用算法的上下文,上下文和前面一样。示例代码如下:

231fd0f69aae40debec4c5ad1d932d01

a6fc3a39ec5b4b6c995cab8965218b8c

(5)客户端和以前也一样。示例代码如下:

9baa7c6dc62e4ec583372fa6cdbc9b20

运行一下客户端再次测试看看,体会一下,看看结果是否带上了时间。

通过这个示例,好好体会一下策略模式和模板方法模式的组合使用,这在实用开发中是很常见的方式。

17.3.5 策略模式的优缺点

策略模式有以下优点。

■ 定义一系列算法

策略模式的功能就是定义一系列算法,实现让这些算法可以相互替换。所以会为这一系列算法定义公共的接口,以约束一系列算法要实现的功能。如果这一系列算法具有公共功能,可以把策略接口实现成为抽象类,把这些公共功能实现到父类中,对于这个问题,前面讲了三种处理方法,这里就不再啰嗦了。

■ 避免多重条件语句

根据前面的示例会发现,策略模式的一系列策略算法是平等的,是可以互换的,写在一起就是通过if-else结构来组织,如果此时具体的算法实现中又有条件语句,就构成了多重条件语句,使用策略模式能避免这样的多重条件语句。

下面的示例演示了不使用策略模式的多重条件语句。示例代码如下:

d457a0a0566d470a9839c34d90796d60

affbcb2b5c6a4481822096bc0b185bdb

■ 更好的扩展性

在策略模式中扩展新的策略实现非常容易,只要增加新的策略实现类,然后在使用策略的地方选择使用这个新的策略实现就可以了。

策略模式有以下缺点。

■ 客户必须了解每种策略的不同

策略模式也有缺点,比如让客户端来选择具体使用哪一个策略,这就需要客户了解所有的策略,还要了解各种策略的功能和不同,这样才能做出正确的选择,而且这样也暴露了策略的具体实现。

■ 增加了对象数目

由于策略模式把每个具体的策略实现都单独封装成为类,如果备选的策略很多的话,那么对象的数目就会很可观。

■ 只适合扁平的算法结构

策略模式的一系列算法地位是平等的,是可以相互替换的,事实上构成了一个扁平的算法结构,也就是在一个策略接口下,有多个平等的策略算法,就相当于兄弟算法。而且在运行时刻只有一个算法被使用,这就限制了算法使用的层级,使用的时候不能嵌套使用。

对于出现需要嵌套使用多个算法的情况,比如折上折、折后返卷等业务的实现,需要组合或者是嵌套使用多个算法的情况,可以考虑使用装饰模式,或是变形的职责链,或是AOP等方式来实现。

17.3.6 思考策略模式

1.策略模式的本质

   策略模式的本质:分离算法,选择实现。

仔细思考策略模式的结构和实现的功能,会发现,如果没有上下文,策略模式就回到了最基本的接口和实现了,只要是面向接口编程的,那么就能够享受到接口的封装隔离带来的好处。也就是通过一个统一的策略接口来封装和隔离具体的策略算法,面向接口编程的话,自然不需要关心具体的策略实现,也可以通过使用不同的实现类来实例化接口,从而实现切换具体的策略。

看起来好像没有上下文什么事情,但是如果没有上下文,那么就需要客户端来直接与具体的策略交互,尤其是当需要提供一些公共功能,或者是相关状态存储的时候,会大大增加客户端使用的难度。因此,引入上下文还是很必要的,有了上下文,这些工作就由上下文来完成了,客户端只需要与上下文交互就可以了,这样会让整个设计模式更独立、更有整体性,也让客户端更简单。

但纵观整个策略模式实现的功能和设计,它的本质还是“分离算法,选择实现”,因为分离并封装了算法,才能够很容易地修改和添加算法;也能很容易地动态切换使用不同的算法,也就是动态选择一个算法来实现需要的功能。

2.对设计原则的体现

从设计原则上来看,策略模式很好地体现了开一闭原则。策略模式通过把一系列可变的算法进行封装,并定义出合理的使用结构,使得在系统出现新算法的时候,能很容易地把新的算法加入到已有的系统中,而已有的实现不需要做任何修改。这在前面的示例中已经体现出来了,好好体会一下。

从设计原则上来看,策略模式还很好地体现了里氏替换原则。策略模式是一个扁平结构,一系列的实现算法其实是兄弟关系,都是实现同一个接口或者继承的同一个父类。这样只要使用策略的客户保持面向抽象类型编程,就能够使用不同策略的具体实现对象来配置它,从而实现一系列算法可以相互替换。

3.何时选用策略模式

建议在以下情况中选用策略模式。

■ 出现有许多相关的类,仅仅是行为有差别的情况下,可以使用策略模式来使用多个行为中的一个来配置一个类的方法,实现算法动态切换。

■ 出现同一个算法,有很多不同实现的情况下,可以使用策略模式来把这些“不同的实现”实现成为一个算法的类层次。

■ 需要封装算法中,有与算法相关数据的情况下,可以使用策略模式来避免暴露这些跟算法相关的数据结构。

■ 出现抽象一个定义了很多行为的类,并且是通过多个if-else语句来选择这些行为的情况下,可以使用策略模式来代替这些条件语句。

17.3.7 相关模式

■ 策略模式和状态模式

这两个模式从模式结构上看是一样的,但是实现的功能却是不一样的。

状态模式是根据状态的变化来选择相应的行为,不同的状态对应不同的类,每个状态对应的类实现了该状态对应的功能,在实现功能的同时,还会维护状态数据的变化。这些实现状态对应的功能的类之间是不能相互替换的。策略模式是根据需要或者是客户端的要求来选择相应的实现类,各个实现类是平等的,是可以相互替换的。另外策略模式可以让客户端来选择需要使用的策略算法;而状态模式一般是由上下文,或者是在状态实现类里面来维护具体的状态数据,通常不由客户端来指定状态。

■ 策略模式和模板方法模式

这两个模式可组合使用,如同前面示例的那样。

模板方法重在封装算法骨架;而策略模式重在分离并封装算法实现。

■ 策略模式和享元模式

这两个模式可组合使用。

策略模式分离并封装出一系列的策略算法对象,这些对象的功能通常都比较单一,很多时候就是为了实现某个算法的功能而存在。因此,针对这一系列的、多个细粒度的对象,可以应用享元模式来节省资源,但前提是这些算法对象要被频繁地使用,如果偶尔用一次,就没有必要做成享元了。