备忘录模式:定义、结构、实现、优缺点、应用场景
大家好,今天我们来聊聊备忘录模式。说实话,这个模式在我刚入行那会儿,总觉得它有点「鸡肋」——不就是保存个状态吗?直到有一次,我在一个文本编辑器项目里,被用户吐槽「撤销功能怎么一撤销就崩了」……嗯,从那以后,我再也不敢小看它了。
什么是备忘录模式?
备忘录模式,说白了就是在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将对象恢复到原先保存的状态。
你想想看,我们平时用的「撤销」功能,背后就是备忘录模式在撑腰。它把每一步操作前的状态拍个「快照」,存起来。需要回退时,直接把快照拿出来恢复就行。
模式结构
备忘录模式有三个核心角色,我习惯把它们叫做「三剑客」:
| 角色 | 名称 | 职责 |
|---|---|---|
| 发起人 | Originator | 需要保存状态的对象,负责创建备忘录和恢复状态 |
| 备忘录 | Memento | 存储发起人内部状态的对象,对外不可修改 |
| 管理者 | Caretaker | 负责保存备忘录,但不能对备忘录内容进行操作 |
这里有个关键点:管理者不能碰备忘录里的数据。我曾经见过一个项目,管理者直接修改了备忘录的内容,结果整个撤销链全乱了。所以,备忘录对外要「只读」,这是底线。
核心逻辑图
下面这张图,是我自己画的结构示意,你看一眼就能明白它们怎么配合:
代码实现
咱们直接上代码。我以一个文本编辑器为例,演示备忘录模式的完整实现:
// 1. 备忘录类 —— 保存状态,对外只读
class TextMemento {
private String content;
private int cursorPosition;
public TextMemento(String content, int cursorPosition) {
this.content = content;
this.cursorPosition = cursorPosition;
}
// 只提供 getter,不提供 setter
public String getContent() {
return content;
}
public int getCursorPosition() {
return cursorPosition;
}
}
// 2. 发起人类 —— 文本编辑器
class TextEditor {
private String content = "";
private int cursorPosition = 0;
public void write(String text) {
this.content += text;
this.cursorPosition = this.content.length();
}
public void delete(int length) {
if (length > content.length()) {
length = content.length();
}
this.content = content.substring(0, content.length() - length);
this.cursorPosition = this.content.length();
}
// 创建备忘录
public TextMemento createMemento() {
return new TextMemento(content, cursorPosition);
}
// 恢复状态
public void restore(TextMemento memento) {
this.content = memento.getContent();
this.cursorPosition = memento.getCursorPosition();
}
public void showContent() {
System.out.println("内容: " + content + " | 光标位置: " + cursorPosition);
}
}
// 3. 管理者类 —— 管理历史记录
class HistoryManager {
private List<TextMemento> mementos = new ArrayList<>();
public void save(TextMemento memento) {
mementos.add(memento);
}
public TextMemento get(int index) {
if (index < 0 || index >= mementos.size()) {
return null;
}
return mementos.get(index);
}
public TextMemento undo() {
if (mementos.isEmpty()) {
return null;
}
// 移除当前状态,返回上一个状态
mementos.remove(mementos.size() - 1);
if (mementos.isEmpty()) {
return null;
}
return mementos.get(mementos.size() - 1);
}
}
// 4. 使用示例
public class Demo {
public static void main(String[] args) {
TextEditor editor = new TextEditor();
HistoryManager history = new HistoryManager();
// 第一次写入
editor.write("Hello");
history.save(editor.createMemento());
editor.showContent(); // Hello
// 第二次写入
editor.write(" World");
history.save(editor.createMemento());
editor.showContent(); // Hello World
// 撤销一次
TextMemento previous = history.undo();
if (previous != null) {
editor.restore(previous);
}
editor.showContent(); // Hello
}
}
优缺点分析
任何模式都有两面性,备忘录模式也不例外。我根据自己的实战经验,给你列个表:
| 优点 | 缺点 |
|---|---|
| 1. 保持封装边界,不暴露内部细节 | 1. 如果状态数据量大,会消耗大量内存 |
| 2. 简化发起人职责,状态管理分离 | 2. 频繁创建备忘录可能影响性能 |
| 3. 支持撤销、重做等操作 | 3. 管理者需要管理备忘录生命周期 |
| 4. 实现简单,易于理解 | 4. 某些语言中,备忘录可能被外部修改 |
⚠️ 避坑指南
我曾经在一个游戏项目中,把整个游戏场景的状态都存到备忘录里。结果每次保存都产生几十MB的数据,内存直接爆了。后来改用「增量快照」——只记录变化的部分,才解决问题。
所以,如果状态数据很大,别一股脑全存。可以考虑只存差异,或者限制历史记录的数量。
应用场景
备忘录模式在实际项目中,最常见的几个场景:
- 文本编辑器的撤销/重做 —— 这是最经典的应用,几乎每个编辑器都在用
- 游戏存档 —— 保存玩家进度,随时可以读档
- 数据库事务的回滚 —— 在提交前保存快照,出错时回滚
- 配置管理 —— 修改配置前保存旧值,方便回退
- 表单填写 —— 用户填到一半,临时保存草稿
💡 我的建议
如果你在做一个需要「后悔药」的功能,备忘录模式就是你的首选。但要注意:别把所有鸡蛋放在一个篮子里。我习惯在备忘录里只保存「核心状态」,那些可以重新计算的数据,就别存了。
总结
备忘录模式,说白了就是给对象拍「快照」。它让状态管理变得清晰,又不会破坏封装。但使用时一定要权衡内存开销,别让「快照」变成「负担」。
嗯,关于备忘录模式,我就讲这么多。记住它的核心思想:保存状态,随时回退。下次你写撤销功能时,不妨试试这个模式。