文章目录
- 简介
- 问题
- 解决
- 代码
- 关键实现要点
- 功能扩展方向
- 总结
简介
备忘录是一种行为设计模式, 允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。
问题
假如你正在开发一款文字编辑器应用。你想加入撤销功能。你可以采用直接的方式来实现: 程序在执行任何操作前会记录所有的对象状态,并把它们保存下来。当用戶需要撤销某个操作的时候,程序会从历史记录里获取最近的快照,然后使用它来恢复所有对象的状态。
让我们来考虑一下这些状态快照。 首先, 到底该怎么生成一个快照呢?你很可能会需要遍历对象的所有成员变量并且把他们的数值复制保存,但这也只有在对象对它的内容没有严格访问权限限制的情况 下,你才能用这种方式。 不过,绝大部分对象都会用私有成员变量来存储重要数据,所以这种方案可能比较麻烦。
那我们假设对象都会公开它的所有状态。尽管这能解决当前问题,但如果之后你想添加或删除一些成员变量,你可能需要更改负责复制这些对象状态的类。
还有更多问题。比如,让我们来考虑下编辑器(Editor)状态的实际“快照”,它需要包含哪些数据? 首先至少必须包含实际的文本、光标坐标和当前滚动条位置等。你需要收集这些数据并把它们放在特定容器中,才能生成快照。
你很可能会把大量的容器对象存储在历史记录列表中。这些容器大概率是由同一个类生成的对象。这个类中几乎没有方法,但有许多与编辑器状态一一对应的成员变量。为了让其他对象能保存或读取快照,你需要将快照的成员变量设为公有,它会暴露所有编辑器状态。其他类会对快照类的每个小改动产生依赖,除非这些改动仅存在于私有成员变量或方法中。
我们似乎走进了一条死胡同:要么会暴露类的所有内部细节而使它过于脆弱;要么会限制对其状态的访问权限而无法生成快照。那么,我们还有其他方式来实现“撤销”功能吗?
解决
我们刚才遇到的所有问题都是封装“破损”造成的。一些对象在执行某些行为时需要获取数据,所以它们侵入了其他对象的私有空间, 而不是让这些对象自己完成工作。
备忘录模式把创建状态快照(Snapshot)的工作委派给实际状态的拥有者即原发器(Originator)对象。这样其他对象就不再需要从“外部”复制编辑器状态了,编辑器类拥有状态的完全访问权,因此可以自行生成快照。
该模式建议把对象状态的副本存储在一个名为备忘录(Memento)的特殊对象里。除了负责创建备忘录的对象外,任何对象都不能访问备忘录的内容。其他对象必须使用受限接口跟备忘录进行交互,它们可以获取快照的元数据(创建时间和操作名称等),但不能获取快照中原始对象的状态。
这种限制策略允许你把备忘录保存在被称为负责人(Caretakers )的对象里。由于负责人只通过受限接口和备忘录互动,所以它不能修改存储在备忘录内部的状态。同时,原发器拥有对备忘录所有成员的访问权限, 从而能随时恢复自己以前的状态。
在文字编辑器的示例中,我们可以创建一个独立的历史(History) 类作为负责人。编辑器每次执行操作前,存储在负责人中的备忘录栈都会生⻓。你甚至可以在应用的 UI 中渲染这个栈,为用戶显示所有的操作历史。
当用戶触发撤销操作时,历史类会从栈里取回最近的备忘录,并把它传递给编辑器来请求进行回滚。由于编辑器拥有对备忘录的完全访问权限, 它就可以从备忘录中获取数值,用来替换自身的状态。
代码
// Originator:文本编辑器核心类
class TextEditor {private String content; // 当前编辑内容private int cursorPosition; // 光标位置public void type(String words) {content = (content == null) ? words : content.substring(0, cursorPosition) + words + content.substring(cursorPosition);cursorPosition += words.length();}public void setCursor(int position) { cursorPosition = Math.min(position, content.length());}// 创建备忘录public EditorMemento createMemento() {return new EditorMemento(content, cursorPosition); }// 从备忘录恢复public void restoreFromMemento(EditorMemento memento) {this.content = memento.getSavedContent();this.cursorPosition = memento.getSavedCursorPosition();}// Memento作为内部类(封装状态细节)public static class EditorMemento {private final String content;private final int cursorPosition;private EditorMemento(String content, int cursor) { // 仅Originator可创建实例this.content = content;this.cursorPosition = cursor;}private String getSavedContent() { return content; } // 包权限访问private int getSavedCursorPosition() { return cursorPosition; }}
}// Caretaker:历史记录管理器
class History {private final Stack<TextEditor.EditorMemento> mementos = new Stack<>();public void save(TextEditor.EditorMemento memento) {mementos.push(memento); // 保存状态快照[^2]}public TextEditor.EditorMemento undo() {if(mementos.size() > 1) {mementos.pop(); // 移除当前状态return mementos.peek(); // 返回前一次状态}return mementos.peek();}
}// Client使用示例
public class EditorClient {public static void main(String[] args) {TextEditor editor = new TextEditor();History history = new History();// 第一次输入并保存editor.type("Hello");history.save(editor.createMemento()); // 第二次输入并保存editor.type(" World");history.save(editor.createMemento());// 执行撤销操作(状态退回第一次保存点)editor.restoreFromMemento(history.undo()); System.out.println(editor.getContent()); // 输出 "Hello"}
}
关键实现要点
- 严格封装:Memento的构造器和访问方法仅对Originator可见
- 状态融合:Originator内部维护状态恢复逻辑
- 历史管理:Caretaker用栈结构实现撤销机制
- 数据安全:Memento对象不可变,确保快照完整性
功能扩展方向
- 增加保存选项(字体/排版)到Memento
- 支持重做功能(双栈实现)
- 持久化备忘录(保存到文件)
总结
- 原发器(Originator)类可以生成自身状态的快照,也可以在需要时通过快照恢复自身状态。
- 备忘录(Memento)是原发器状态快照的值对象(value object) ,通常做法是把备忘录设为不可变的,通过构造函数一次性传递数据。
- 负责人(Caretaker)只知道“何时”和“为何”捕捉原发器的状态, 以及何时恢复状态。 负责人通过保存备忘录栈来记录原发器的历史状态。当原发器需要回滚时,负责人会从栈里获取最顶部的备忘录,并把它传递给原发器的恢复(restoration)方法。
- 在这个实现方法中,备忘录类会被嵌套在原发器中。这样原发器就可以访问备忘录的成员变量和方法, 就算这些方法被声明为私有;另一方面,负责人对于备忘录的成员变量和方法的访问权限非常有限: 它们只能在栈中保存备忘录, 但不能修改它的状态。