一引子
俗话说世上难买后悔药所以凡事讲究个三思而后行但总常见有人做痛心疾首状当初我要是……如果真的有《大话西游》中能时光倒流的月光宝盒那这世上也许会少一些伤感与后悔——当然这只能是癡人说梦了
但是在我们手指下的程序世界里却有的后悔药买今天我们要讲的备忘录模式便是程序世界里的月光宝盒
二定义与结构
备忘录(Memento)模式又称标记(Token)模式GOF给备忘录模式的定义为在不破坏封装性的前提下捕获一个对象的内部状态并在该对象之外保存这个状态这样以后就可将该对象恢复到原先保存的状态
在讲命令模式的时候我们曾经提到利用中间的命令角色可以实现undoredo的功能从定义可以看出备忘录模式是专门来存放对象历史状态的这对于很好的实现undoredo功能有很大的帮助所以在命令模式中undoredo功能可以配合备忘录模式来实现
其实单就实现保存一个对象在某一时刻的状态的功能还是很简单的——将对象中要保存的属性放到一个专门管理备份的对象中需要的时候则调用约定好的方法将备份的属性放回到原来的对象中去但是你要好好看看为了能让你的备份对象访问到原对象中的属性是否意味着你就要全部公开或者包内公开对象原本私有的属性呢?如果你的做法已经破坏了封装那么就要考虑重构一下了
备忘录模式只是GOF对恢复对象某时的原有状态这一问题提出的通用方案因此在如何保持封装性上——由于受到语言特性等因素的影响备忘录模式并没有详细描述只是基于C++阐述了思路那么基于Java的应用应该怎样来保持封装呢?我们将在实现一节里面讨论
来看下月光宝盒备忘录模式的组成部分
) 备忘录(Memento)角色备忘录角色存储备忘发起角色的内部状态备忘发起角色根据需要决定备忘录角色存储备忘发起角色的哪些内部状态为了防止备忘发起角色以外的其他对象访问备忘录备忘录实际上有两个接口备忘录管理者角色只能看到备忘录提供的窄接口——对于备忘录角色中存放的属性是不可见的备忘发起角色则能够看到一个宽接口——能够得到自己放入备忘录角色中属性
) 备忘发起(Originator)角色备忘发起角色创建一个备忘录用以记录当前时刻它的内部状态在需要时使用备忘录恢复内部状态
) 备忘录管理者(Caretaker)角色负责保存好备忘录不能对备忘录的内容进行操作或检查
三举例
按照定义中的要求备忘录角色要保持完整的封装最好的情况便是备忘录角色只应该暴露操作内部存储属性的的接口给备忘发起角色而对于其他角色则是不可见的GOF在书中以C++为例进行了探讨但是在Java中没有提供类似于C++中友元的概念在Java中怎样才能保持备忘录角色的封装呢?
下面对三种在Java中可保存封装的方法进行探讨
第一种就是采用两个不同的接口类来限制访问权限这两个接口类中一个提供比较完备的操作状态的方法我们称它为宽接口而另一个则可以只是一个标示我们称它为窄接口备忘录角色要实现这两个接口类这样对于备忘发起角色采用宽接口进行访问而对于其他的角色或者对象则采用窄接口进行访问
这种实现比较简单但是需要人为的进行规范约束——而这往往是没有力度的
第二种方法便很好的解决了第一种的缺陷采用内部类来控制访问权限将备忘录角色作为备忘发起角色的一个私有内部类好处我不详细解释了看看代码吧就明白了下面的代码是一个完整的备忘录模式的教学程序它便采用了第二种方法来实现备忘录模式
还有一点值得指出的是在下面的代码中对于客户程序来说备忘录管理者角色是不可见的这样简化了客户程序使用备忘录模式的难度下面采用备忘发起角色来调用访问备忘录管理者角色也可以参考门面模式在客户程序与备忘录角色之间添加一个门面角色
class Originator{
//这个是要保存的状态
private int state= ;
//保持一个备忘录管理者角色的对象
private Caretaker c = new Caretaker();
//读取备忘录角色以恢复以前的状态
public void setMemento(){
Memento memento = (Memento)cgetMemento();
state = mementogetState();
Systemoutprintln(the state is +state+ now);
}
//创建一个备忘录角色并将当前状态属性存入托给备忘录管理者角色存放
public void createMemento(){
csaveMemento(new Memento(state));
}
//this is other business methods
//they maybe modify the attribute state
public void modifyStateTest(int m){
state = m;
Systemoutprintln(the state is +state+ now);
}
//作为私有内部类的备忘录角色它实现了窄接口可以看到在第二种方法中宽接口已经不再需要
//注意里面的属性和方法都是私有的
private class Memento implements MementoIF{
private int state ;
private Memento(int state){
thisstate = state ;
}
private int getState(){
return state;
}
}
}
//测试代码——客户程序
public class TestInnerClass{
public static void main(String[] args){
Originator o = new Originator();
ocreateMemento();
omodifyStateTest();
osetMemento();
}
}
//窄接口
interface MementoIF{}
//备忘录管理者角色
class Caretaker{
private MementoIF m ;
public void saveMemento(MementoIF m){
thism = m;
}
public MementoIF getMemento(){
return m;
}
}
第三种方式是不太推荐使用的使用clone方法来简化备忘录模式由于Java提供了clone机制这使得复制一个对象变得轻松起来使用了clone机制的备忘录模式备忘录角色基本可以省略了而且可以很好的保持对象的封装但是在为你的类实现clone方法时要慎重啊
在上面的教学代码中我们简单的模拟了备忘录模式的整个流程在实际应用中我们往往需要保存大量备忘发起角色的历史状态这时就要对我们的备忘录管理者角色进行改造最简单的方式就是采用容器来按照顺序存放备忘录角色这样就可以很好的实现undoredo功能了
四适用情况
从上面的讨论可以看出使用了备忘录模式来实现保存对象的历史状态可以有效地保持封装边界使用备忘录可以避免暴露一些只应由备忘发起角色管理却又必须存储在备忘发起角色之外的信息把备忘发起角色内部信息对其他对象屏蔽起来 从而保持了封装边界
但是如果备份的备忘发起角色存在大量的信息或者创建恢复操作非常频繁则可能造成很大的开销
GOF在《设计模式》中总结了使用备忘录模式的前提
) 必须保存一个对象在某一个时刻的(部分)状态 这样以后需要时它才能恢复到先前的状态
) 如果一个用接口来让其它对象直接得到这些状态将会暴露对象的实现细节并破坏对象的封装性
五总结
介绍了怎样来使用备忘录模式实现存储对象历史状态的功能并对基于Java的实现进行了讨论欢迎大家指正