在软件使用过程中用户难免会出现一些误操作,例如不小心删除了某些文字或图片,对于这些误操作需要提供一种“后悔药”的机制,实现撤销操作,例如我们常用Ctrl + Z。
备忘录模式正式为了解决此类撤销问题而诞生的,它为软件提供了“后悔药”,通过使用备忘录模式可以让系统恢复到某一特定的历史状态。
定义
在不破会封装的前提下捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。
结构
Originator(原发器)
原发器是一个普通类,它通过创建一个备忘录来存储当前内部状态,也可以使用备忘录来恢复其内部状态,一般将系统中需要保存内部状态的类设计为原发器。
Memento(备忘录)
备忘录用于存储原发器的内部状态,根据原发器来决定保存哪些内部状态。备忘录的设计一般可以参考原发器的设计,根据实际需要确定备忘录类中的属性。需要注意的是,除了原发器本身与负责人类之外,备忘录对象不能直接提供其他类使用,原发器的设计在不同的编程语言中谁先机制会有所不同。
Caretaker(负责人)
负责人又称为管理者,它负责保存备忘录,但是不能对备忘录的内容进行操作或检查。在负责人类中可以存储一个或多个备忘录对象,它只负责存储对象,而不能修改对象,也无须知道对象的实现细节。
举例说明
象棋的悔棋功能。
1.原发器 - 象棋棋子类
// 原发器 - 象棋棋子
@Setter@Getter
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 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();
}
}
2.备忘录
// 备忘录 - 象棋棋子备忘录类
@Getter@Setter
public 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;
}
}
3.负责人/管理者
// 负责人 - 象棋棋子备忘录管理类
@Setter@Getter
public class MementoCaretaker {
private ChessmanMemento memento;
}
4.测试
public class Client {
public static void main(String[] args) {
// 创建负责人类 - 用于存储备忘录类
MementoCaretaker mementoCaretaker = new MementoCaretaker();
// 设置棋子初始位置及棋名
Chessman chessman = new Chessman("车", 1, 1);
display(chessman);
mementoCaretaker.setMemento(chessman.save()); // 保存状态
chessman.setY(4);
display(chessman);
mementoCaretaker.setMemento(chessman.save()); // 保存状态
chessman.setY(6);
display(chessman);
System.out.println("******悔棋******");
chessman.restore(mementoCaretaker.getMemento());
display(chessman);
}
public static void display(Chessman chessman) {
System.out.println("棋子 " + chessman.getLabel() + " 当前位置为(" +
chessman.getX() + "," + chessman.getY() + ")"
);
}
}
5.运行截图
本例中,只保存了上一次的棋子状态,因此只能悔棋一次。
实现多次撤销
在负责人类中定义一个集合来存储多个备忘录,每个备忘录负责保存一个历史状态,在撤销时可以对备忘录集合进行逆向遍历,回到一个指定的历史装填,而且还可以对备忘录集合进行正向遍历,实现重做/恢复操作,即取消撤销,让对象状态得到恢复。
1.修改负责人类
// 负责人 - 象棋棋子备忘录管理类
public class MementoCaretaker {
// 存储历史状态
private List<ChessmanMemento> mementolist = new ArrayList<ChessmanMemento>();
public ChessmanMemento getMemento(int i) {
return (ChessmanMemento) mementolist.get(i);
}
public void setMemento(ChessmanMemento chessmanMemento) {
mementolist.add(chessmanMemento);
}
}
2.修改客户端
public class Client {
// 索引 - 用来记录当前状态所在的位置
private static int index = -1;
private static MementoCaretaker mc = new MementoCaretaker();
public static void main(String[] args) {
Chessman chessman = new Chessman("车", 1, 1);
play(chessman);
chessman.setY(2);
play(chessman);
chessman.setY(3);
play(chessman);
undo(chessman, index);
undo(chessman, index);
redo(chessman, index);
redo(chessman, index);
}
// 下棋
public static void play(Chessman chessman) {
// 保存备忘录
mc.setMemento(chessman.save());
index++;
System.out.println("棋子 " + chessman.getLabel() + " 当前位置为(" +
chessman.getX() + "," + chessman.getY() + ")"
);
}
// 悔棋
public static void undo(Chessman chessman, int i) {
System.out.println("******悔棋******");
index --;
// 撤销到上一个备忘录
chessman.restore(mc.getMemento(i - 1));
System.out.println("棋子 " + chessman.getLabel() + " 当前位置为(" +
chessman.getX() + "," + chessman.getY() + ")"
);
}
// 撤销悔棋
public static void redo(Chessman chessman,int i) {
System.out.println("****撤销悔棋****");
index ++;
// 恢复到下一个备忘录
chessman.restore(mc.getMemento(i + 1));
System.out.println("棋子 " + chessman.getLabel() + " 当前位置为(" +
chessman.getX() + "," + chessman.getY() + ")"
);
}
}
3.运行截图
本例只实现了最简单的撤销和恢复操作,并未考虑对象状态在操作过程中出现分支的情况。如果在撤销到某个历史状态之后用户再修改对象状态,此后执行撤销操作时可能会发生对象状态错误。在实际开发中可以使用链表或者堆栈来处理由分支的对象状态改变。
优点
1.提供了一种状态恢复的实现机制,使得用户可以方便地回到某一特定的历史步骤。
2.实现了对信息的封装,一个备忘录对象是一种原发器对象状态的表示,不会被其他代码所改动。备忘录保存了原发器的状态,采用列表、堆栈等集合来存储备忘录对象可以实现多次撤销操作。
缺点
资源消耗过大,如果需要保存的原发器类的成员变量太多,就不可避免地需要占用大量的存储空间,每保存一次对象的状态都需要消耗一定的系统资源。