本节课程概览
学习备忘录模式
一:初识备忘录模式
包括:定义、结构、参考实现
二:体会备忘录模式
包括:场景问题、不用模式的解决方案、使用模式的解决方案
三:理解备忘录模式
包括:认识备忘录模式、结合原型模式 、离线存储 、
再次实现可撤销操作、备忘录模式的优缺点
四:思考备忘录模式
包括:备忘录模式的本质、何时选用
初识备忘录模式
定义
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保
存这个状态。这样以后就可将该对象恢复到原先保存的状态 。
结构和说明
示例
备忘录的窄接口,没有任何方法定义
package cn.javass.dp.memento.example2;
/**
* 备忘录的窄接口,没有任何方法定义
*/
public interface Memento {
}
原发器对象
package cn.javass.dp.memento.example2;
/**
* 原发器对象
*/
public class Originator {
/**
* 示意,表示原发器的状态
*/
private String state = "";
/**
* 创建保存原发器对象的状态的备忘录对象
* @return 创建好的备忘录对象
*/
public Memento createMemento() {
return new MementoImpl(state);
}
/**
* 重新设置原发器对象的状态,让其回到备忘录对象记录的状态
* @param memento 记录有原发器状态的备忘录对象
*/
public void setMemento(Memento memento) {
MementoImpl mementoImpl = (MementoImpl)memento;
this.state = mementoImpl.getState();
}
/**
* 真正的备忘录对象,实现备忘录窄接口
* 实现成私有的内部类,不让外部访问
*/
private static class MementoImpl implements Memento{
/**
* 示意,表示需要保存的状态
*/
private String state = "";
public MementoImpl(String state){
this.state = state;
}
public String getState() {
return state;
}
}
}
负责保存备忘录的对象
package cn.javass.dp.memento.example2;
import java.util.*;
/**
* 负责保存备忘录的对象
*/
public class Caretaker{
/**
* 记录被保存的备忘录对象
*/
private Memento memento = null;
/**
* 保存备忘录对象
* @param memento 被保存的备忘录对象
*/
public void saveMemento(Memento memento){
this.memento = memento;
}
/**
* 获取被保存的备忘录对象
* @return 被保存的备忘录对象
*/
public Memento retriveMemento(){
return this.memento;
}
}
体会备忘录模式
Memento:
备忘录。主要用来存储原发器对象的内部状态,但是具体需要存储哪些数
据是由原发器对象来决定的。另外备忘录应该只能由原发器对象来访问它内部的
数据,原发器外部的对象不应该能访问到备忘录对象的内部数据。
Originator:
原发器。使用备忘录来保存某个时刻原发器自身的状态,也可以使用备忘
录来恢复内部状态。
Caretaker:
备忘录管理者,或者称为备忘录负责人。主要负责保存备忘录对象,但是
不能对备忘录对象的内容进行操作或检查。
开发仿真系统
考虑这样一个仿真应用,功能是:模拟运行针对某个具体问题的多个解决
方案,记录运行过程的各种数据,在模拟运行完成过后,好对这多个解决方案进
行比较和评价,从而选定最优的解决方案。
由于都是解决同一个具体的问题,这多个解决方案并不是完全不一样的,
假定它们的前半部分运行是完全一样的,只是在后半部分采用了不同的解决方
案,后半部分需要使用前半部分运行所产生的数据。
由于要模拟运行多个解决方案,而且最后要根据运行结果来进行评价,这
就意味着每个方案的后半部分的初始数据应该是一样。
那么,这样的系统该如何实现呢?尤其是每个方案运行需要的初始数据应
该一样,要如何来保证呢?
不用模式的解决方案
要保证初始数据的一致,实现思路也很简单:
- 1:首先模拟运行流程第一个阶段,得到后阶段各个方案运行需要的数据,并把数据
保存下来,以备后用
- 2:每次在模拟运行某一个方案之前,用保存的数据去重新设置模拟运行流程的对
象,这样运行后面不同的方案时,对于这些方案,初始数据就是一样的了
示例
模拟运行流程A,只是一个示意,代指某个具体流程
package cn.javass.dp.memento.example1;
/**
* 模拟运行流程A,只是一个示意,代指某个具体流程
*/
public class FlowAMock {
/**
* 流程名称,不需要外部存储的状态数据
*/
private String flowName;
/**
* 示意,代指某个中间结果,需要外部存储的状态数据
*/
private int tempResult;
/**
* 示意,代指某个中间结果,需要外部存储的状态数据
*/
private String tempState;
/**
* 构造方法,传入流程名称
* @param flowName 流程名称
*/
public FlowAMock(String flowName){
this.flowName = flowName;
}
public String getTempState() {
return tempState;
}
public void setTempState(String tempState) {
this.tempState = tempState;
}
public int getTempResult() {
return tempResult;
}
public void setTempResult(int tempResult) {
this.tempResult = tempResult;
}
/**
* 示意,运行流程的第一个阶段
*/
public void runPhaseOne(){
//在这个阶段,可能产生了中间结果,示意一下
tempResult = 3;
tempState = "PhaseOne";
}
/**
* 示意,按照方案一来运行流程后半部分
*/
public void schema1(){
//示意,需要使用第一个阶段产生的数据
this.tempState += ",Schema1";
System.out.println(this.tempState + " : now run "+tempResult);
this.tempResult += 11;
}
/**
* 示意,按照方案二来运行流程后半部分
*/
public void schema2(){
//示意,需要使用第一个阶段产生的数据
this.tempState += ",Schema2";
System.out.println(this.tempState + " : now run "+tempResult);
this.tempResult += 22;
}
}
客户端
package cn.javass.dp.memento.example1;
public class Client {
public static void main(String[] args) {
// 创建模拟运行流程的对象
FlowAMock mock = new FlowAMock("TestFlow");
//运行流程的第一个阶段
mock.runPhaseOne();
//得到第一个阶段运行所产生的数据,后面要用
int tempResult = mock.getTempResult();
String tempState = mock.getTempState();
//按照方案一来运行流程后半部分
mock.schema1();
//把第一个阶段运行所产生的数据重新设置回去
mock.setTempResult(tempResult);
mock.setTempState(tempState);
//按照方案二来运行流程后半部分
mock.schema2();
}
}
有何问题
上面的实现有一个不太好的地方,那就是数据是一个一个零散着在外部存
放的,如果需要外部存放的数据多了,会显得很杂乱。这个好解决,只需要定义
一个数据对象来封装这些需要外部存放的数据就可以了。
还有一个严重的问题:为了把运行期间的数据放到外部存储起来,模拟流
程的对象被迫把内部数据结构开放出来,这暴露了对象的实现细节,而且也破坏
了对象的封装性。本来这些数据只是模拟流程的对象内部数据,是不对外的。
那么究竟如何实现这样的功能会比较好呢?
使用模式来解决的思路
在这个示例中出现的、需要解决的问题就是:如何能够在不破坏对象的封
装性的前提下,来保存和恢复对象的状态。
备忘录模式引入一个存储状态的备忘录对象,为了让外部无法访问这个对
象的值,一般把这个对象实现成为需要保存数据的对象的内部类,通常还是私有
的,这样一来,除了这个需要保存数据的对象,外部无法访问到这个备忘录对象
的数据,这就保证了对象的封装性不被破坏。
但是这个备忘录对象需要存储在外部,为了避免让外部访问到这个对象内
部的数据,备忘录模式引入了一个备忘录对象的窄接口,这个接口一般是空的,
什么方法都没有,这样外部存储的地方,只是知道存储了一些备忘录接口的对
象,但是由于接口是空的,它们无法通过接口去访问备忘录对象内的数据。
使用模式的解决方案的类图
示例
模拟运行流程A的对象的备忘录接口,是个窄接口
package cn.javass.dp.memento.example3;
import java.io.*;
/**
* 模拟运行流程A的对象的备忘录接口,是个窄接口
*/
public interface FlowAMockMemento extends java.io.Serializable{
//空的
}
模拟运行流程A,只是一个示意,代指某个具体流程(内部数据备忘)
package cn.javass.dp.memento.example3;
import java.io.Serializable;
/**
* 模拟运行流程A,只是一个示意,代指某个具体流程
*/
public class FlowAMock{
/**
* 流程名称,不需要外部存储的状态数据
*/
private String flowName;
/**
* 示意,代指某个中间结果,需要外部存储的状态数据
*/
private int tempResult;
/**
* 示意,代指某个中间结果,需要外部存储的状态数据
*/
private String tempState;
/**
* 构造方法,传入流程名称
* @param flowName 流程名称
*/
public FlowAMock(String flowName){
this.flowName = flowName;
}
/**
* 示意,运行流程的第一个阶段
*/
public void runPhaseOne(){
//在这个阶段,可能产生了中间结果,示意一下
tempResult = 3;
tempState = "PhaseOne";
}
/**
* 示意,按照方案一来运行流程后半部分
*/
public void schema1(){
//示意,需要使用第一个阶段产生的数据
this.tempState += ",Schema1";
System.out.println(this.tempState + " : now run "+tempResult);
this.tempResult += 11;
}
/**
* 示意,按照方案二来运行流程后半部分
*/
public void schema2(){
//示意,需要使用第一个阶段产生的数据
this.tempState += ",Schema2";
System.out.println(this.tempState + " : now run "+tempResult);
this.tempResult += 22;
}
/**
* 创建保存原发器对象的状态的备忘录对象
* @return 创建好的备忘录对象
*/
public FlowAMockMemento createMemento() {
return new MementoImpl(this.tempResult,this.tempState);
}
/**
* 重新设置原发器对象的状态,让其回到备忘录对象记录的状态
* @param memento 记录有原发器状态的备忘录对象
*/
public void setMemento(FlowAMockMemento memento) {
MementoImpl mementoImpl = (MementoImpl)memento;
this.tempResult = mementoImpl.getTempResult();
this.tempState = mementoImpl.getTempState();
}
/**
* 真正的备忘录对象,实现备忘录窄接口
* 实现成私有的内部类,不让外部访问
*/
private static class MementoImpl implements FlowAMockMemento{
/**
* 示意,保存某个中间结果
*/
private int tempResult;
/**
* 示意,保存某个中间结果
*/
private String tempState;
public MementoImpl(int tempResult,String tempState){
this.tempResult = tempResult;
this.tempState = tempState;
}
public int getTempResult() {
return tempResult;
}
public String getTempState() {
return tempState;
}
}
}
模拟运行流程A,只是一个示意,代指某个具体流程(类拷贝)
package cn.javass.dp.memento.example3;
import java.io.Serializable;
/**
* 模拟运行流程A,只是一个示意,代指某个具体流程
*/
public class FlowAMockPrototype implements Cloneable {
/**
* 流程名称,不需要外部存储的状态数据
*/
private String flowName;
/**
* 示意,代指某个中间结果,需要外部存储的状态数据
*/
private int tempResult;
/**
* 示意,代指某个中间结果,需要外部存储的状态数据
*/
private String tempState;
/**
* 构造方法,传入流程名称
* @param flowName 流程名称
*/
public FlowAMockPrototype(String flowName){
this.flowName = flowName;
}
/**
* 示意,运行流程的第一个阶段
*/
public void runPhaseOne(){
//在这个阶段,可能产生了中间结果,示意一下
tempResult = 3;
tempState = "PhaseOne";
}
/**
* 示意,按照方案一来运行流程后半部分
*/
public void schema1(){
//示意,需要使用第一个阶段产生的数据
this.tempState += ",Schema1";
System.out.println(this.tempState + " : now run "+tempResult);
this.tempResult += 11;
}
/**
* 示意,按照方案二来运行流程后半部分
*/
public void schema2(){
//示意,需要使用第一个阶段产生的数据
this.tempState += ",Schema2";
System.out.println(this.tempState + " : now run "+tempResult);
this.tempResult += 22;
}
/**
* 创建保存原发器对象的状态的备忘录对象
* @return 创建好的备忘录对象
*/
public FlowAMockMemento createMemento() {
try {
return new MementoImplPrototype((FlowAMockPrototype) this.clone());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
/**
* 重新设置原发器对象的状态,让其回到备忘录对象记录的状态
* @param memento 记录有原发器状态的备忘录对象
*/
public void setMemento(FlowAMockMemento memento) {
MementoImplPrototype mementoImpl = (MementoImplPrototype)memento;
this.tempResult = mementoImpl.getFlowAMock().tempResult;
this.tempState = mementoImpl.getFlowAMock().tempState;
}
/**
* 真正的备忘录对象,实现备忘录窄接口
* 实现成私有的内部类,不让外部访问
*/
private static class MementoImplPrototype implements FlowAMockMemento{
private FlowAMockPrototype flowAMock = null;
public MementoImplPrototype(FlowAMockPrototype f){
this.flowAMock = f;
}
public FlowAMockPrototype getFlowAMock() {
return flowAMock;
}
}
}
客户端
package cn.javass.dp.memento.example3;
public class Client {
public static void main(String[] args) {
// 创建模拟运行流程的对象
FlowAMock mock = new FlowAMock("TestFlow");
//运行流程的第一个阶段
mock.runPhaseOne();
//创建一个管理者
FlowAMementoFileCareTaker careTaker = new FlowAMementoFileCareTaker();
//创建此时对象的备忘录对象,并保存到管理者对象那里,后面要用
FlowAMockMemento memento = mock.createMemento();
careTaker.saveMemento(memento);
//按照方案一来运行流程后半部分
mock.schema1();
//从管理者获取备忘录对象,然后设置回去,
//让模拟运行流程的对象自己恢复自己的内部状态
mock.setMemento(careTaker.retriveMemento());
//按照方案二来运行流程后半部分
mock.schema2();
}
}
理解备忘录模式
认识备忘录模式
1:备忘录模式的功能
备忘录模式的功能,首先是在不破坏封装性的前提下,捕获一个对象的内部状
态。这里要注意两点,一个是不破坏封装性,也就是对象不能暴露它不应该暴露的细
节;另外一个是捕获的是对象的内部状态,而且通常还是运行期间某个时刻,对象的
内部状态。
为什么要捕获这个对象的内部状态呢?捕获这个内部状态有什么用呢?
是要在以后的某个时候,将该对象的状态恢复到备忘录所保存的状态,这才是
备忘录真正的目的,前面保存状态就是为了后面恢复,虽然不是一定要恢复,但是目
的是为了恢复。这也是很多人理解备忘录模式的时候,忽视掉的地方,他们太关注备
忘,而忽视了恢复,这是不全面的理解。
捕获的状态存放在哪里呢?
备忘录模式中,捕获的内部状态,存储在备忘录对象中;而备忘录对象,通常
会被存储在原发器对象之外,也就是被保存状态的对象的外部,通常是存放在管理者
对象哪里。
2:备忘录对象
在备忘录模式中,备忘录对象,通常就是用来记录原发器需要保存的状态
的对象,简单点的实现,也就是个封装数据的对象。
但是这个备忘录对象和普通的封装数据的对象还是有区别的,主要就是这
个备忘录对象,一般只让原发器对象来操作,而不是像普通的封装数据的对象那
样,谁都可以使用。为了保证这一点,通常会把备忘录对象作为原发器对象的内
部类来实现,而且会实现成私有的,这就断绝了外部来访问这个备忘录对象的途
径。
但是备忘录对象需要保存在原发器对象之外,为了与外部交互,通常备忘
录对象都会实现一个窄接口,来标识对象的类型。。
3:原发器对象
原发器对象,就是需要被保存状态的对象,也是有可能需要恢复状态的对
象。原发器一般会包含备忘录对象的实现。
通常原发器对象应该提供捕获某个时刻对象内部状态的方法,在这个方法
里面,原发器对象会创建备忘录对象,把需要保存的状态数据设置到备忘录对象
中,然后把备忘录对象提供给管理者对象来保存。
当然,原发器对象也应该提供这样的方法:按照外部要求来恢复内部状态
到某个备忘录对象记录的状态。
4:管理者对象
在备忘录模式中,管理者对象,主要是负责保存备忘录对象,这里有几点
要讲一下。
- (1):并不一定要特别的做出一个管理者对象来,广义地说,调用原发器获得备忘
录对象后,备忘录对象放在哪里,哪个对象就可以算是管理者对象。
- (2):管理者对象并不是只能管理一个备忘录对象,一个管理者对象可以管理很多
的备忘录对象,虽然前面的示例中是保存一个备忘录对象,别忘了那只是个示
意,并不是只能实现成那样。
- (3):狭义的管理者对象,是只管理同一类的备忘录对象,但是广义管理者对象是
可以管理不同类型的备忘录对象的。
- (4):管理者对象需要实现的基本功能主要就是:存入备忘录对象、保存备忘录对
象、获取备忘录对象,如果从功能上看,就是一个缓存功能的实现,或者是一个
简单的对象实例池的实现。
- (5):管理者虽然能存取备忘录对象,但是不能访问备忘录对象内部的数据。
5:窄接口和宽接口
窄接口:管理者只能看到备忘录的窄接口,它的实现里面通常没有任何的
方法,只是一个类型标识,窄接口使得管理者只能将备忘录传递给其它对象。
宽接口:原发器能够看到一个宽接口,允许它访问所需的所有数据,来返
回到先前的状态。理想状况是:只允许生成备忘录的原发器来访问该备忘录的内
部状态,通常实现成为原发器内的一个私有内部类。
备忘录模式的标准实现方式,那就是窄接口没有任何的方法,把备忘录对
象实现成为原发器对象的私有内部类。那么能不能在窄接口里面提供备忘录对象
对外的方法,变相对外提供一个“宽”点的接口呢?
通常情况是不会这么做的,因为这样一来,所有能拿到这个接口的对象就
可以通过这个接口来访问备忘录内部的数据或是功能,这违反了备忘录模式的初
衷,备忘录模式要求“在不破坏封装性的前提下”,如果这么做,那就等于是暴
露了内部细节,因此,备忘录模式在实现的时候,对外多是采用窄接口,而且通
常不会定义任何方法。
6:使用备忘录的潜在代价
标准的备忘录模式的实现机制是依靠缓存来实现的,因此,当需要备忘的数据
量较大时,或者是存储的备忘录对象数据量不大但是数量很多的时候,或者是用户很
频繁的创建备忘录对象的时候,这些都会导致非常大的开销。
因此在使用备忘录模式的时候,一定要好好思考应用的环境,如果使用的代价
太高,就不要选用备忘录模式,可以采用其它的替代方案
7:增量存储
如果需要频繁的创建备忘录对象,而且创建和应用备忘录对象来恢复状态的顺
序是可控的,那么可以让备忘录进行增量存储,也就是备忘录可以仅仅存储原发器内
部相对于上一次存储状态后的增量改变。
比如:在命令模式实现可撤销命令的实现中,就可以使用备忘录来保存每个命
令对应的状态,然后在撤销命令的时候,使用备忘录来恢复这些状态。由于命令的历
史列表是按照命令操作的顺序来存放的,也是按照这个历史列表来进行取消和重做
的,因此顺序是可控的。那么这种情况,还可以让备忘录对象只存储一个命令所产生
的增量改变而不是它所影响的每一个对象的完整状态。
8:备忘录模式调用顺序示意图
在使用备忘录模式的时候,分成了两个阶段,第一个阶段是创建备忘录对
象的阶段,第二个阶段是使用备忘录对象来恢复原发器对象的状态的阶段。它们
的调用顺序是不一样的,下面分开用图来示意一下
使用备忘录对象来恢复原发器对象的状态的阶段,调用顺序如图
结合原型模式
在原发器对象创建备忘录对象的时候,如果原发器对象中全部或者大部分
的状态都需要保存,一个简洁的方式就是直接克隆一个原发器对象。也就是说,
这个时候备忘录对象里面存放的是一个原发器对象的实例 。
示例
package cn.javass.dp.memento.example3;
import java.io.Serializable;
/**
* 模拟运行流程A,只是一个示意,代指某个具体流程
*/
public class FlowAMockPrototype implements Cloneable {
/**
* 流程名称,不需要外部存储的状态数据
*/
private String flowName;
/**
* 示意,代指某个中间结果,需要外部存储的状态数据
*/
private int tempResult;
/**
* 示意,代指某个中间结果,需要外部存储的状态数据
*/
private String tempState;
/**
* 构造方法,传入流程名称
* @param flowName 流程名称
*/
public FlowAMockPrototype(String flowName){
this.flowName = flowName;
}
/**
* 示意,运行流程的第一个阶段
*/
public void runPhaseOne(){
//在这个阶段,可能产生了中间结果,示意一下
tempResult = 3;
tempState = "PhaseOne";
}
/**
* 示意,按照方案一来运行流程后半部分
*/
public void schema1(){
//示意,需要使用第一个阶段产生的数据
this.tempState += ",Schema1";
System.out.println(this.tempState + " : now run "+tempResult);
this.tempResult += 11;
}
/**
* 示意,按照方案二来运行流程后半部分
*/
public void schema2(){
//示意,需要使用第一个阶段产生的数据
this.tempState += ",Schema2";
System.out.println(this.tempState + " : now run "+tempResult);
this.tempResult += 22;
}
/**
* 创建保存原发器对象的状态的备忘录对象
* @return 创建好的备忘录对象
*/
public FlowAMockMemento createMemento() {
try {
return new MementoImplPrototype((FlowAMockPrototype) this.clone());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
/**
* 重新设置原发器对象的状态,让其回到备忘录对象记录的状态
* @param memento 记录有原发器状态的备忘录对象
*/
public void setMemento(FlowAMockMemento memento) {
MementoImplPrototype mementoImpl = (MementoImplPrototype)memento;
this.tempResult = mementoImpl.getFlowAMock().tempResult;
this.tempState = mementoImpl.getFlowAMock().tempState;
}
/**
* 真正的备忘录对象,实现备忘录窄接口
* 实现成私有的内部类,不让外部访问
*/
private static class MementoImplPrototype implements FlowAMockMemento{
private FlowAMockPrototype flowAMock = null;
public MementoImplPrototype(FlowAMockPrototype f){
this.flowAMock = f;
}
public FlowAMockPrototype getFlowAMock() {
return flowAMock;
}
}
}
离线存储
标准的备忘录模式,没有讨论离线存储的实现。
事实上,从备忘录模式的功能和实现上,是可以把备忘录的数据实现成为
离线存储的,也就是不仅限于存储于内存中,可以把这些备忘数据存储到文件
中、xml中、数据库中,从而支持跨越会话的备份和恢复功能。
离线存储甚至能帮助应对应用崩溃,然后关闭重启的情况,应用重启过
后,从离线存储里面获取相应的数据,然后重新设置状态,恢复到崩溃前的状
态。
示例
package cn.javass.dp.memento.example3;
import java.io.*;
/**
* 负责在文件中保存模拟运行流程A的对象的备忘录对象
*/
public class FlowAMementoFileCareTaker {
/**
* 保存备忘录对象
* @param memento 被保存的备忘录对象
*/
public void saveMemento(FlowAMockMemento memento){
//写到文件中
ObjectOutputStream out = null;
try{
out = new ObjectOutputStream(
new BufferedOutputStream(
new FileOutputStream("FlowAMemento")
)
);
out.writeObject(memento);
System.out.println("now write file==========");
}catch(Exception err){
err.printStackTrace();
}finally{
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 获取被保存的备忘录对象
* @return 被保存的备忘录对象
*/
public FlowAMockMemento retriveMemento(){
FlowAMockMemento memento = null;
//从文件中获取备忘录数据
ObjectInputStream in = null;
try{
in = new ObjectInputStream(
new BufferedInputStream(
new FileInputStream("FlowAMemento")
)
);
memento = (FlowAMockMemento)in.readObject();
}catch(Exception err){
err.printStackTrace();
}finally{
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return memento;
}
}
再次实现可撤销操作
存储恢复式,意思就是把操作前的状态记录下来,然后要撤销操作的时候
就直接恢复回去就可以了
1:范例需求
考虑一个计算器的功能,最简单的那种,只能实现加减法运算,现在要让
这个计算器支持可撤销的操作。
2:存储恢复式的解决方案
- (1)把原来的运算类,就是那个Operation类,当作原发器,原来的内部状态
result,就只提供一个getter方法,来让外部获取运算的结果 - (2)在这个原发器里面,实现一个私有的备忘录对象
- (3)把原来的计算器类,就是Calculator类,当作管理者,把命令对应的备忘录对
象保存在这里。当需要撤销操作的时候,就把相应的备忘录对象设置回到原发器
去,恢复原发器的状态
示例
Memento
package cn.javass.dp.memento.example4;
public interface Memento {
//空的
}
操作运算的接口
package cn.javass.dp.memento.example4;
/**
* 操作运算的接口
*/
public interface OperationApi {
/**
* 获取计算完成后的结果
* @return 计算完成后的结果
*/
public int getResult();
/**
* 执行加法
* @param num 需要加的数
*/
public void add(int num);
/**
* 执行减法
* @param num 需要减的数
*/
public void substract(int num);
/**
* 创建保存原发器对象的状态的备忘录对象
* @return 创建好的备忘录对象
*/
public Memento createMemento();
/**
* 重新设置原发器对象的状态,让其回到备忘录对象记录的状态
* @param memento 记录有原发器状态的备忘录对象
*/
public void setMemento(Memento memento);
}
运算类,真正实现加减法运算
package cn.javass.dp.memento.example4;
/**
* 运算类,真正实现加减法运算
*/
public class Operation implements OperationApi{
/**
* 记录运算的结果
*/
private int result;
public int getResult() {
return result;
}
public void add(int num){
result += num;
}
public void substract(int num){
result -= num;
}
public Memento createMemento() {
MementoImpl m = new MementoImpl(result);
return m;
}
public void setMemento(Memento memento) {
MementoImpl m = (MementoImpl)memento;
this.result = m.getResult();
}
/**
* 备忘录对象
*/
private static class MementoImpl implements Memento{
private int result = 0;
public MementoImpl(int result){
this.result = result;
}
public int getResult() {
return result;
}
}
}
定义一个命令的接口
package cn.javass.dp.memento.example4;
/**
* 定义一个命令的接口
*/
public interface Command {
/**
* 执行命令
*/
public void execute();
/**
* 撤销命令,恢复到备忘录对象记录的状态
* @param m 备忘录对象
*/
public void undo(Memento m);
/**
* 重做命令,恢复到备忘录对象记录的状态
* @param m 备忘录对象
*/
public void redo(Memento m);
/**
* 创建保存原发器对象的状态的备忘录对象
* @return 创建好的备忘录对象
*/
public Memento createMemento();
}
命令对象的公共对象,实现各个命令对象的公共方法
package cn.javass.dp.memento.example4;
/**
* 命令对象的公共对象,实现各个命令对象的公共方法
*/
public abstract class AbstractCommand implements Command{
/**
* 具体的功能实现,这里不管
*/
public abstract void execute();
/**
* 持有真正的命令实现者对象
*/
protected OperationApi operation = null;
public void setOperation(OperationApi operation) {
this.operation = operation;
}
public Memento createMemento() {
return this.operation.createMemento();
}
public void redo(Memento m) {
this.operation.setMemento(m);
}
public void undo(Memento m) {
this.operation.setMemento(m);
}
}
加法命令
package cn.javass.dp.memento.example4;
public class AddCommand extends AbstractCommand{
private int opeNum;
public AddCommand(int opeNum){
this.opeNum = opeNum;
}
public void execute() {
this.operation.add(opeNum);
}
}
减法命令
package cn.javass.dp.memento.example4;
public class SubstractCommand extends AbstractCommand{
private int opeNum;
public SubstractCommand(int opeNum){
this.opeNum = opeNum;
}
public void execute() {
this.operation.substract(opeNum);
}
}
计算器类,计算器上有加法按钮、减法按钮,还有撤销和恢复的按钮
package cn.javass.dp.memento.example4;
import java.util.*;
/**
* 计算器类,计算器上有加法按钮、减法按钮,还有撤销和恢复的按钮
*/
public class Calculator {
/**
* 命令的操作的历史记录,在撤销时候用
*/
private List<Command> undoCmds = new ArrayList<Command>();
/**
* 命令被撤销的历史记录,在恢复时候用
*/
private List<Command> redoCmds = new ArrayList<Command>();
/**
* 命令操作对应的备忘录对象的历史记录,在撤销时候用
* 由于对于每个命令对象,撤销和重做的状态是不一样的,
* 撤销是回到命令操作前的状态,而重做是回到命令操作后的状态,
* 因此对每一个命令,使用一个备忘录对象的数组来记录对应的状态
*/
private List<Memento[]> undoMementos = new ArrayList<Memento[]>();
/**
* 被撤销命令对应的备忘录对象的历史记录,在恢复时候用,
* 数组有两个元素,第一个是命令执行前的状态,第二个是命令执行后的状态
*/
private List<Memento[]> redoMementos = new ArrayList<Memento[]>();
private Command addCmd = null;
private Command substractCmd = null;
public void setAddCmd(Command addCmd) {
this.addCmd = addCmd;
}
public void setSubstractCmd(Command substractCmd) {
this.substractCmd = substractCmd;
}
public void addPressed(){
//获取对应的备忘录对象,并保存在相应的历史记录里面
Memento m1 = this.addCmd.createMemento();
//执行命令
this.addCmd.execute();
//把操作记录到历史记录里面
undoCmds.add(this.addCmd);
//获取执行命令后的备忘录对象
Memento m2 = this.addCmd.createMemento();
//设置到撤销的历史记录里面
this.undoMementos.add(new Memento[]{m1,m2});
}
public void substractPressed(){
//获取对应的备忘录对象,并保存在相应的历史记录里面
Memento m1 = this.substractCmd.createMemento();
//执行命令
this.substractCmd.execute();
//把操作记录到历史记录里面
undoCmds.add(this.substractCmd);
//获取执行命令后的备忘录对象
Memento m2 = this.substractCmd.createMemento();
//设置到撤销的历史记录里面
this.undoMementos.add(new Memento[]{m1,m2});
}
public void undoPressed(){
if(undoCmds.size()>0){
//取出最后一个命令来撤销
Command cmd = undoCmds.get(undoCmds.size()-1);
//获取对应的备忘录对象
Memento[] ms = undoMementos.get(undoCmds.size()-1);
//撤销
cmd.undo(ms[0]);
//如果还有恢复的功能,那就把这个命令记录到恢复的历史记录里面
redoCmds.add(cmd);
//把相应的备忘录对象也添加过去
redoMementos.add(ms);
//然后把最后一个命令删除掉,
undoCmds.remove(cmd);
//把相应的备忘录对象也删除掉
undoMementos.remove(ms);
}else{
System.out.println("很抱歉,没有可撤销的命令");
}
}
public void redoPressed(){
if(redoCmds.size()>0){
//取出最后一个命令来重做
Command cmd = redoCmds.get(redoCmds.size()-1);
//获取对应的备忘录对象
Memento[] ms = redoMementos.get(redoCmds.size()-1);
//重做
cmd.redo(ms[1]);
//把这个命令记录到可撤销的历史记录里面
undoCmds.add(cmd);
//把相应的备忘录对象也添加过去
undoMementos.add(ms);
//然后把最后一个命令删除掉
redoCmds.remove(cmd);
//把相应的备忘录对象也删除掉
redoMementos.remove(ms);
}else{
System.out.println("很抱歉,没有可恢复的命令");
}
}
}
客户端
package cn.javass.dp.memento.example4;
public class Client {
public static void main(String[] args) {
//1:组装命令和接收者
//创建接收者
OperationApi operation = new Operation();
//创建命令
AddCommand addCmd = new AddCommand(5);
SubstractCommand substractCmd = new SubstractCommand(3);
//组装命令和接收者
addCmd.setOperation(operation);
substractCmd.setOperation(operation);
//2:把命令设置到持有者,就是计算器里面
Calculator calculator = new Calculator();
calculator.setAddCmd(addCmd);
calculator.setSubstractCmd(substractCmd);
//3:模拟按下按钮,测试一下
calculator.addPressed();
System.out.println("一次加法运算后的结果为:"+operation.getResult());
calculator.substractPressed();
System.out.println("一次减法运算后的结果为:"+operation.getResult());
//测试撤消
calculator.undoPressed();
System.out.println("撤销一次后的结果为:"+operation.getResult());
calculator.undoPressed();
System.out.println("再撤销一次后的结果为:"+operation.getResult());
//测试恢复
calculator.redoPressed();
System.out.println("恢复操作一次后的结果为:"+operation.getResult());
calculator.redoPressed();
System.out.println("再恢复操作一次后的结果为:"+operation.getResult());
}
}
备忘录模式的优缺点
- 1:更好的封装性
- 2:简化了原发器
- 3:窄接口和宽接口
- 4:可能会导致高开销
思考备忘录模式
备忘录模式的本质
备忘录模式的本质是:保存和恢复内部状态
何时选用备忘录模式
1:如果必须保存一个对象在某一个时刻的全部或者部分状态,这样在以后需要的时
候,可以把该对象恢复到先前的状态。可以使用备忘录模式,使用备忘录对象来
封装和保存需要保存的内部状态,然后把备忘录对象保存到管理者对象里面,在
需要的时候,再从管理者对象里面获取备忘录对象,来恢复对象的状态
2:如果需要保存一个对象的内部状态,但是如果用接口来让其它对象直接得到这些
需要保存的状态,将会暴露对象的实现细节并破坏对象的封装性。可以使用备忘
录模式,把备忘录对象实现成为原发器对象的内部类,而且还是私有的,从而保
证只有原发器对象才能访问该备忘录对象。这样既保存了需要保存的状态,又不
会暴露原发器对象的内部实现细节。