行为型模式之撤销功能的实现(备忘录模式)
实现多次撤销
Sunny软件公司开发人员通过使用备忘录模式实现了中国象棋棋子的撤销操作,但是使用上述代码只能实现一次撤销,因为在负责人类中只定义一个备忘录对象来保存状态,后面保存的状态会将前一次保存的状态覆盖,但有时候用户需要撤销多步操作。如何实现多次撤销呢? 本节将提供一种多次撤销的解决方案,那就是在负责人类中定义一个集合来存储多个备忘录,每个备忘录负责保存一个历史状态,在撤销时可以对备忘录集合进行逆向遍历,回到一个指定的历史状态,而且还可以对备忘录集合进行正向遍历,实现重做(Redo)操作,即取消撤销,让对象状态得到恢复。
在图中,我们对负责人类MementoCaretaker进行了修改,在其中定义了一个ArrayList类型的集合对象来存储多个备忘录,其代码如下所示:
//象棋棋子类:原发器 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; } //get、set方法省略 //保存状态 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(); } }
//象棋棋子备忘录类:备忘录 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; } //get、set方法省略 }
//象棋棋子备忘录管理类:负责人 class MementoCaretaker { //定义一个集合来存储多个备忘录 private ArrayList mementolist = new ArrayList(); public ChessmanMemento getMemento(int i) { return (ChessmanMemento) mementolist.get(i); } public void setMemento(ChessmanMemento memento) { mementolist.add(memento); } }
编写如下客户端测试代码:
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列。
本实例只能实现最简单的Undo和Redo操作,并未考虑对象状态在操作过程中出现分支的情况。如果在撤销到某个历史状态之后,用户再修改对象状态,此后执行Undo操作时可能会发生对象状态错误,大家可以思考其产生原因。【注:可将对象状态的改变绘制成一张树状图进行分析。】
在实际开发中,可以使用链表或者堆栈来处理有分支的对象状态改变,大家可通过链表或者堆栈对上述实例进行改进。
备忘录模式总结
备忘录模式在很多软件的使用过程中普遍存在,但是在应用软件开发中,它的使用频率并不太高,因为现在很多基于窗体和浏览器的应用软件并没有提供撤销操作。如果需要为软件提供撤销功能,备忘录模式无疑是一种很好的解决方案。在一些字处理软件、图像编辑软件、数据库管理系统等软件中备忘录模式都得到了很好的应用。
备忘录模式的主要优点如下:
(1) 它提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原。
(2) 备忘录实现了对信息的封装,一个备忘录对象是一种原发器对象状态的表示,不会被其他代码所改动。备忘录保存了原发器的状态,采用列表、堆栈等集合来存储备忘录对象可以实现多次撤销操作。
备忘录模式的主要缺点如下:
资源消耗过大,如果需要保存的原发器类的成员变量太多,就不可避免需要占用大量的存储空间,每保存一次对象的状态都需要消耗一定的系统资源。
在以下情况下可以考虑使用备忘录模式:
(1) 保存一个对象在某一个时刻的全部状态或部分状态,这样以后需要时它能够恢复到先前的状态,实现撤销操作。
(2) 防止外界对象破坏一个对象历史状态的封装性,避免将对象历史状态的实现细节暴露给外界对象。