中国有句古话:"千金难买后悔药"。生活中很多时候,我们做过了的事情,虽然后悔,却无济于事。但是,在软件世界,我们却可以自己制作"后悔药"。比如,以前玩"仙剑",每每遇到Boss战,那必须要存档的,就算没打过也可以恢复存档再来一次,否则,玩过的人都知道,哭去吧……这里的游戏存档,就会用到今天说的——备忘录模式。
其实还有很多这种例子,比如Word等办公软件的撤销操作,撤销后需要恢复到前一状态;又比如,虚拟机或者今天的云服务器都支持创建快照,如果系统出问题了,可以直接恢复到快照版本……
1. 概念
备忘录,顾名思义用来做备份的,后续可能按照备份数据进行恢复。DP 对备忘录模式的定义如下:
定义 备忘录模式(Memento Pattern):在 不破坏封装性 的前提下,捕获一个对象的内部状态,将在 该对象之外 保存这个状态。这样以后就可将该对象恢复到原先保存的状态。 |
这个定义比较好理解,其基本思想就是要将对象的内部状态保存下来,便于之后恢复,但是保存的逻辑不由对象本身负责,而是单独抽出取来,减轻对象的职责,同时也便于扩展。这个思想与前边的 迭代器模式] 非常类似。
需要保存内部状态的这个对象,我们称为 原发器 (Originator),抽取出来的保存原发器状态的对象我们称为 备忘录 (Memento),我们来看看它们之间的关系。
2. 结构
备忘录模式的结构如下:
可以看到,备忘录模式中有三种角色:
Originator: 即原发器,持有内部状态,提供创建备忘录的方法,以保存某个时刻内部的全部或部分状态,同时还提供还原的方法,从而支持后续可以还原到保存的状态
Memento: 备忘录对象,它存储 Originator 的状态,但要求它能够防止除了 Originator 之外的对象访问备忘录
Caretaker: 管理者,它负责管理备忘录,包括存储、删除操作,但是要求它本身不能访问和修改备忘录
备忘录的宽窄接口 备忘录对象它要求能够防止除了 Originator 之外的对象访问备忘录,而管理者又不能访问和修改备忘录,因此,要求备忘录能够具有 宽窄两种接口,Originator 能够通过 宽接口 创建、访问、修改备忘录对象,而管理者只能使用 窄接口 存储、删除和允许他人查询备忘录,本身不能修改和访问备忘录。 |
备忘录模式具有如下的优缺点:
抽取了备忘录对象,用来保存状态,减轻了原发器的职责
备忘录和原发器都可以再次抽象出单独的接口,便于扩展
备忘录会创建多个状态的副本,可能造成很大的开销
管理者管理多个备忘录,但它并不知道备忘录的内部状态,一个小的管理者可能存储和删除很大的备忘录,带来大的开销
备忘录模式适用于以下场景:
对象需要保存自身某一时刻的状态,以便后续进行恢复
对象保存自身状态时不暴露其实现细节,需要保持其封装性不被破坏
3. 实现
备忘录模式的实现大概有三种方式,每一种都有其适用场景。
3.1. 标准实现
标准实现方式,它抽取备忘录为单独的对象,由管理者来存储。我们以仙剑游戏为例,看看示例代码实现,如下:
1、定义备忘录对象
public class Memento {
private State state;
public Memento(State state) { (1)
this.state = state;
}
public State getState() {
return state;
}
public void setState(State state) {
this.state = state;
}
}
1 | 内部存储了原发器的状态对象 |
2、定义原发器
public final class State {
// 攻击力
private final int ack;
// 防御力
private final int def;
// 血量
private final int hp;
// 蓝量
private final int mp;
public State(int ack, int def, int hp, int mp) {
this.ack = ack;
this.def = def;
this.hp = hp;
this.mp = mp;
}
@Override
public String toString() {
return "攻击力 " + ack + ", 防御力 " + def + ", 血量 " + hp + ", 蓝量 " + mp;
}
}
状态对象定义了攻击力、防御力、血量和蓝量值等属性。
public class Originator {
private State state;
// 角色名称
private final String name;
// 角色称号
private String title;
public Originator(String name) {
this.name = name;
}
public Memento createMemento() { (1)
return new Memento(this.state);
}
public void recover(Memento memento) { (2)
this.state = memento.getState();
}
// 省略getter/setter...
@Override
public String toString() {
return this.name + "[" + this.title + "] : " + this.state.toString();
}
}
1 | 创建备忘录对象 |
2 | 用备忘录恢复原发器到某一个时刻的状态 |
3、定义管理者
public class Caretaker {
private Memento memento;
public Memento getMemento() { (1)
return memento;
}
public void setMemento(Memento memento) { (2)
this.memento = memento;
}
}
1 | 供其他对象查询存储的备忘录 |
2 | 存储备忘录 |
可以看到,管理者只是存储备忘录和提供查询接口,本身并不修改备忘录。为了简单,这里略去了删除备忘录操作。
4、客户端调用
public class MementoClient {
public static void main(String[] args) {
// 初始状态
String name = "李逍遥";
Originator originator = new Originator(name);
originator.setState(new State(10, 10, 100, 100));
originator.setTitle("出生小菜鸟");
System.out.println(originator);
// 打怪升级一段时间后
originator.setState(new State(40, 50, 40, 10));
originator.setTitle("江湖小虾米");
System.out.println(originator);
// 弄了一套装备,准备打boss
originator.setState(new State(90, 80, 100, 100));
originator.setTitle("武林高手");
System.out.println(originator);
// 存档
Memento memento = originator.createMemento(); (1)
Caretaker caretaker = new Caretaker();
caretaker.setMemento(memento); (2)
// 打BOSS之后,挂了,需要恢复存档
originator.setState(new State(90, 80, 0, 0));
System.out.println("挑战boss失败:" + originator);
originator.recover(caretaker.getMemento()); (3)
System.out.println("恢复存档:" + originator);
}
}
1 | 原发器创建备忘录 |
2 | 管理者存储备忘录 |
3 | 原发器从管理者那儿获取备忘录对象并恢复 |
上边的代码输出如下:
李逍遥[出生小菜鸟] : 攻击力 10, 防御力 10, 血量 100, 蓝量 100
李逍遥[江湖小虾米] : 攻击力 40, 防御力 50, 血量 40, 蓝量 10
李逍遥[武林高手] : 攻击力 90, 防御力 80, 血量 100, 蓝量 100
挑战boss失败:李逍遥[武林高手] : 攻击力 90, 防御力 80, 血量 0, 蓝量 0
恢复存档:李逍遥[武林高手] : 攻击力 90, 防御力 80, 血量 100, 蓝量 100
3.2. 内部类实现
还可以使用内部类的方式实现,这样就可以将备忘录对象完全隐藏在原发器对象内部。为了简单,这里我省略了管理者对象,代码如下:
public class OriginatorWithInnerClass {
private State state;
private InnerMemento innerMemento;
private final String name;
private String title;
public OriginatorWithInnerClass(String name) {
this.name = name;
}
public void createMemento() { (1)
this.innerMemento = new InnerMemento(this.state);
}
public void recover() { (2)
this.state = this.innerMemento.getState();
}
// 省略getter/setter......
@Override
public String toString() {
return this.name + "[" + this.title + "] : " + this.state.toString();
}
private class InnerMemento { (3)
private final State state;
InnerMemento(State state) {
this.state = state;
}
State getState() {
return state;
}
}
}
1 | 创建备忘录,保存在原发器对象自身中 |
2 | 恢复到上一个备忘录 |
3 | 内部类实现备忘录对象 |
此时,备忘录对象完全被封装到原发器,原发器创建备忘录并保存它。
3.3. 原型模式实现
前边的示例,都是将原发器对象的状态进行了保存。有时候,我们需要保存整个对象,即在某一时刻复制当前原发器对象,此时就是创建原发器对象的快照,我们很容易想到使用 原型模式 来实现。示例代码如下:
public class PrototypeOriginator implements Cloneable { (1)
private State state;
private final String name;
private String title;
public PrototypeOriginator(String name) {
this.name = name;
}
public PrototypeOriginator createMemento() {
return this.clone();
}
public void recover(PrototypeOriginator memento) {
this.setState(memento.getState());
this.setTitle(memento.getTitle());
}
// 省略getter/setter ...
@Override
public String toString() {
return this.name + "[" + this.title + "] : " + this.state.toString();
}
@Override
protected PrototypeOriginator clone() { (2)
PrototypeOriginator originator = new PrototypeOriginator(this.name);
originator.setState(this.getState());
originator.setTitle(this.getTitle());
return originator;
}
}
1 | 实现 Cloneable 接口已实现原型模式 |
2 | 实现 clone() 方法,注意这里必须使用 深拷贝 |
这种方式,原发器对象本身就是备忘录,其他与标准实现相同,不再贴代码了,有兴趣可以看文末源码。
4. 总结
当需要保存对象的状态,又不想暴露对象的实现细节,此时可以考虑使用原型模式,将备忘录和其保存的逻辑抽取出来,对象仅需暴露一个创建备忘录和一个恢复到备忘录的方法即可,便于扩展。但是,备忘录对象会保存原发器的部分甚至整个状态,会带来很大的开销,因此使用时需要权衡利弊。
本文示例代码见: github