13.3 模 式 讲 解
13.3.1 认识命令模式
1.命令模式的关键
命令模式的关键之处就是把请求封装成为对象,也就是命令对象,并定义了统一的执行操作的接口,这个命令对象可以被存储、转发、记录、处理、撤销等,整个命令模式都是围绕这个对象在进行。
2.命令模式的组装和调用
在命令模式中经常会有一个命令的组装者,用它来维护命令的“虚”实现和真实实现之间的关系。如果是超级智能的命令,也就是说命令对象自己完全实现好了,不需要接收者,那就是命令模式的退化,不需要接收者,自然也不需要组装者了。
而真正的用户就是具体化请求的内容,然后提交请求进行触发就可以了。真正的用户会通过Invoker来触发命令。
在实际开发过程中,Client和Invoker可以融合在一起,由客户在使用命令模式的时候,先进行命令对象和接收者的组装,组装完成后,就可以调用命令执行请求。
3.命令模式的接收者
接收者可以是任意的类,对它没有什么特殊要求,这个对象知道如何真正执行命令的操作,执行时是从Command的实现类里面转调过来。
一个接收者对象可以处理多个命令,接收者和命令之间没有约定的对应关系。接收者提供的方法个数、名称、功能和命令中的可以不一样,只要能够通过调用接收者的方法来实现命令对应的功能就可以了。
4.智能命令
在标准的命令模式里面,命令的实现类是没有真正实现命令要求的功能的,真正执行命令的功能的是接收者。
如果命令的实现对象比较智能,它自己就能真实地实现命令要求的功能,而不再需要调用接收者,那么这种情况就称为智能命令。
也可以有半智能的命令,命令对象知道部分实现,其他的还是需要调用接收者来完成,也就是说命令的功能由命令对象和接收者共同来完成。
5.发起请求的对象和真正实现的对象是解耦的
请求究竟由谁处理?如何处理?发起请求的对象是不知道的,也就是发起请求的对象和真正实现的对象是解耦的。发起请求的对象只管发出命令,其他的就不管了。
6.命令模式的调用顺序示意图
使用命令模式的过程分成两个阶段,一个阶段是组装命令对象和接收者对象的过程;另外一个阶段是触发调用Invoker,来让命令真正执行的过程。
先看看组装过程的调用顺序示意图,如图13.4所示。
图13.4 命令模式组装过程的调用顺序示意图
接下来再看看真正执行命令时的调用顺序示意图,如图13.5所示。
13.3.2 参数化配置
所谓命令模式的参数化配置,指的是:可以用不同的命令对象,去参数化配置客户的请求。
像前面描述的那样:客户按下一个按钮,到底是开机还是重启,那要看参数化配置的是哪一个具体的按钮对象。如果参数化的是开机的命令对象,那就执行开机的功能;如果参数化的是重启的命令对象,那就执行重启的功能。虽然按下的是同一个按钮,相当于是同一个请求,但是为请求配置不同的按钮对象,就会执行不同的功能。
把这个功能用代码实现出来,一起来体会一下命令模式的参数化配置。
(1)先定义主板接口。现在想要添加一个重启的按钮,因此主板需要添加一个方法来实现重启的功能。示例代码如下:
接口发生了改变,实现类也需要相应地改变。由于两个主板的实现示意差不多,因此这里只示例一个。示例代码如下:
(2)接下来定义命令和按钮。命令接口没有任何变化,原有的开机命令的实现也没有任何变化,只是新添加了一个重启命令的实现。示例代码如下:
(3)持有命令的机箱也需要修改。现在不只一个命令按钮了,有两个了,所以需要在机箱类里面新添加重启的按钮,为了简单,没有做成集合。示例代码如下:
(4)看看客户如何使用这两个按钮。示例代码如下:
运行一下看看,很有意思。结果如下:
13.3.3 可撤销的操作
可撤销操作的意思就是:放弃该操作,回到未执行该操作前的状态。这个功能是一个非常重要的功能,几乎所有的GUI应用中都有撤销操作的功能。GUI的菜单是命令模式最典型的应用之一,所以总是能在菜单上找到撤销这样的菜单项。
既然这么常用,那该如何实现呢?
有两种基本的思路来实现可撤销的操作,一种是补偿式,又称反操作式,比如被撤销的操作是加的功能,那撤销的实现就变成减的功能;同理被撤销的操作是打开的功能,那么撤销的实现就变成关闭的功能。
另外一种方式是存储恢复式,意思就是把操作前的状态记录下来,然后要撤销操作的时候就直接恢复回去就可以了。
提示
这里先讲第一种方式,就是补偿式或者反操作式,第二种方式放到备忘录模式中进行讲解,详见19.3.4。
为了让大家更好地理解可撤销操作的功能,还是用一个例子来说明会比较清楚。
1.范例需求
考虑一个计算器的功能,最简单的那种,只能实现加减法运算,现在要让这个计算器支持可撤销的操作。
2.补偿式或者反操作式的解决方案
(1)在实现命令接口之前,先来定义真正实现计算的接口,没有它命令就什么都做不了,操作运算的接口的示例代码如下:
定义了接口,再来看看真正执行加减法的实现。示例代码如下:
(2)接下来来抽象命令接口。由于要支持可撤销的功能,所以除了和前面一样定义一个执行方法外,还需要定义一个撤销操作的方法。示例代码如下:
(3)应该来实现命令了,具体的命令分成了加法命令和减法命令。
先来看看加法命令的实现。示例代码如下:
减法命令和加法类似,只是在实现的时候和加法反过来了。示例代码如下:
(4)接下来应该看看计算器了。计算器就相当于Invoker,持有多个命令对象,计算器是实现可撤销操作的地方。
为了大家更好地理解可撤销的功能,先来看看不加可撤销操作的计算器类是什么样子,然后再添加上可撤销的功能示例。示例代码如下:
目前看起来同前面的例子实现起来差不多,现在就在这个基本的实现上来添加可撤销操作的功能。
要想实现可撤销操作,首先就需要把操作过的命令记录下来,形成命令的历史列表,撤销的时候就从最后一个开始执行撤销。因此我们先在计算器类里面加上命令历史列表,示例代码如下:
什么时候向命令的历史记录里面加值呢?
很简单,答案是在每个操作按钮被按下的时候,也就是你操作加法按钮或者减法按钮的时候。示例代码如下:
然后在计算器类里面添加上一个撤销的按钮,如果它被按下,那么就从命令历史记录里取出最后一个命令来撤销,撤销完成后要把已经撤销的命令从历史记录里面删除掉,相当于没有执行过该命令。
示例代码如下:
同样的方式,还可以实现恢复的功能。也为恢复设置一个可恢复的列表,需要恢复的时候从列表里面取最后一个命令进行重新执行就可以了。示例代码如下:
那么什么时候向这个集合里面赋值呢?大家要注意,恢复的命令数据是来源于撤销的命令,也就是说有撤销才会有恢复,所以在撤销的时候向这个集合里面赋值,注意要在撤销的命令被删除前赋值。示例代码如下,注意蓝色的代码:
那么如何实现恢复呢?请看示例代码:
好了,上面分步讲解了计算器类,下面一起来看看完整的计算器类的代码:
(5)终于到可以收获的时候了,写个客户端,组装好命令和接收者,然后操作几次命令,来测试一下撤销和恢复的功能。示例代码如下:
(6)运行一下,看看结果,享受一下可以撤销和恢复的操作。结果如下:
13.3.4 宏命令
什么是宏命令呢?简单点说就是包含多个命令的命令,是一个命令的组合。举个例子来说吧,设想一下你去饭店吃饭的过程。
(1)你走进一家饭店,找到座位坐下;
(2)服务员走过来,递给你菜谱;
(3)你开始点菜,服务员开始记录菜单,菜单是三联的,点菜完毕,服务员就会把菜单分成三份,一份给后厨,一份给收银台,一份保留备查;
(4)点完菜,你坐在座位上等候,后厨会按照菜单做菜;
(5)每做好一份菜,就会由服务员送到你桌子上;
(6)然后你就可以大快朵颐了。
事实上,到饭店点餐是一个很典型的命令模式应用。作为客户的你,只需要发出命令,就是要吃什么菜,每道菜就相当于一个命令对象,服务员会在菜单上记录你点的菜,然后把菜单传递给后厨,后厨拿到菜单,会按照菜单进行饭菜制作,后厨就相当于接收者,是命令的真正执行者,厨师才知道每道菜具体怎么实现。
在这个过程中,地位比较特殊的是服务员,在不考虑更复杂的管理,比如后厨管理的时候,负责命令和接收者的组装的就是服务员。比如你点了凉菜、热菜,你其实是不知道到底凉菜由谁来完成,热菜由谁来完成的,因此你只管发命令,而组装的工作就由服务员完成了,服务员知道凉菜送到凉菜部,那是已经做好的了,热菜才送到后厨,需要厨师现做,看起来服务员是一个组装者。
同时呢,服务员还持有命令对象,也就是菜单,最后启动命令执行的也是服务员。因此,服务员就相当于标准命令模式中的Client和Invoker的融合。
画个图来描述上述对应关系,如图13.6所示。
图13.6 点菜行为与命令模式对应示意图
1.宏命令在哪里
仔细观察上面的过程,再想想前面的命令模式的实现,看出点什么没有?
前面实现的命令模式都是客户端发出一个命令,然后马上就执行了这个命令,但是在上面的描述里面呢?是点一个菜,服务员就告诉厨师,然后厨师就开始做吗?很明显不是的,服务员会一直等,等到你点完菜,当你说“点完了”的时候,服务员才会启动命令的执行,请注意,这个时候执行的就不是一个命令了,而是执行一堆命令。
描述这一堆命令的就是菜单,如果把菜单也抽象成为一个命令,就相当于一个大的命令,当客户说“点完了”的时候,就相当于触发这个大的命令,意思就是执行菜单这个命令就可以了,这个菜单命令包含多个命令对象,一个命令对象就相当于一道菜。
那么这个菜单就相当于我们说的宏命令。
2.如何实现宏命令
宏命令从本质上讲类似于一个命令,基本上把它当命令对象进行处理。但是它跟普通的命令对象又有些不一样,就是宏命令包含有多个普通的命令对象,简单点说,执行一个宏命令,就是执行宏命令里面所包含的所有命令对象,有点打包执行的意味。
(1)先来定义接收者,就是厨师的接口和实现,先看接口。示例代码如下:
厨师又分成两类,一类是做热菜的师傅;一类是做凉菜的师傅,先看看做热菜的厨师的实现示意。示例代码如下:
做凉菜的师傅示例代码如下:
(2)接下来定义命令接口,和以前一样。示例代码如下:
(3)定义好了命令的接口,该来具体实现命令了。
实现方式和以前一样,持有接收者,当执行命令的时候,转调接收者,让接收者去真正实现功能,这里的接收者就是厨师。
提示
这里实现命令的时候,跟标准的命令模式的命令实现有一点不同,标准的命令模式的命令实现的时候,是通过构造方法传入接收者对象,这里改成了使用setter的方式来设置接收者对象,也就是说可以动态地切换接收者对象,而无须重新构建对象。
示例中定义了三道菜,分别是两道热菜:北京烤鸭、绿豆排骨煲,一道凉菜:蒜泥白肉,三个具体的实现类非常类似,只是菜名不同,为了节省篇幅,这里就只看一个命令对象的具体实现。代码示例如下:
(4)该来组合菜单对象了,也就是宏命令对象。
⑴宏命令就其本质还是一个命令,所以一样要实现Command接口。
⑵宏命令和普通命令的不同在于:宏命令是多个命令组合起来的,因此在宏命令对象里面会记录多个组成它的命令对象。
⑶既然是包含多个命令对象,得有方法让这么多个命令对象能被组合进来。
⑷既然宏命令包含了多个命令对象,执行宏命令对象就相当于依次执行这些命令对象,也就是循环执行这些命令对象
看看代码示例会更清晰些。代码示例如下:
(5)该服务员类重磅登场了,它实现的功能,相当于标准命令模式实现中的Client加上Invoker,前面都是文字讲述,看看代码如何实现。示例代码如下:
(6)费了这么大力气,终于可以坐下来歇息一下,点菜吃饭吧,一起来看看客户端怎样使用这个宏命令,其实在客户端非常简单,根本看不出宏命令来,代码示例如下:
运行一下,享受一下成果。结果如下:
本厨师正在做:绿豆排骨煲
本厨师正在做:北京烤鸭
13.3.5 队列请求
所谓队列请求,就是对命令对象进行排队,组成工作队列,然后依次取出命令对象来执行。通常用多线程或者线程池来进行命令队列的处理,当然也可以不用多线程,就是一个线程,一个命令一个命令地循环处理,就是慢了点。
继续宏命令的例子。其实在后厨,会收到很多菜单,一般是按照菜单传递到后厨的先后顺序来进行处理。对每张菜单,假定也是按照菜品的先后顺序进行制作,那么在后厨则自然形成了一个菜品的队列,也就是很多个用户的命令对象的队列。
后厨有很多厨师,每个厨师都从这个命令队列里面取出一个命令,然后按照命令做出菜来,就相当于多个线程在同时处理一个队列请求。
因此后厨就是一个典型的队列请求的例子。
注意
后厨的厨师与命令队列之间是没有任何关联的,也就是说是完全解耦的。命令队列是客户发出的命令,厨师只是负责从队列里面取出一个,处理,然后再取下一个,再处理,仅此而已,厨师不知道也不管客户是谁。
下面来看看如何实现队列请求。
1.如何实现命令模式的队列请求
(1)先从命令接口开始。除了execute方法外,新增加了一个返回发出命令的桌号,就是点菜的桌号,还有一个是为命令对象设置接收者的方法,也把它添加到接口上,这个是为了后面多线程处理的时候方便使用。示例代码如下:
(2)厨师的接口也发生了一点变化,在cook的方法上添加了发出命令的桌号。这样,在多线程输出信息的时候,才知道到底是在给哪个桌做菜。示例代码如下:
(3)开始来实现命令接口。为了简单,这次只有热菜,因为要做的工作都在后厨的命令队列里面,因此凉菜就不要了。示例代码如下:
还有一个命令对象是“北京烤鸭”,和上面的实现一样,只是菜名不同而已,所以就不去展示示例代码了。
(4)接下来构建很重要的命令对象的队列。其实也不是有多难,多个命令对象可以用一个集合来存储就可以了,然后按照放入的顺序,先进先出即可。
请注意:为了演示的简单性,这里没有使用java.util.Queue,直接使用List来模拟实现了。
示例代码如下:
注意
这里并没有考虑一些复杂的情况,比如,如果命令队列里面没有命令,而厨师又来获取命令该怎么办?这里只是做了一个基本的示范,并没有完整的实现,所以也就没有去处理这些问题。当然,出现这种问题,需要使用wait/notify来进行线程调度。
(5)有了命令队列,谁来向这个队列里面传入命令呢?
很明显是服务员,当客户点菜完成,服务员就会执行菜单,现在执行菜单就相当于把菜单直接传递给后厨,也就是要把菜单里的所有命令对象加入到命令队列里面,因此菜单对象的实现需要改变。示例代码如下:
(6)现在有了命令队列,也有人负责向队列里面添加命令了,可是谁来执行命令队列里面的命令呢?
答案是:由厨师从命令队列里面获取命令,并真正处理命令,而且厨师在处理命令前会把自己设置到命令对象里面去当接收者,表示这个菜由我来实际做。
厨师对象的实现大致有以下的改变。
■ 为了更好地体现命令队列的用法,而实际情况也是多个厨师,这里用多线程来模拟多个厨师。他们自己从命令队列里面获取命令,然后处理命令,再获取下一个,如此反复。因此厨师类要实现多线程接口。
■ 还有一个改变,为了在多线程中输出信息,让我们知道是哪一个厨师在执行命令,给厨师添加了一个姓名的属性,通过构造方法传入。
■ 另外一个改变是为了在多线程中看出效果,在厨师真正做菜的方法里面使用随机数模拟了一个做菜的时间。
好了,介绍完了改变的地方,一起看看代码吧。示例代码如下:
(7)下面该来看看服务员类了。由于现在考虑了后厨的管理,因此从实际情况来看,这次服务员也不知道到底命令的真正接收者是谁了,也就是说服务员也不知道某个菜最后到底由哪一位厨师完成,所以服务员类就简单了。
组装命令对象和接收者的功能后移到厨师类的线程里面了。当某个厨师从命令队列里面获取一个命令对象的时候,这个厨师就是这个命令的真正接收者。
服务员类的示例代码如下:
(8)在见到曙光之前,还有一个问题要解决,就是谁来启动多线程的厨师呢?
为了实现后厨的管理,为此专门定义一个后厨管理的类,在这个类里面启动多个厨师的线程,而且这种启动在运行期间只有一次。示例代码如下:
(9)下面写个客户端测试一下。示例代码如下:
(10)运行一下,看看效果。因为是使用多线程在处理请求队列,可能每次运行的效果不一样。某次运行的结果如下:
仔细观察上面的数据。在多线程环境下,虽然保障了命令对象取出的顺序是先进先出,但是究竟是哪一个厨师来做,还有具体做多长时间都是不定的。
13.3.6 日志请求
所谓日志请求,就是把请求的历史记录保存下来,一般是采用永久存储的方式。如果在运行请求的过程中,系统崩溃了,那么当系统再次运行时,就可以从保存的历史记录中获取日志请求,并重新执行命令。
日志请求的实现有两种方案:一种是直接使用Java中的序列化方法;另外一种就是在命令对象中添加上存储和装载的方法,其实就是让命令对象自己实现类似序列化的功能。当然要简单就直接使用Java中的序列化。
结合前面队列请求的例子,来简单演示一下日志请求的功能。
考虑在队列请求的例子里面,当菜单都被传到后台以后,后台会把这些菜单做成一个请求队列,然后让厨师从这个队列里面获取命令去执行。这里就存在一个问题,如果这个系统运行中突然崩溃了呢?比如突然断电了,那该怎么办呢?
难道等系统再次运行的时候,后厨要求服务员再把菜单全部重新传递一次吗?即使服务员全部传一次,那样也还是有问题,因为有些命令是已经被执行了的,它们不应该被重复执行,而服务员是不知道哪些命令是已经执行了的。
提示
一个可行的解决方案就是把这个请求队列日志化,当有新的菜单传递过来的时候,更新日志,当厨师从队列里面获取一个请求去执行的时候,这个请求就应该从日志中去掉,这样即使系统崩溃了,也能够恢复,而且恢复的都是还没有执行的命令。
1.实现日志请求
由于篇幅关系,就不去做比较复杂的示例了,直接沿用前面对列请求的例子,采用Java中序列化的方法,这样最简单。
(1)先序列化命令对象,因为要把这些对象保存到文件中去。在ChopCommand和DuckCommand对象上实现java.io.Serializable,这个就不用代码示例了。
(2)前面的实现中把请求队列实现成了List对象,为了保存这个List对象,设计一个文件操作的工具类,来实现向文件中写入List和从文件中获取List对象。示例代码如下:
(3)有了读写文件的工具类,下面来看看在命令队列里面如何实现把队列日志化了。其实思路很简单,就是在装载队列的时候,先从日志中获取上一次还没有做完的命令。如果没有,那就新建一个队列,然后在每次向队列里面添加命令的时候就更新日志;如果有厨师取出命令,那就删除掉该命令,并重新更新日志。示例代码如下:
(4)其他部分和队列请求的例子完全一样,就不再赘述。
可以运行测试代码来看看效果,要想看出在中断后,下次运行时是否能够恢复上次没有运行的命令,可以在第一次运行的时候,也就是多线程还没有处理完全部命令的时候强制终止运行,然后再启动看看,是否能够恢复上次没有执行完的命令。
第一次运行客户端,中途强制终止。示例的结果如下:
注意第一次运行终止的时候,刚好把第三桌的菜做好。下面再次运行客户端,应该先要把刚才没有做完的菜做完,然后才继续新的菜单。示例的结果如下:
2.小结
通过上面的示例可以看出,实现日志请求也不是很麻烦,当然,如果需要自己做序列化会复杂一些。对于扩展日志请求,在高级应用中,可以扩展到事务的处理中,因为事务的基本实现机制就是先写日志,然后再操作数据库,这里就不再继续展开了。
13.3.7 命令模式的优点
■ 更松散的耦合
命令模式使得发起命令的对象——客户端,和具体实现命令的对象——接收者对象完全解耦,也就是说发起命令的对象完全不知道具体实现对象是谁,也不知道如何实现。
■ 更动态的控制
命令模式把请求封装起来,可以动态地对它进行参数化、队列化和日志化等操作,从而使得系统更灵活。
■ 很自然的复合命令
命令模式中的命令对象能够很容易地组合成复合命令,也就是前面讲的宏命令,从而使系统操作更简单,功能更强大。
■ 更好的扩展性
由于发起命令的对象和具体的实现完全解耦,因此扩展新的命令就很容易,只需要实现新的命令对象,然后在装配的时候,把具体的实现对象设置到命令对象中,然后就可以使用这个命令对象,已有的实现完全不用变化。
13.3.8 思考命令模式
1.命令模式的本质
命令模式的本质:封装请求。
前面讲了,命令模式的关键就是把请求封装成为命令对象,然后就可以对这个对象进行一系列的处理了,比如上面讲到的参数化配置、可撤销操作、宏命令、队列请求、日志请求等功能处理。
2.何时选用命令模式
建议在以下情况时选用命令模式。
■ 如果需要抽象出需要执行的动作,并参数化这些对象,可以选用命令模式。将这些需要执行的动作抽象成为命令,然后实现命令的参数化配置。
■ 如果需要在不同的时刻指定、排列和执行请求,可以选用命令模式。将这些请求封装成为命令对象,然后实现将请求队列化。
■ 如果需要支持取消操作,可以选用命令模式,通过管理命令对象,能很容易地实现命令的恢复和重做功能。
■ 如果需要支持当系统崩溃时,能将系统的操作功能重新执行一遍,可以选用命令模式。将这些操作功能的请求封装成命令对象,然后实现日志命令,就可以在系统恢复以后,通过日志获取命令列表,从而重新执行一遍功能。
■ 在需要事务的系统中,可以选用命令模式。命令模式提供了对事务进行建模的方法。命令模式有一个别名就是Transaction。
13.3.9 退化的命令模式
在领会了命令模式的本质后,接下来思考一个命令模式退化的情况。
前面讲到了智能命令,如果命令的实现对象超级智能,实现了命令要求的所有功能,那么就不需要接收者了,既然没有了接收者,那么也就不需要组装者了。
(1)举个最简单的示例来说明。
比如现在要实现一个打印服务,由于非常简单,所以基本上就没有什么讲述,依次来看,命令接口定义如下:
命令的实现示例代码如下:
此时的Invoker示例代码如下:
最后看看客户端的代码。示例如下:
测试结果如下:
打印的内容为=退化的命令模式示例
(2)继续变化。
如果此时继续变化,Invoker也开始变得智能化,在Invoker的startPrint方法里面,Invoker加入了一些实现,同时Invoker对持有命令也有意见,觉得自己是个傀儡,要求改变一下,直接在调用方法的时候传递命令对象进来。示例代码如下:
看起来Invoker退化成一个方法了。
这个时候Invoker很高兴,宣称自己是一个智能的服务,不再是一个傻傻的转调者,而是有自己功能的服务了。这个时候Invoker调用命令对象的执行方法,也不叫转调,改名叫“回调”,意思是在我Invoker需要的时候,会回调你命令对象,命令对象你就乖乖地写好实现,等我“回调”你就可以了。
事实上这个时候的命令模式的实现基本上就等同于Java回调机制的实现。可能有些朋友看起来感觉还不是佷像,那是因为在Java回调机制的常见实现上,经常没有单独的接口实现类,而是采用匿名内部类的方式来实现的。
(3)再进一步。
把单独实现命令接口的类改成用匿名内部类实现,这个时候就只剩下命令的接口、Invoker类,还有客户端了。
为了使用匿名内部类,还需要设置输出的值,对命令接口做点小改动,增加一个设置输出值的方法。示例代码如下:
此时Invoker就是上面那个,而客户端会有些改变。客户端的示例代码如下:
运行测试一下。结果如下:
(4)现在是不是看出来了,这个时候的命令模式的实现基本上就等同于Java回调机制的实现。这也是很多人常说的命令模式可以实现Java回调的意思。
当然更狠的是连Invoker也不要了,直接把那个方法搬到Client中,那样测试起来就更方便了。在实际开发中,应用命令模式来实现回调机制的时候,Invoker通常还是有的,但可以智能化实现,更准确地说Invoker充当客户调用的服务实现,而回调的方法只是实现服务功能中的一个或者几个步骤。
13.3.10 相关模式
■ 命令模式和组合模式
这两个模式可以组合使用。
在命令模式中,实现宏命令的功能就可以使用组合模式来实现。前面的示例并没有按照组合模式来做,那是为了保持示例的简单,还有突出命令模式的实现,这点请注意。
■ 命令模式和备忘录模式
这两个模式可以组合使用。
在命令模式中,实现可撤销操作功能时,前面讲了有两种实现方式,其中有一种就是保存命令执行前的状态,撤销的时候就把状态恢复。如果采用这种方式实现,就可以考虑使用备忘录模式。
如果状态存储在命令对象中,那么还可以使用原型模式,把命令对象当作原型来克隆一个新的对象,然后将克隆出来的对象通过备忘录模式存放。
■ 命令模式和模板方法模式
这两个模式从某种意义上有相似的功能,命令模式可以作为模板方法的一种替代模式,也就是说命令模式可以模仿实现模板方法模式的功能。
如同前面讲述的退化的命令模式可以实现Java的回调,而Invoker智能化后向服务进化,如果Invoker的方法就是一个算法骨架,其中有两步在这个骨架里面没有具体实现,需要外部来实现,这个时候就可以通过回调命令接口来实现。而类似的功能在模板方法中,是先调用抽象方法,然后等待子类来实现。可以看出虽然实现方式不一样,但是可以实现相同的功能。