19.3 模 式 讲 解

19.3.1 认识备忘录模式

1.备忘录模式的功能

备忘录模式的功能,首先是在不破坏封装性的前提下,捕获一个对象的内部状态。这里要注意两点,一个是不破坏封装性,也就是对象不能暴露它不应该暴露的细节;另外一个是捕获的是对象的内部状态,而且通常还是运行期间某个时刻对象的内部状态。

为什么要捕获这个对象的内部状态呢?捕获这个内部状态有什么用呢?

是为了在以后的某个时候,将该对象的状态恢复到备忘录所保存的状态,这才是备忘录真正的目的。前面保存状态就是为了后面恢复,虽然不是一定要恢复,但是目的是为了恢复。这也是很多人理解备忘录模式的时候,忽视掉的地方,他们太关注备忘,而忽视了恢复,这是不全面的理解。

捕获的状态存放在哪里呢?

备忘录模式中,捕获的内部状态存储在备忘录对象中;而备忘录对象通常会被存储在原发器对象之外,也就是被保存状态的对象的外部,通常是存放在管理者对象那里。

2.备忘录对象

在备忘录模式中,备忘录对象通常就是用来记录原发器需要保存的状态的对象,简单点的实现,也就是封装数据的对象。

但是备忘录对象和普通的封装数据的对象还是有区别的,主要是备忘录对象一般只让原发器对象来操作,而不是像普通的封装数据的对象那样,谁都可以使用。为了保证这一点,通常会把备忘录对象作为原发器对象的内部类来实现,而且实现成私有的,这就断绝了外部来访问这个备忘录对象的途径。

备忘录对象需要保存在原发器对象之外,为了与外部交互,通常备忘录对象都会实现一个窄接口,来标识对象的类型。

3.原发器对象

原发器对象就是需要被保存状态的对象,也是有可能需要恢复状态的对象。原发器一般会包含备忘录对象的实现。

通常原发器对象应该提供捕获某个时刻对象内部状态的方法,在这个方法中,原发器对象会创建备忘录对象,把需要保存的状态数据设置到备忘录对象中,然后把备忘录对象提供给管理者对象来保存。

当然,原发器对象也应该提供这样的方法:按照外部要求来恢复内部状态到某个备忘录对象记录的状态。

4.管理者对象

在备忘录模式中,管理者对象主要是负责保存备忘录对象。这里有几点要讲一下。

■ 并不一定要特别的做出一个管理者对象来。广义地说,调用原发器获得备忘录对象后,备忘录对象放在哪里,哪个对象就可以算是管理者对象。

■ 管理者对象并不是只能管理一个备忘录对象,一个管理者对象可以管理很多的备忘录对象。虽然前面的示例中是保存一个备忘录对象,但别忘了那只是个示意,并不是只能实现成那样。

■ 狭义的管理者对象是只管理同一类的备忘录对象,但广义的管理者对象是可以管理不同类型的备忘录对象的。

■ 管理者对象需要实现的基本功能主要是:存入备忘录对象、保存备忘录对象和获取备忘录对象。如果从功能上看,就是一个缓存功能的实现,或者是一个简单的对象实例池的实现。

■ 管理者虽然能存取备忘录对象,但是不能访问备忘录对象内部的数据。

5.窄接口和宽接口

在备忘录模式中,为了控制对备忘录对象的访问,出现了窄接口和宽接口的概念。

■ 窄接口:管理者只能看到备忘录的窄接口,窄接口的实现中通常没有任何的方法,只是一个类型标识。窄接口使得管理者只能将备忘录传递给其他对象。

■ 宽接口:原发器能够看到一个宽接口,允许它访问所需的所有数据,来返回到先前的状态。理想状况是:只允许生成备忘录的原发器来访问该备忘录的内部状态,通常实现成为原发器内的一个私有内部类。

在前面的示例中,定义了一个名称为FlowAMockMemento的接口,里面没有定义任何方法,然后让备忘录来实现这个接口,从而标识备忘录就是这么一个FlowAMockMemento的类型,这个接口就是窄接口。

在前面的实现中,备忘录对象是实现在原发器内的一个私有内部类,只有原发器对象能访问它,原发器可以访问到备忘录对象中所有的内部状态,这就是宽接口。

这也算是备忘录模式的标准实现方式,那就是窄接口没有任何的方法,把备忘录对象实现成为原发器对象的私有内部类。

注意

那么能不能在窄接口中提供备忘录对象对外的方法,变相对外提供一个“宽”点的接口呢?

通常情况是不会这么做的。因为这样一来,所有能拿到这个接口的对象就可以通过这个接口来访问备忘录内部的数据或是功能,这违反了备忘录模式的初衷,备忘录模式要求“在不破坏封装性的前提下”,如果这么做,那就等于是暴露了内部细节。因此,备忘录模式在实现的时候,对外多是采用窄接口,而且通常不会定义任何方法。

6.使用备忘录的潜在代价

标准的备忘录模式的实现机制是依靠缓存来实现的,因此,当需要备忘的数据量较大时,或者是存储的备忘录对象数据量不大但是数量很多的时候,或者是用户很频繁地创建备忘录对象的时候,这些都会导致非常大的开销。

因此在使用备忘录模式的时候,一定要好好思考应用的环境,如果使用的代价太高,就不要选用备忘录模式,可以采用其他的替代方案。

7.增量存储

如果需要频繁地创建备忘录对象,而且创建和应用备忘录对象来恢复状态的顺序是可控的,那么可以让备忘录进行增量存储,也就是备忘录可以仅仅存储原发器内部相对于上一次存储状态后的增量改变。

比如,在命令模式实现可撤销命令的实现中,就可以使用备忘录来保存每个命令对应的状态,然后在撤销命令的时候,使用备忘录来恢复这些状态。由于命令的历史列表是按照命令操作的顺序来存放的,也是按照这个历史列表来进行取消和重做的,因此顺序是可控的。那么这种情况,还可以让备忘录对象只存储一个命令所产生的增量改变而不是它所影响的每一个对象的完整状态。

8.备忘录模式调用顺序示意图

在使用备忘录模式的时候,分成了两个阶段,第一个阶段是创建备忘录对象的阶段,第二个阶段是使用备忘录对象来恢复原发器对象状态的阶段。它们的调用顺序是不一样的,下面分别用图来示意一下。

先看看创建备忘录对象的阶段。调用顺序如图19.3所示。

图片

图19.3 创建备忘录对象的调用顺序示意图

再看看使用备忘录对象来恢复原发器对象状态的阶段。调用顺序如图19.4所示。

图片

图19.4 使用备忘录对象来恢复原发器对象状态的调用顺序示意图

19.3.2 结合原型模式

在原发器对象创建备忘录对象的时候,如果原发器对象中全部或者大部分的状态都需要保存,一个简洁的方式就是直接克隆一个原发器对象。也就是说,这个时候备忘录对象中存放的是一个原发器对象的实例。

还是通过示例来说明。只需要修改原发器对象就可以了,大致有如下变化。

■ 原发器对象要实现可克隆的,好在这个原发器对象的状态数据都很简单,都是基本数据类型,所以直接使用默认的克隆方法就可以了,不用自己实现克隆,更不涉及深度克隆,否则,正确实现深度克隆还是个问题。

■ 备忘录对象的实现要修改,只需要存储原发器对象克隆出来的实例对象就可以了。

■ 相应的创建和设置备忘录对象的地方都要做修改。

示例代码如下:

26023822b6f5414e92f123450f60a3ff

5a4d60b33f1b465cac0ebb8c4febd852

9db35189c2b4409bad9d35a60dd46e5f

d087426d70004be19e080145c0a9acb2

好了,结合原型模式来实现备忘录模式的示例就写好了,在前面的客户测试程序中,创建原发器对象的时候,使用这个新实现的原发器对象就可以了。去测试和体会一下,看看是否能正确地实现需要的功能。

注意

不过要注意一点,就是如果克隆对象非常复杂,或者需要很多层次的深度克隆,实现克隆的时候会比较麻烦。

19.3.3 离线存储

标准的备忘录模式,没有讨论离线存储的实现。

事实上,从备忘录模式的功能和实现上,是可以把备忘录的数据实现成为离线存储的,也就是不仅限于存储在内存中,可以把这些备忘数据存储到文件中、XML中、数据库中,从而支持跨越会话的备份和恢复功能。

离线存储甚至能帮助应对应用崩溃,然后关闭重启的情况。应用重启后,从离线存储中获取相应的数据,然后重新设置状态,恢复到崩溃前的状态。

当然,并不是所有的备忘数据都需要离线存储。一般来讲,需要存储很长时间,或者需要支持跨越会话的备份和恢复功能,或者是希望系统关闭后还能被保存的备忘数据,这些情况建议采用离线存储。

离线存储的实现也很简单,就以前面模拟运行流程的应用来说,如果要实现离线存储,主要需要修改管理者对象,把它保存备忘录对象的方法实现成为保存到文件中,而恢复备忘录对象实现成为读取文件就可以了。对于其他的相关对象,主要是要实现序列化,只有可序列化的对象才能被存储到文件中。

如果实现保存备忘录对象到文件,就不用在内存中保存了,删除用来“记录被保存的备忘录对象”的这个属性。示例代码如下:

f846530a42174e29b65fbefcef2eae80

f846530a42174e29b65fbefcef2eae80

同时需要让备忘录对象的窄接口继承可序列化接口。示例代码如下:

d4c6fb53863c4d699dd55c7e15de14b6

还有FlowAMock对象,也需要实现可序列化。示例代码如下:

a2d3af35fec04977ba631f7e065dfb5c

c53605c617d94b20b116849d5c80b86e

好了,保存到文件的存储就实现好了。在前面的客户测试程序中,创建管理者对象的时候,使用这个新实现的管理者对象就可以了。去测试和体会一下。

19.3.4 再次实现可撤销操作

在命令模式中,讲到了可撤销的操作,在那里讲到:有两种基本的思路来实现可撤销的操作,一种是补偿式或者反操作式,比如被撤销的操作是加的功能,那撤销的实现就变成减的功能;同理被撤销的操作是打开的功能,那么撤销的实现就变成关闭的功能。

另外一种方式是存储恢复式,意思就是把操作前的状态记录下来,然后要撤销操作的时候就直接恢复回去就可以了。

这里就来实现第二种方式存储恢复式。为了让大家更好地理解可撤销操作的功能,还是用原来的那个例子,对比学习会比较清楚。

这也相当于是命令模式和备忘录模式结合的一个例子,而且由于命令列表的存在,对应保存的备忘录对象也有多个。

1.范例需求

考虑一个计算器的功能,最简单的那种,只能实现加减法运算,现在要让这个计算器支持可撤销的操作。

2.存储恢复式的解决方案

存储恢复式的实现,可以使用备忘录模式,大致实现的思路如下。

■ 把原来的运算类,就是Operation类,当作原发器,原来的内部状态result,就只提供一个getter方法,来让外部获取运算的结果。

■ 在这个原发器中,实现一个私有的备忘录对象。

■ 把原来的计算器类,就是Calculator类,当作管理者,把命令对应的备忘录对象保存在这里。当需要撤销操作的时候,就把相应的备忘录对象设置回到原发器中,恢复原发器的状态。

一起来看看具体的实现,会更清楚。

(1)定义备忘录对象的窄接口。示例代码如下:

d514f994945b4368969d409f3d0f1275

(2)定义命令的接口。有以下几点修改。

■ 修改原来的undo方法,传入备忘录对象。

■ 添加一个redo方法,传入备忘录对象。

■ 添加一个createMemento的方法,获取需要被保存的备忘录对象。

示例代码如下:

99712b4fe501470db5892a8e1cd2219d

(3)再来定义操作运算的接口,相当于计算器类这个原发器对外提供的接口,它需要做如下的调整。

■ 删除原有的setResult方法,内部状态,不允许外部操作。

■ 添加一个createMemento的方法,获取需要保存的备忘录对象。

■ 添加一个setMemento的方法,来重新设置原发器对象的状态。

示例代码如下:

ca10557ab5a0434f858fbd69e8ad79b4

472bfe544bfc4908923e732408b77e8d

(4)由于现在撤销和恢复操作是通过使用备忘录对象,直接来恢复原发器的状态,因此不再需要按照操作类型来区分了,对于所有的命令实现,它们的撤销和重做都是一样的。原来的实现是要区分的,如果是撤销加的操作,那就是减,而撤销减的操作,那就是加。现在就不再区分了,统一使用备忘录对象来恢复。

因此,实现一个所有命令的公共对象,在其中把公共功能都实现了,这样每个命令在实现的时候就简单了。顺便把设置持有者的公共实现也放到这个公共对象中来,这样各个命令对象就不用再实现这个方法了。示例代码如下:

202284d1211743f2bf5c6f0454b76cbe

4d2539f78c334497b8f4c01404ec4b2b

(5)有了公共的命令实现对象,各个具体命令的实现就简单了。实现加法命令的对象实现,不再直接实现Command接口了,而是继承命令的公共对象,这样只需要实现和自己命令相关的业务方法就可以了。示例代码如下:

1f0ce5c5b7424ced830476ed15b67d92

看看减法命令的实现,跟加法命令的实现差不多。示例代码如下:

afe9b0b86c0d44a2a92a2bd405b689d5

(6)接下来看看运算类的实现,相当于是原发器对象,它的实现有如下改变。

■ 不再提供setResult方法,内部状态,不允许外部来操作。

■ 添加了createMemento和setMemento方法的实现。

■ 添加实现了一个私有的备忘录对象。

示例代码如下:

61bc408be4fc49da8aa72e4764c72293

e21741746d2a4d90b665985c182d3cdc

d02e9bf90bb84265a416d7bf24ebe035

(7)接下来该看看如何具体地使用备忘录对象来实现撤销操作和重做操作了。同样在计算器类中实现,这个时候,计算器类就相当于是备忘录模式管理者对象。

提示

实现思路:由于对于每个命令对象,撤销和重做的状态是不一样的,撤销是回到命令操作前的状态,而重做是回到命令操作后的状态,因此对每一个命令,使用一个备忘录对象的数组来记录对应的状态。

这些备忘录对象和命令对象是相对应的,因此也跟命令历史记录一样,设置相应的历史记录,它的顺序和命令完全对应起来。在操作命令历史记录的同时,对应操作相应的备忘录对象记录。

示例代码如下:

5737ddb73ca94c64af051f9f600f2647

08f4ade349254e2baaaa43e66bed5a74

6482a27951cc488fb17ce75e3e89ff9d

b10c11235e254f88b12043fae57f5847

9540074fe3334f0885b0d28a349d2b0c

(8)客户端跟以前的实现没有什么变化。示例代码如下:

4d5cebce1f7f4a9b92f7ddeb5e7b1be4

a2865f8bf996491a8747beb23bc0a00d

25016efa7714432aba287a095b8763b1

运行结果示例如下。示例代码如下:

6edc5b4c4ce1469b8238796ba980d16a

和前面采用补偿式或者反操作式得到的结果是一样的。好好体会一下,对比两种实现方式,看看都是怎么实现的。顺便也体会一下命令模式和备忘录模式是如何结合起来实现功能的。

19.3.5 备忘录模式的优缺点

备忘录模式有以下优点。

■ 更好的封装性

备忘录模式通过使用备忘录对象,来封装原发器对象的内部状态,虽然这个对象是保存在原发器对象的外部,但是由于备忘录对象的窄接口并不提供任何方法。

这样有效地保证了对原发器对象内部状态的封装,不把原发器对象的内部实现细节暴露给外部。

■ 简化了原发器

备忘录模式中,备忘录对象被保存到原发器对象之外,让客户来管理他们请求的状态,从而让原发器对象得到简化。

■ 窄接口和宽接口

备忘录模式,通过引入窄接口和宽接口,使得不同的地方,对备忘录对象的访问是不一样的。窄接口保证了只有原发器才可以访问备忘录对象的状态。

备忘录模式的缺点,是可能会导致高开销。

备忘录模式基本的功能,就是对备忘录对象的存储和恢复,它的基本实现方式就是缓存备忘录对象。这样一来,如果需要缓存的数据量很大,或者是特别频繁地创建备忘录对象,开销是很大的。

19.3.6 思考备忘录模式

1.备忘录模式的本质

   备忘录模式的本质:保存和恢复内部状态。

保存是手段,恢复才是目的,备忘录模式备忘些什么东西呢?

备忘录模式备忘的就是原发器对象的内部状态,这些内部状态是不对外的,只有原发器对象才能够进行操作。

标准的备忘录模式保存数据的手段是,通过内存缓存,广义的备忘录模式实现的时候,可以采用离线存储的方式,把这些数据保存到文件或者数据库等地方。

备忘录模式为何要保存数据呢?目的就是为了在有需要的时候,恢复原发器对象的内部状态。所以恢复是备忘录模式的目的。

根据备忘录模式的本质,从广义上讲,进行数据库存取操作;或者是Web应用中的request、session、servletContext等的attribute数据存取;更进一步,大多数基于缓存功能的数据操作都可以视为广义的备忘录模式。不过广义到这个地步,还提备忘录模式已经没有什么意义了。所以对于备忘录模式还是多从狭义上来说。

事实上,对于备忘录模式最主要的一个特点,就是封装状态的备忘录对象,不应该被除了原发器对象之外的对象访问,至于如何存储那都是小事情。因为备忘录模式要解决的主要问题就是:在不破坏对象封装性的前提下,来保存和恢复对象的内部状态,这是一个很主要的判断依据。如果备忘录对象可以让原发器对象以外的对象访问的话,那就算是广义的备忘录模式了,此时提不提备忘录模式已经没有太大的意义了。

2.何时选用备忘录模式

建议在以下情况中选用备忘录模式。

■ 如果必须保存一个对象在某一个时刻的全部或者部分状态,方便在以后需要的时候,可以把该对象恢复到先前的状态,可以使用备忘录模式。使用备忘录对象来封装和保存需要保存的内部状态,然后把备忘录对象保存到管理者对象中,在需要的时候,再从管理者对象中获取备忘录对象,来恢复对象的状态。

■ 如果需要保存一个对象的内部状态,但是如果用接口来让其他对象直接得到这些需要保存的状态,将会暴露对象的实现细节并破坏对象的封装性,这时可以使用备忘录模式,把备忘录对象实现成为原发器对象的内部类,而且还是私有的,从而保证只有原发器对象才能访问该备忘录对象。这样既保存了需要保存的状态,又不会暴露原发器对象的内部实现细节。

19.3.7 相关模式

■ 备忘录模式和命令模式

这两个模式可以组合使用。

命令模式实现中,在实现命令的撤销和重做的时候,可以使用备忘录模式,在命令操作的时候记录下操作前后的状态,然后在命令撤销和重做的时候,直接使用相应的备忘录对象来恢复状态就可以了。

在这种撤销的执行顺序和重做的执行顺序可控的情况下,备忘录对象还可以采用增量式记录的方式,有效减少缓存的数据量。

■ 备忘录模式和原型模式

这两个模式可以组合使用。

在原发器对象创建备忘录对象的时候,如果原发器对象中全部或者大部分的状态都需要保存,一个简洁的方式就是直接克隆一个原发器对象。也就是说,这个时候备忘录对象里面存放的是一个原发器对象的实例,这个在前面已经示例过了,这里就不再赘述。