备忘录模式
备忘录模式概述
备忘录模式——软件中的“后悔药”——撤销(Undo)
- 通过使用备忘录模式可以让系统恢复到某一特定的历史状态
- 首先保存软件系统的历史状态,当用户需要取消错误操作并且返回到某个历史状态时,可以取出事先保存的历史状态来覆盖当前状态
模式定义
备忘录模式:在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样就可以在以后将对象恢复到原先保存的状态。
- 别名为标记(Token)模式
- 提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤
- 在很多软件所提供的撤销(Undo)操作中就使用了备忘录模式
备忘录模式结构
备忘录模式包含以下3个角色:
- Originator(原发器) 需要保存状态的对象
- Memento(备忘录) 由原发器创建,保存原发器的状态
- Caretaker(负责人) 知道并负责存储备忘录
备忘录模式实现
- 除了Originator类,不允许其他类来调用备忘录类Memento的构造函数与相关方法
- 如果允许其他类调用
setState()
等方法,将导致在备忘录中保存的历史状态发生改变,通过撤销操作所恢复的状态就不再是真实的历史状态,备忘录模式也就失去了本身的意义 - 理想的情况是只允许生成该备忘录的原发器访问备忘录的内部状态
Java语言实现
(1) 将Memento
类与Originator
类定义在同一个包(package)中来实现封装,使用默认可见性定义Memento
类,即保证其在包内可见
(2) 将备忘录类作为原发器类的内部类,使得只有原发器才可以访问备忘录中的数据
,其他对象都无法使用备忘录中的数据
实例学习
某软件公司要使用Java语言开发一款可以运行在Android平台的触摸式中国象棋软件。
由于考虑到有些用户是“菜鸟”,经常不小心走错棋;还有些用户因为不习惯使用手指在手机屏幕上拖动棋子,常常出现操作失误,因此该中国象棋软件要提供“悔棋”功能,在用户走错棋或操作失误后可恢复到前一个步骤。
为了实现“悔棋”功能,现使用备忘录模式来设计该中国象棋软件
备忘录类
该类用于存储棋子的当前状态,作为备忘录
//象棋棋子备忘录类:备忘录
class ChessmanMemento {
private String label;
private int x;
private int y;
public ChessmanMemento(String label,int x,int y) {
this.label = label;
this.x = x;
this.y = y;
}
public void setLabel(String label) {
this.label = label;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
public String getLabel() {
return (this.label);
}
public int getX() {
return (this.x);
}
public int getY() {
return (this.y);
}
}
原发器类
// 该类是原发器类,定义需要被保存的状态
采用保存状态的方式实现备忘录模式,还有一种方式,在下面介绍
//象棋棋子类:原发器
public class Chessman {
private String label;
private int x;
private int y;
public Chessman(String label,int x,int y) {
this.label = label;
this.x = x;
this.y = y;
}
public void setLabel(String label) {
this.label = label;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
public String getLabel() {
return (this.label);
}
public int getX() {
return (this.x);
}
public int getY() {
return (this.y);
}
//保存状态
public ChessmanMemento save() {
return new ChessmanMemento(this.label,this.x,this.y);
}
//恢复状态
public void restore(ChessmanMemento memento) {
this.label = memento.getLabel();
this.x = memento.getX();
this.y = memento.getY();
}
}
负责人
// 负责人类需要知道维护的备忘录列表,便于获取原发器的不同时刻的状态
// 它的职责只是保存状态
public class MementoCaretaker {
//定义一个集合来存储多个备忘录
private ArrayList<ChessmanMemento> mementolist = new ArrayList<ChessmanMemento>();
public ChessmanMemento getMemento(int i) {
return (ChessmanMemento)mementolist.get(i);
}
public void setMemento(ChessmanMemento memento) {
mementolist.add(memento);
}
}
调用类
public class Client {
private static int index = -1; //定义一个索引来记录当前状态所在位置
private static MementoCaretaker mc = new MementoCaretaker();
public static void main(String args[]) {
Chessman chess = new Chessman("车",1,1);
play(chess);
chess.setY(4);
play(chess);
chess.setX(5);
play(chess);
undo(chess,index);
undo(chess,index);
redo(chess,index);
redo(chess,index);
}
//下棋
public static void play(Chessman chess) {
mc.setMemento(chess.save()); //保存备忘录
index ++;
System.out.println("棋子" + chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");
}
//悔棋
public static void undo(Chessman chess,int i) {
System.out.println("******悔棋******");
index --;
chess.restore(mc.getMemento(i-1)); //撤销到上一个备忘录
System.out.println("棋子" + chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");
}
//撤销悔棋
public static void redo(Chessman chess,int i) {
System.out.println("******撤销悔棋******");
index ++;
chess.restore(mc.getMemento(i+1)); //恢复到下一个备忘录
System.out.println("棋子" + chess.getLabel() + "当前位置为:" + "第" + chess.getX() + "行" + "第" + chess.getY() + "列。");
}
}
结果及分析
棋子车当前位置为:第1行第1列。
棋子车当前位置为:第1行第4列。
棋子车当前位置为:第5行第4列。
******悔棋******
棋子车当前位置为:第1行第4列。
******悔棋******
棋子车当前位置为:第1行第1列。
******撤销悔棋******
棋子车当前位置为:第1行第4列。
******撤销悔棋******
棋子车当前位置为:第5行第4列。
实现符合单一职责原则:
- 原发类只关心内部状态
- Memento只存储状态
- Caretaker管理备忘录
- Client实现撤销和前进行为
实例类介绍
(1) Chessman:象棋棋子类,充当原发器
(2) ChessmanMemento:象棋棋子备忘录类,充当备忘录
(3) MementoCaretaker:象棋棋子备忘录管理类,充当负责人
(4) Client:客户端测试类
备忘录类定义位置
定义在原发器的内部
将备忘录作为内部类
内部定义备忘录的优点:
- 设计和实现比较简单。
- 性能高效,仅需要保存一个状态。
缺点:
- 只能保存单次记录,不能管理多个备忘录。
- 职责不清晰,原发器对象管理其自身状态。
- 如果需要共享备忘录,则实现复杂。
定义在外部
比如我们的实例代码就是写在原发器外面,原发器里只需要定义备忘录创建和备忘录读取的方法(因为我们恢复状态,恢复的是原发器的状态,所以需要从备忘录读取状态赋值给当前原发器)。
外部定义备忘录的优点:
- 更灵活,可以保存多个状态。可以使用集合等结构来管理多个备忘录对象。
- 可以伴随其它对象或结构一起管理。
- 可以在多个原发器对象之间共享备忘录。
- 符合单一职责原则,将状态保存和管理分离。
缺点:
- 设计和实现复杂度较高,需要额外的管理类。
- 会有额外的性能开销,需要创建和管理多个备忘录对象。
模式优缺点
模式优点
- 提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤
- 实现了对信息的封装,一个备忘录对象是一种原发器对象状态的表示,不会被其他代码所改动
模式缺点
- 资源消耗过大。如果需要保存的原发器类的成员变量太多,就不可避免地需要占用大量的存储空间,每保存一次对象的状态都需要消耗一定的系统资源
适用环境
- 保存一个对象在某一个时刻的全部状态或部分状态,这样以后需要时能够恢复到先前的状态,实现撤销操作
- 防止外界对象破坏一个对象历史状态的封装性,避免将对象历史状态的实现细节暴露给外界对象