1.初识备忘录模式
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
- Memento:
备忘录。主要用来存储原发器对象的内部状态,但是具体需要存储哪些数据是由原发器对象来决定的。另外备忘录应该只能由原发器对象来访问它内部的数据,原发器外部的对象不应该能访问到备忘录对象的内部数据。 - Originator:
原发器。使用备忘录来保存某个时刻原发器自身的状态,也可以使用备忘录来恢复内部状态。 - Caretaker:
备忘录管理者,或者称为备忘录负责人。主要负责保存备忘录对象,但是不能对备忘录对象的内容进行操作或检查。
2.体会备忘录模式
2.1 场景问题——开发仿真系统
考虑这样一个仿真应用,功能是:模拟运行针对某个具体问题的多个解决方案,记录运行过程的各种数据,在模拟运行完成过后,好对这多个解决方案进行比较和评价,从而选定最优的解决方案。
由于都是解决同一个具体的问题,这多个解决方案并不是完全不一样的,假定它们的前半部分运行是完全一样的,只是在后半部分采用了不同的解决方案,后半部分需要使用前半部分运行所产生的数据。
由于要模拟运行多个解决方案,而且最后要根据运行结果来进行评价,这就意味着每个方案的后半部分的初始数据应该是一样。
那么,这样的系统该如何实现呢?尤其是每个方案运行需要的初始数据应该一样,要如何来保证呢?
2.2 不用模式的解决方案
不用模式的解决方案example1
要保证初始数据的一致,实现思路也很简单:
- 1)首先模拟运行流程第一个阶段,得到后阶段各个方案运行需要的数据,并把数据保存下来,以备后用
- 2)每次在模拟运行某一个方案之前,用保存的数据去重新设置模拟运行流程的对象,这样运行后面不同的方案时,对于这些方案,初始数据就是一样的了
2.2.1 有何问题
上面的实现有一个不太好的地方,那就是数据是一个一个零散着在外部存放的,如果需要外部存放的数据多了,会显得很杂乱。这个好解决,只需要定义一个数据对象来封装这些需要外部存放的数据就可以了。
还有一个严重的问题:为了把运行期间的数据放到外部存储起来,模拟流程的对象被迫把内部数据结构开放出来,这暴露了对象的实现细节,而且也破坏了对象的封装性。本来这些数据只是模拟流程的对象内部数据,是不对外的。
那么究竟如何实现这样的功能会比较好呢?
2.3 使用模式的解决方案
使用模式的解决方案example2
在这个示例中出现的、需要解决的问题就是:如何能够在不破坏对象的封装性的前提下,来保存和恢复对象的状态。
备忘录模式引入一个存储状态的备忘录对象,为了让外部无法访问这个对象的值,一般把这个对象实现成为需要保存数据的对象的内部类,通常还是私有的,这样一来,除了这个需要保存数据的对象,外部无法访问到这个备忘录对象的数据,这就保证了对象的封装性不被破坏。
但是这个备忘录对象需要存储在外部,为了避免让外部访问到这个对象内部的数据,备忘录模式引入了一个备忘录对象的窄接口,这个接口一般是空的,什么方法都没有,这样外部存储的地方,只是知道存储了一些备忘录接口的对象,但是由于接口是空的,它们无法通过接口去访问备忘录对象内的数据。
3.理解备忘录模式
3.1 认识备忘录模式
3.1.1 备忘录模式的功能
备忘录模式的功能,首先是在不破坏封装性的前提下,捕获一个对象的内部状态。这里要注意两点,一个是不破坏封装性,也就是对象不能暴露它不应该暴露的细节;另外一个是捕获的是对象的内部状态,而且通常还是运行期间某个时刻,对象的内部状态。
为什么要捕获这个对象的内部状态呢?捕获这个内部状态有什么用呢?
是要在以后的某个时候,将该对象的状态恢复到备忘录所保存的状态,这才是备忘录真正的目的,前面保存状态就是为了后面恢复,虽然不是一定要恢复,但是目的是为了恢复。这也是很多人理解备忘录模式的时候,忽视掉的地方,他们太关注备忘,而忽视了恢复,这是不全面的理解。
捕获的状态存放在哪里呢?
备忘录模式中,捕获的内部状态,存储在备忘录对象中;而备忘录对象,通常会被存储在原发器对象之外,也就是被保存状态的对象的外部,通常是存放在管理者对象哪里。
3.1.2 备忘录对象
在备忘录模式中,备忘录对象,通常就是用来记录原发器需要保存的状态的对象,简单点的实现,也就是个封装数据的对象。
但是这个备忘录对象和普通的封装数据的对象还是有区别的,主要就是这个备忘录对象,一般只让原发器对象来操作,而不是像普通的封装数据的对象那样,谁都可以使用。为了保证这一点,通常会把备忘录对象作为原发器对象的内部类来实现,而且会实现成私有的,这就断绝了外部来访问这个备忘录对象的途径。
但是备忘录对象需要保存在原发器对象之外,为了与外部交互,通常备忘录对象都会实现一个窄接口,来标识对象的类型。
3.1.3 原发器对象
原发器对象,就是需要被保存状态的对象,也是有可能需要恢复状态的对象。原发器一般会包含备忘录对象的实现。
通常原发器对象应该提供捕获某个时刻对象内部状态的方法,在这个方法里面,原发器对象会创建备忘录对象,把需要保存的状态数据设置到备忘录对象中,然后把备忘录对象提供给管理者对象来保存。
当然,原发器对象也应该提供这样的方法:按照外部要求来恢复内部状态到某个备忘录对象记录的状态。
3.1.4 管理者对象
在备忘录模式中,管理者对象,主要是负责保存备忘录对象。
- 1)并不一定要特别的做出一个管理者对象来,广义地说,调用原发器获得备忘录对象后,备忘录对象放在哪里,哪个对象就可以算是管理者对象。
- 2)管理者对象并不是只能管理一个备忘录对象,一个管理者对象可以管理很多的备忘录对象,虽然前面的示例中是保存一个备忘录对象,别忘了那只是个示意,并不是只能实现成那样。
- 3)狭义的管理者对象,是只管理同一类的备忘录对象,但是广义管理者对象是可以管理不同类型的备忘录对象的。
- 4)管理者对象需要实现的基本功能主要就是:存入备忘录对象、保存备忘录对象、获取备忘录对象,如果从功能上看,就是一个缓存功能的实现,或者是一个简单的对象实例池的实现。
- 5)管理者虽然能存取备忘录对象,但是不能访问备忘录对象内部的数据。
3.1.5 窄接口和宽接口
- 窄接口:管理者只能看到备忘录的窄接口,它的实现里面通常没有任何的方法,只是一个类型标识,窄接口使得管理者只能将备忘录传递给其它对象。
- 宽接口:原发器能够看到一个宽接口,允许它访问所需的所有数据,来返回到先前的状态。理想状况是:只允许生成备忘录的原发器来访问该备忘录的内部状态,通常实现成为原发器内的一个私有内部类。
备忘录模式的标准实现方式,那就是窄接口没有任何的方法,把备忘录对象实现成为原发器对象的私有内部类。那么能不能在窄接口里面提供备忘录对象对外的方法,变相对外提供一个“宽”点的接口呢?
通常情况是不会这么做的,因为这样一来,所有能拿到这个接口的对象就可以通过这个接口来访问备忘录内部的数据或是功能,这违反了备忘录模式的初衷,备忘录模式要求“在不破坏封装性的前提下”,如果这么做,那就等于是暴露了内部细节,因此,备忘录模式在实现的时候,对外多是采用窄接口,而且通常不会定义任何方法。
3.1.6 使用备忘录的潜在代价
标准的备忘录模式的实现机制是依靠缓存来实现的,因此,当需要备忘的数据量较大时,或者是存储的备忘录对象数据量不大但是数量很多的时候,或者是用户很频繁的创建备忘录对象的时候,这些都会导致非常大的开销。
因此在使用备忘录模式的时候,一定要好好思考应用的环境,如果使用的代价太高,就不要选用备忘录模式,可以采用其它的替代方案
3.1.7 增量存储
如果需要频繁的创建备忘录对象,而且创建和应用备忘录对象来恢复状态的顺序是可控的,那么可以让备忘录进行增量存储,也就是备忘录可以仅仅存储原发器内部相对于上一次存储状态后的增量改变。
比如:在命令模式实现可撤销命令的实现中,就可以使用备忘录来保存每个命令对应的状态,然后在撤销命令的时候,使用备忘录来恢复这些状态。由于命令的历史列表是按照命令操作的顺序来存放的,也是按照这个历史列表来进行取消和重做的,因此顺序是可控的。那么这种情况,还可以让备忘录对象只存储一个命令所产生的增量改变而不是它所影响的每一个对象的完整状态。
3.1.8 备忘录模式调用顺序示意图
在使用备忘录模式的时候,分成了两个阶段,第一个阶段是创建备忘录对象的阶段,第二个阶段是使用备忘录对象来恢复原发器对象的状态的阶段。它们的调用顺序是不一样的,下面分开用图来示意一下:
使用备忘录对象来恢复原发器对象的状态的阶段,调用顺序如图:
3.2 结合原型模式
在原发器对象创建备忘录对象的时候,如果原发器对象中全部或者大部分的状态都需要保存,一个简洁的方式就是直接克隆一个原发器对象。也就是说,这个时候备忘录对象里面存放的是一个原发器对象的实例。
3.3 离线存储
标准的备忘录模式,没有讨论离线存储的实现。
事实上,从备忘录模式的功能和实现上,是可以把备忘录的数据实现成为离线存储的,也就是不仅限于存储于内存中,可以把这些备忘数据存储到文件中、xml中、数据库中,从而支持跨越会话的备份和恢复功能。
离线存储甚至能帮助应对应用崩溃,然后关闭重启的情况,应用重启过后,从离线存储里面获取相应的数据,然后重新设置状态,恢复到崩溃前的状态。
3.4 再次实现可撤销操作
再次实现可撤销操作example3
存储恢复式,意思就是把操作前的状态记录下来,然后要撤销操作的时候就直接恢复回去就可以了
1.范例需求
考虑一个计算器的功能,最简单的那种,只能实现加减法运算,现在要让这个计算器支持可撤销的操作。
2.存储恢复式的解决方案
- 1)把原来的运算类,就是那个Operation类,当作原发器,原来的内部状态result,就只提供一个getter方法,来让外部获取运算的结果
- 2)在这个原发器里面,实现一个私有的备忘录对象
- 3)把原来的计算器类,就是Calculator类,当作管理者,把命令对应的备忘录对象保存在这里。当需要撤销操作的时候,就把相应的备忘录对象设置回到原发器去,恢复原发器的状态
3.5 备忘录模式的优缺点
- 更好的封装性
- 简化了原发器
- 窄接口和宽接口
- 可能会导致高开销
4.思考备忘录模式
4.1 备忘录模式的本质
备忘录模式的本质是:保存和恢复内部状态
4.2 何时选用
1)如果必须保存一个对象在某一个时刻的全部或者部分状态,这样在以后需要的时候,可以把该对象恢复到先前的状态。可以使用备忘录模式,使用备忘录对象来封装和保存需要保存的内部状态,然后把备忘录对象保存到管理者对象里面,在需要的时候,再从管理者对象里面获取备忘录对象,来恢复对象的状态
2)如果需要保存一个对象的内部状态,但是如果用接口来让其它对象直接得到这些需要保存的状态,将会暴露对象的实现细节并破坏对象的封装性。可以使用备忘录模式,把备忘录对象实现成为原发器对象的内部类,而且还是私有的,从而保证只有原发器对象才能访问该备忘录对象。这样既保存了需要保存的状态,又不会暴露原发器对象的内部实现细节。