系列定位:软考软件设计师 / 系统架构设计师 --- 行为型模式专题第 6 讲
考察分值:上午题 1-2 分,下午题常作为代码填空或类图识别出现
难度等级:⭐⭐⭐☆☆(结构清晰,Originator、Memento、Caretaker 三者的封装关系是理解重点)
一、考纲定位与模式定义
1.1 考纲要求
备忘录模式在软考中属于 行为型模式 的基础内容。考察形式包括:
-
上午选择题:判断场景描述所属模式;识别备忘录模式与原型模式、命令模式的区别;判断 Originator、Memento、Caretaker 三个角色;识别 "在不破坏封装性的前提下捕获和恢复对象内部状态" 的设计意图
-
下午设计题 :补全
Originator中createMemento()和restoreMemento()的代码;补全Memento中保存状态字段的代码;补全Caretaker中保存和获取备忘录的代码;识别类图中 Memento 的封装性设计(Caretaker 不能访问 Memento 内部状态)
1.2 模式定义
备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
核心意图 :当一个对象需要 保存和恢复其内部状态 (如撤销操作、游戏存档、事务回滚),且状态信息需要 封装(不希望外部直接访问对象的私有属性)时,如果直接暴露对象的内部状态给外部,会破坏封装性;如果对象自己管理历史状态,会导致类职责过重。
备忘录模式引入一个 备忘录对象(Memento) 来封装状态,由 原发器(Originator) 创建和恢复备忘录,由 负责人(Caretaker) 负责保存备忘录。外部(Caretaker)只负责"保管"备忘录,不能访问也不能修改备忘录的内部状态。
通俗理解:
你玩 RPG 游戏,打到 BOSS 门前想先存个档。存档文件(Memento)里记录了你的等级、血量、装备、位置等状态。这个存档文件由游戏系统(Originator)生成,由你自己(Caretaker)保管在硬盘里。你可以随时读取这个存档恢复游戏状态,但 你无法用记事本打开存档文件去修改自己的血量(Memento 对 Caretaker 是封装的)。如果存档文件随便就能改,那游戏就没意思了。
二、UML 类图与角色划分
┌─────────────────────────────────────────────────────────┐
│ Client │
│ + main() │
│ ├── Originator originator = new Originator() │
│ ├── originator.setState("状态A") │
│ ├── Memento memento = originator.createMemento() │
│ ├── Caretaker caretaker = new Caretaker() │
│ ├── caretaker.saveMemento(memento) │
│ ├── originator.setState("状态B") // 修改状态 │
│ ├── Memento restored = caretaker.getMemento() │
│ └── originator.restoreMemento(restored) // 恢复状态A │
└───────────────────────┬─────────────────────────────────┘
│ uses
▼
┌─────────────────────────────────────────────────────────┐
│ Originator │
│ ── 原发器:创建备忘录、使用备忘录恢复状态 ── │
│ - state: String ◄── 需要保存的内部状态 │
│ + setState(String s) │
│ + getState(): String │
│ + createMemento(): Memento { │
│ return new Memento(state); // 创建备忘录 │
│ } │
│ + restoreMemento(Memento m) { │
│ this.state = m.getState(); // 从备忘录恢复 │
│ } │
└───────────────────────┬─────────────────────────────────┘
│ creates / uses
▼
┌─────────────────────────────────────────────────────────┐
│ Memento │
│ ── 备忘录:封装 Originator 的内部状态 ── │
│ - state: String ◄── 私有状态,Caretaker 不可访问 │
│ + Memento(String state) { this.state = state; } │
│ + getState(): String ◄── 只允许 Originator 访问 │
│ // 通常包级私有或内部类,Caretaker 无法调用 │
└───────────────────────┬─────────────────────────────────┘
│ saves / manages
▼
┌─────────────────────────────────────────────────────────┐
│ Caretaker │
│ ── 负责人:保存备忘录,不访问备忘录内部状态 ── │
│ - mementoList: List<Memento> │
│ + saveMemento(Memento m) { │
│ mementoList.add(m); // 只保存,不打开看内容 │
│ } │
│ + getMemento(): Memento { │
│ return mementoList.get(last); // 只传递,不修改 │
│ } │
└─────────────────────────────────────────────────────────┘
| 角色 | 职责 | 软考填空关键词 |
|---|---|---|
| Originator(原发器) | 需要保存状态的对象。提供 createMemento() 创建备忘录,提供 restoreMemento() 从备忘录恢复状态 |
createMemento() + restoreMemento(Memento m) + 内部状态字段 |
| Memento(备忘录) | 封装 Originator 的内部状态。通常只提供 getter(或包级私有访问),不允许 Caretaker 访问或修改 其内部状态 | 私有状态字段 + 构造器 + getState()(受限访问) |
| Caretaker(负责人) | 负责保存和传递备忘录,不访问、不修改 备忘录的内部状态。可以管理多个备忘录(如撤销栈、历史列表) | List<Memento> + saveMemento() + getMemento()(只存不读) |
| Client(客户端) | 创建 Originator、触发状态保存、将备忘录交给 Caretaker 保管、从 Caretaker 取回备忘录恢复状态 | new Originator() + createMemento() + caretaker.saveMemento() + restoreMemento() |
类图识别要点 :软考类图中,备忘录模式的核心特征是 Originator 创建 Memento,Caretaker 持有 Memento 但只保存不访问,Originator 从 Memento 恢复状态。如果类图里有一个类负责创建和恢复备忘录,另一个类只负责保管备忘录(没有访问备忘录内部的方法)→ 备忘录模式。
三、场景一:文本编辑器撤销(最经典的备忘录模式)
业务背景:文本编辑器需要支持撤销(Undo)功能。编辑器的当前状态包括:文本内容和光标位置。用户每输入一段文字后,可以撤销回到之前的状态。要求保存状态时不能破坏编辑器类的封装性。
说明:这是备忘录模式最经典的例子,也是软考下午题最爱考的 "状态保存与恢复" 场景。
3.1 代码实现
java
// Memento:编辑器状态备忘录
// 注意:Memento 通常设计为不可变对象,状态通过构造器传入,只提供 getter
public class EditorMemento {
private final String content; // 文本内容
private final int cursorPosition; // 光标位置
// 构造器:由 Originator 调用,传入状态
public EditorMemento(String content, int cursorPosition) {
this.content = content;
this.cursorPosition = cursorPosition;
}
// 只允许 Originator 访问状态(软考中通常默认 Originator 可以访问)
public String getContent() {
return content;
}
public int getCursorPosition() {
return cursorPosition;
}
}
// Originator:文本编辑器
public class TextEditor {
private String content = "";
private int cursorPosition = 0;
public void setContent(String content) {
this.content = content;
this.cursorPosition = content.length();
System.out.println("【编辑器】内容更新为: \"" + content + "\",光标位置: " + cursorPosition);
}
public void append(String text) {
this.content += text;
this.cursorPosition = content.length();
System.out.println("【编辑器】追加: \"" + text + "\",当前内容: \"" + content + "\"");
}
// 创建备忘录:保存当前状态
public EditorMemento createMemento() {
System.out.println("【编辑器】创建备忘录,保存状态: \"" + content + "\"");
return new EditorMemento(content, cursorPosition);
}
// 从备忘录恢复状态
public void restoreMemento(EditorMemento memento) {
this.content = memento.getContent();
this.cursorPosition = memento.getCursorPosition();
System.out.println("【编辑器】恢复状态: \"" + content + "\",光标位置: " + cursorPosition);
}
public void display() {
System.out.println("【编辑器】当前内容: \"" + content + "\",光标: " + cursorPosition);
}
}
// Caretaker:撤销历史管理器
// 关键:Caretaker 只负责保存和传递备忘录,绝不访问备忘录内部状态
public class HistoryManager {
private Stack<EditorMemento> history = new Stack<>();
// 保存备忘录
public void save(EditorMemento memento) {
history.push(memento);
System.out.println("【历史管理器】保存备忘录,历史栈深度: " + history.size());
}
// 获取上一个备忘录(用于撤销)
public EditorMemento undo() {
if (history.isEmpty()) {
System.out.println("【历史管理器】没有可撤销的状态");
return null;
}
EditorMemento memento = history.pop();
System.out.println("【历史管理器】取出备忘录用于撤销,剩余: " + history.size());
return memento;
}
// 注意:Caretaker 没有任何方法可以读取 memento.getContent() 或修改 memento 的状态
// 它只负责"保管"和"传递"
}
// Client:客户端
public class Client {
public static void main(String[] args) {
TextEditor editor = new TextEditor();
HistoryManager history = new HistoryManager();
// 初始输入
editor.setContent("Hello");
history.save(editor.createMemento()); // 保存状态1
// 继续输入
editor.append(" World");
history.save(editor.createMemento()); // 保存状态2
// 再输入
editor.append("!");
editor.display(); // 当前: "Hello World!"
System.out.println("---");
// 第一次撤销:恢复到 "Hello World"
EditorMemento memento1 = history.undo();
if (memento1 != null) {
editor.restoreMemento(memento1);
}
editor.display();
System.out.println("---");
// 第二次撤销:恢复到 "Hello"
EditorMemento memento2 = history.undo();
if (memento2 != null) {
editor.restoreMemento(memento2);
}
editor.display();
}
}
关键体会:
-
TextEditor(Originator)是唯一能创建和读取EditorMemento的类。它知道自己的状态结构,所以由它来生成备忘录和从备忘录恢复。 -
HistoryManager(Caretaker)只负责把备忘录放进栈里保存,或者从栈里取出来。它 永远不知道 备忘录里存的是什么内容,也 不能修改 备忘录里的内容。这就是封装性。 -
如果没有备忘录模式,要实现撤销,可能需要在
TextEditor里维护一个历史状态列表,或者把内部状态暴露给外部管理,都会破坏封装。 -
这就是备忘录模式的 "窄接口"设计:Caretaker 只能看到 Memento 对象本身,看不到内部状态;只有 Originator 能看到 Memento 的内部(宽接口)。
四、场景二:游戏存档系统(实际开发场景)
业务背景:RPG 游戏中有玩家角色(Player),状态包括:等级、生命值、魔法值、当前地图位置、背包物品。玩家可以在任意时刻存档,之后可以读档恢复。存档文件由游戏系统管理,玩家不能直接修改存档数据。要求存档时不能暴露玩家类的内部实现细节。
说明:这是实际开发中备忘录模式的典型应用,也是软考下午题中 "状态快照" 场景的代表。
4.1 代码实现
java
// Memento:游戏存档
// 设计为不可变对象,防止外部修改
public class GameSave {
private final int level;
private final int hp;
private final int mp;
private final String map;
private final List<String> inventory;
public GameSave(int level, int hp, int mp, String map, List<String> inventory) {
this.level = level;
this.hp = hp;
this.mp = mp;
this.map = map;
// 深拷贝,防止外部修改影响存档
this.inventory = new ArrayList<>(inventory);
}
// 只允许 Originator 访问
public int getLevel() { return level; }
public int getHp() { return hp; }
public int getMp() { return mp; }
public String getMap() { return map; }
public List<String> getInventory() { return new ArrayList<>(inventory); }
}
// Originator:玩家角色
public class Player {
private int level = 1;
private int hp = 100;
private int mp = 50;
private String currentMap = "新手村";
private List<String> inventory = new ArrayList<>();
public void levelUp() {
level++;
hp += 20;
mp += 10;
System.out.println("【玩家】升级!等级: " + level + ",HP: " + hp + ",MP: " + mp);
}
public void takeDamage(int damage) {
hp -= damage;
if (hp < 0) hp = 0;
System.out.println("【玩家】受到 " + damage + " 点伤害,剩余 HP: " + hp);
}
public void pickItem(String item) {
inventory.add(item);
System.out.println("【玩家】获得物品: " + item + ",背包: " + inventory);
}
public void enterMap(String map) {
this.currentMap = map;
System.out.println("【玩家】进入地图: " + map);
}
// 创建存档
public GameSave createSave() {
System.out.println("【玩家】创建存档...");
return new GameSave(level, hp, mp, currentMap, inventory);
}
// 读档恢复
public void loadSave(GameSave save) {
this.level = save.getLevel();
this.hp = save.getHp();
this.mp = save.getMp();
this.currentMap = save.getMap();
this.inventory = save.getInventory();
System.out.println("【玩家】读档成功!等级: " + level + ",HP: " + hp +
",地图: " + currentMap + ",背包: " + inventory);
}
public void displayStatus() {
System.out.println("【玩家状态】等级: " + level + ",HP: " + hp +
",MP: " + mp + ",地图: " + currentMap + ",背包: " + inventory);
}
}
// Caretaker:存档管理器
// 只负责保存和提供存档,绝不访问存档内容
public class SaveManager {
private Map<String, GameSave> saves = new HashMap<>();
public void saveGame(String saveName, GameSave save) {
saves.put(saveName, save);
System.out.println("【存档管理器】存档 \"" + saveName + "\" 已保存");
}
public GameSave loadGame(String saveName) {
GameSave save = saves.get(saveName);
if (save != null) {
System.out.println("【存档管理器】读取存档 \"" + saveName + "\"");
} else {
System.out.println("【存档管理器】存档 \"" + saveName + "\" 不存在");
}
return save;
}
public void listSaves() {
System.out.println("【存档管理器】已有存档: " + saves.keySet());
}
}
// Client
public class Client {
public static void main(String[] args) {
Player player = new Player();
SaveManager saveManager = new SaveManager();
// 新手村发育
player.pickItem("木剑");
player.levelUp();
saveManager.saveGame("新手村存档", player.createSave());
// 进入地下城,挨打
player.enterMap("地下城一层");
player.takeDamage(30);
player.pickItem("恢复药水");
player.displayStatus();
System.out.println("---");
// 打不过,读档回到新手村
GameSave save = saveManager.loadGame("新手村存档");
if (save != null) {
player.loadSave(save);
}
player.displayStatus();
}
}
五、场景三:数据库事务回滚(状态恢复场景)
业务背景:数据库连接需要在执行一系列 SQL 操作前保存事务前的状态(如数据快照),如果执行过程中出错,可以回滚到之前的状态。使用备忘录模式保存事务开始前的数据库状态,事务失败时由事务管理器负责恢复。
说明:这是备忘录模式在状态恢复场景中的典型应用,也是软考上午题中 "事务回滚" 场景的代表。
5.1 代码实现
java
// Memento:数据库状态快照
public class DatabaseSnapshot {
private final Map<String, String> data; // 模拟表数据
public DatabaseSnapshot(Map<String, String> data) {
// 深拷贝,确保快照不可变
this.data = new HashMap<>(data);
}
public Map<String, String> getData() {
return new HashMap<>(data);
}
}
// Originator:数据库连接
public class DatabaseConnection {
private Map<String, String> tableData = new HashMap<>();
public void insert(String key, String value) {
tableData.put(key, value);
System.out.println("【数据库】插入: " + key + " = " + value);
}
public void delete(String key) {
tableData.remove(key);
System.out.println("【数据库】删除: " + key);
}
public void update(String key, String value) {
tableData.put(key, value);
System.out.println("【数据库】更新: " + key + " = " + value);
}
public void display() {
System.out.println("【数据库】当前数据: " + tableData);
}
// 创建快照
public DatabaseSnapshot createSnapshot() {
System.out.println("【数据库】创建事务快照");
return new DatabaseSnapshot(tableData);
}
// 恢复快照
public void rollback(DatabaseSnapshot snapshot) {
this.tableData = snapshot.getData();
System.out.println("【数据库】事务回滚,数据恢复到: " + tableData);
}
}
// Caretaker:事务管理器
public class TransactionManager {
private DatabaseSnapshot snapshot;
private boolean inTransaction = false;
public void beginTransaction(DatabaseConnection connection) {
snapshot = connection.createSnapshot();
inTransaction = true;
System.out.println("【事务管理器】事务开始,已保存快照");
}
public void commit() {
inTransaction = false;
snapshot = null;
System.out.println("【事务管理器】事务提交,快照清除");
}
public void rollback(DatabaseConnection connection) {
if (inTransaction && snapshot != null) {
connection.rollback(snapshot);
inTransaction = false;
snapshot = null;
} else {
System.out.println("【事务管理器】没有可回滚的事务");
}
}
}
// Client
public class Client {
public static void main(String[] args) {
DatabaseConnection db = new DatabaseConnection();
TransactionManager tx = new TransactionManager();
// 初始数据
db.insert("user1", "Alice");
db.insert("user2", "Bob");
db.display();
System.out.println("---");
// 开始事务
tx.beginTransaction(db);
// 事务内操作
db.update("user1", "Alice Smith");
db.delete("user2");
db.insert("user3", "Charlie");
db.display();
System.out.println("---");
// 模拟出错,回滚
tx.rollback(db);
db.display();
System.out.println("---");
// 再次事务,成功提交
tx.beginTransaction(db);
db.update("user1", "Alice Updated");
tx.commit();
db.display();
}
}
六、三种场景对比与演进思路
| 维度 | 场景一:文本编辑器 | 场景二:游戏存档 | 场景三:数据库事务 |
|---|---|---|---|
| Originator | TextEditor(编辑器) |
Player(玩家角色) |
DatabaseConnection(数据库连接) |
| Memento | EditorMemento(文本+光标) |
GameSave(等级+HP+MP+地图+背包) |
DatabaseSnapshot(表数据快照) |
| Caretaker | HistoryManager(撤销栈) |
SaveManager(存档映射表) |
TransactionManager(事务管理器) |
| 保存触发 | 用户主动保存(每次输入后) | 玩家主动存档 | 事务开始时自动保存 |
| 恢复触发 | 用户撤销(Ctrl+Z) | 玩家读档 | 事务回滚(出错时) |
| 多状态管理 | Stack<Memento>(支持多级撤销) |
Map<String, Memento>(多存档槽) |
单个 Memento(事务单点) |
| 封装性 | Caretaker 不访问 content/cursor | Caretaker 不访问 level/hp/inventory | Caretaker 不访问 tableData |
| 软考考法 | 下午大题(补全撤销栈逻辑) | 下午大题(补全存档管理) | 上午选择题(识别事务回滚) |
七、备忘录模式 vs 原型模式 vs 命令模式:核心对比
| 对比项 | 备忘录模式 | 原型模式 | 命令模式 |
|---|---|---|---|
| 模式分类 | 行为型 | 创建型 | 行为型 |
| 核心思想 | 保存对象状态 用于后续恢复,不破坏封装 | 复制对象 用于创建新对象 | 封装请求 为对象,支持撤销/重做 |
| 状态用途 | 保存后 恢复 到原对象 | 保存后用于 创建 新对象 | 保存后用于 执行/撤销 操作 |
| 封装性 | 强(Caretaker 不能访问 Memento 内部) | 弱(通常需要暴露内部状态用于克隆) | 中(Receiver 状态可能暴露给 Command) |
| 角色关系 | Originator ↔ Memento(创建/恢复),Caretaker → Memento(保管) | Prototype → Client(克隆),ConcretePrototype 自己克隆自己 | Invoker → Command → Receiver(解耦请求与执行) |
| 撤销机制 | 直接恢复对象状态 | 无 | 通过 Command 的 undo() 方法反向操作 |
| 典型应用 | 撤销/重做、游戏存档、事务回滚 | 对象克隆、避免重复创建 | 菜单命令、宏录制、事务管理 |
| 软考判断 | 看到 "保存状态恢复"、"不破坏封装"、"快照" → 备忘录 | 看到 "克隆对象"、"复制自身"、"原型实例" → 原型 | 看到 "封装请求"、"命令队列"、"撤销操作" → 命令 |
记忆口诀:备忘录是 "游戏存档"(保存状态用于恢复),原型是 "复印机"(复制对象用于创建),命令是 "遥控器"(封装请求用于执行/撤销)。
八、软考高频考点与易混淆辨析
8.1 高频考点
| 考点 | 内容 |
|---|---|
| 模式分类 | 行为型模式(GoF 23 正式成员) |
| 核心三角色 | Originator(原发器)、Memento(备忘录)、Caretaker(负责人) |
| 核心思想 | 在不破坏封装性的前提下捕获对象内部状态,并在对象之外保存,以便后续恢复 |
| 封装性 | Caretaker 不能访问 Memento 的内部状态,只能保存和传递 Memento 对象 |
| Originator 职责 | 创建 Memento(createMemento())和从 Memento 恢复(restoreMemento()) |
| Memento 设计 | 通常设计为 不可变对象,状态通过构造器传入,只提供 getter |
| 宽接口 vs 窄接口 | Originator 可以访问 Memento 内部状态(宽接口);Caretaker 只能持有 Memento 对象引用(窄接口) |
| 与原型模式区别 | 备忘录:保存状态用于 恢复 原对象;原型:复制状态用于 创建 新对象 |
| 与命令模式区别 | 备忘录:直接保存/恢复对象状态;命令:封装操作,通过 undo() 反向执行来撤销 |
| 适用场景 | ① 需要保存和恢复对象状态 ② 需要实现撤销/重做功能 ③ 状态信息需要封装,不能暴露给外部 |
| 不适用场景 | 对象状态占用内存极大(频繁保存备忘录会消耗大量内存);状态变化极频繁 |
8.2 易混淆辨析:备忘录 vs 原型 vs 命令
| 对比项 | 备忘录模式 | 原型模式 | 命令模式 |
|---|---|---|---|
| UML 结构 | Originator 创建/恢复 Memento,Caretaker 保管 Memento | Prototype 声明 clone(),ConcretePrototype 实现克隆 |
Invoker 调用 Command,Command 调用 Receiver |
| 状态流向 | Originator → Memento → Originator(闭环恢复) | Prototype → Client → 新对象(创建新实例) | Receiver → Command → 执行操作(改变 Receiver 状态) |
| 封装性 | 最强(Caretaker 完全看不到内部状态) | 较弱(克隆通常需要访问内部状态) | 中等(Command 可能持有 Receiver 引用) |
| 撤销方式 | 直接恢复之前保存的状态快照 | 不支持撤销 | 通过 undo() 方法反向执行操作 |
| 软考考法 | 代码填空(补全 createMemento + restoreMemento + Caretaker 保存逻辑) |
代码填空(补全 clone() 或拷贝构造器) |
代码填空(补全 execute() + undo() + 命令队列) |
九、真题风格模拟与代码填空
模拟题 1(上午选择题)
以下关于备忘录模式的叙述中,正确的是()。
A. 备忘录模式属于创建型模式,主要用于封装对象的创建逻辑
B. 在备忘录模式中,Caretaker 可以直接访问和修改 Memento 的内部状态
C. 备忘录模式在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后恢复
D. 备忘录模式与原型模式的核心区别在于:备忘录模式用于创建新对象,原型模式用于保存对象状态
答案:C
解析:
-
A 错误:备忘录模式属于 行为型 模式,不是创建型。它封装的是 状态保存与恢复行为,不是对象的创建。
-
B 错误:Caretaker 不能访问也不能修改 Memento 的内部状态。它只负责保管和传递备忘录对象。如果 Caretaker 能修改状态,就破坏了封装性。
-
C 正确:这是备忘录模式的核心定义(GoF 原书定义)。
-
D 错误:说反了。备忘录模式用于 保存状态以恢复 (不是创建新对象),原型模式用于 复制对象以创建 新对象。
模拟题 2(下午代码填空 --- 补全 Originator 和 Caretaker)
某系统使用备忘录模式实现绘图软件的撤销功能。
Canvas是画布(Originator),状态包括图形列表。CanvasMemento是备忘录。UndoManager是 Caretaker。请补全(1)~(4)。
java
// Memento
class CanvasMemento {
private List<String> shapes;
public CanvasMemento(List<String> shapes) {
this.shapes = new ArrayList<>(shapes); // 深拷贝
}
public List<String> getShapes() {
return new ArrayList<>(shapes);
}
}
// Originator
class Canvas {
private List<String> shapes = new ArrayList<>();
public void addShape(String shape) {
shapes.add(shape);
System.out.println("添加图形: " + shape);
}
public void display() {
System.out.println("画布内容: " + shapes);
}
// 创建备忘录
public CanvasMemento (1)______ {
return (2)______;
}
// 从备忘录恢复
public void (3)______(CanvasMemento memento) {
this.shapes = memento.getShapes();
System.out.println("画布状态已恢复");
}
}
// Caretaker
class UndoManager {
private Stack<CanvasMemento> history = new Stack<>();
public void save(CanvasMemento memento) {
history.push(memento);
}
public CanvasMemento undo() {
if (history.isEmpty()) return null;
return history.pop();
}
// 注意:Caretaker 没有 getShapes() 或修改 shapes 的方法
}
public class Client {
public static void main(String[] args) {
Canvas canvas = new Canvas();
UndoManager undo = new UndoManager();
canvas.addShape("圆形");
undo.save(canvas.createMemento());
canvas.addShape("矩形");
undo.save(canvas.createMemento());
canvas.addShape("三角形");
canvas.display();
// 撤销一次
CanvasMemento memento = undo.undo();
if (memento != null) {
canvas.(4)______(memento);
}
canvas.display();
}
}
答案:
-
(1)
createMemento() -
(2)
new CanvasMemento(shapes) -
(3)
restoreMemento -
(4)
restoreMemento
阅卷要点:
(1) 必须是
createMemento()。这是 Originator 创建备忘录的标准方法名。软考中通常用createMemento()或saveToMemento()。如果写getMemento()或newMemento()可能给分,但createMemento()是标准答案。(2) 必须是
new CanvasMemento(shapes)(或new CanvasMemento(this.shapes))。Originator 将当前状态封装进备忘录对象。如果写new CanvasMemento()不传参数,或写return shapes会 零分。(3) 必须是
restoreMemento。这是 Originator 从备忘录恢复状态的标准方法名。如果写loadMemento或recover可能给分,但restoreMemento是标准答案。(4) 必须是
restoreMemento。客户端从 Caretaker 取回备忘录后,调用 Originator 的恢复方法还原状态。如果写undo或load会 零分(除非 Originator 中定义了这些方法)。
模拟题 3(下午代码填空 --- 补全封装性设计)
某系统使用备忘录模式保存配置对象的状态。要求 Caretaker 不能访问配置的具体内容。请补全(1)~(2)。
java
// Memento:设计为包级私有或静态内部类,限制外部访问
public class ConfigMemento {
private String configData; // 私有状态
// 构造器:只允许同包或 Originator 访问
ConfigMemento(String data) { // 包级私有
this.configData = data;
}
// getter:只允许同包访问
String getConfigData() { // 包级私有
return configData;
}
}
// Originator
public class AppConfig {
private String settings = "default";
public void setSettings(String settings) {
this.settings = settings;
}
public (1)______ createMemento() {
return new ConfigMemento(settings);
}
public void restoreMemento((2)______ memento) {
this.settings = memento.getConfigData();
}
}
// Caretaker:只能保存 ConfigMemento 对象,无法访问其内部
public class ConfigManager {
private ConfigMemento backup;
public void backup(ConfigMemento memento) {
this.backup = memento;
}
public ConfigMemento getBackup() {
return backup;
}
// 注意:ConfigManager 无法调用 backup.getConfigData(),因为 getter 是包级私有
// 而 ConfigManager 如果在不同包,就完全看不到内部状态
}
答案:
-
(1)
ConfigMemento -
(2)
ConfigMemento
阅卷要点:
(1) 必须是
ConfigMemento。这是 Originator 创建备忘录的返回类型。如果写Memento或Object会 零分(除非题目定义了接口)。(2) 必须是
ConfigMemento。这是restoreMemento方法的参数类型。如果写String或Object会 零分。本题的核心考点是 封装性 :
ConfigMemento的构造器和 getter 都是包级私有(没有public),ConfigManager(如果在不同包)无法访问这些方法和字段,只能持有ConfigMemento对象引用。这确保了 Caretaker 不破坏封装性。
十、常见陷阱与注意事项
陷阱 1:误认为备忘录是创建型模式
备忘录模式属于 行为型 模式,因为它关注的是 对象的行为 (状态的保存与恢复),而不是对象的创建。软考上午题如果问"以下属于创建型模式的是",选项里出现"备忘录" 不能选。
陷阱 2:Caretaker 访问 Memento 的内部状态
这是软考 最高频的失分点。备忘录模式的核心封装性要求:
- ❌ 错误:
java
class Caretaker {
private Memento memento;
public void peek() {
System.out.println(memento.getState()); // Caretaker 偷看备忘录内容!
}
}
- ✅ 正确:
java
class Caretaker {
private Memento memento;
public void save(Memento m) {
this.memento = m; // 只保存,不查看
}
public Memento get() {
return memento; // 只传递,不查看
}
}
Caretaker 的职责是 "保管员" ,不是 "审计员"。它只负责把备忘录存起来和取出来,绝不应该访问备忘录的内部状态。如果 Caretaker 需要知道状态内容,说明设计违背了备忘录模式的封装原则。
陷阱 3:与原型模式混淆
这是软考 最难的辨析题 之一。两者的核心区别:
表格
| 维度 | 备忘录模式 | 原型模式 |
|---|---|---|
| 模式分类 | 行为型 | 创建型 |
| 核心目的 | 保存状态 用于 恢复 原对象 | 复制状态 用于 创建 新对象 |
| 状态流向 | Originator → Memento → Originator(闭环) | Prototype → Client → 新对象(开环) |
| 封装性 | 强(Caretaker 不能访问内部) | 较弱(克隆需要访问内部状态) |
| 产生对象 | 不产生新对象,只恢复原有对象 | 产生新对象 |
| 方法名 | createMemento()、restoreMemento() |
clone()、拷贝构造器 |
终极区分方法:看状态保存后的用途。
-
如果保存状态是为了 回到之前的状态(撤销、回滚、读档)→ 备忘录。
-
如果保存状态是为了 基于现有对象创建一个新对象(复制、克隆)→ 原型。
陷阱 4:与命令模式混淆
表格
| 维度 | 备忘录模式 | 命令模式 |
|---|---|---|
| 撤销方式 | 直接保存并恢复对象的 状态快照 | 封装操作,通过 undo() 方法 反向执行操作 |
| 状态存储 | 存储在 Memento 对象中 | 存储在 Command 对象中(可能记录操作前状态) |
| 封装对象 | 封装 Originator 的 状态 | 封装 操作/请求 |
| 典型应用 | 编辑器撤销、游戏存档 | 菜单命令、宏录制、事务管理 |
如果类图里有 execute() 和 undo() 方法,且 undo() 是通过反向操作(如 insert 的 undo 是 delete)实现的 → 命令模式。如果类图里有 createMemento() 和 restoreMemento(),且恢复是直接替换状态 → 备忘录模式。
陷阱 5:Memento 没有深拷贝
如果 Memento 中保存的是引用类型(如 List、Map),而 Memento 只是浅拷贝了引用,那么外部修改原对象的状态会影响备忘录中保存的状态,导致恢复时出错。
- ❌ 错误:
java
class Memento {
private List<String> state;
public Memento(List<String> state) {
this.state = state; // 浅拷贝!外部修改会影响备忘录
}
}
- ✅ 正确:
java
class Memento {
private List<String> state;
public Memento(List<String> state) {
this.state = new ArrayList<>(state); // 深拷贝
}
}
软考中,如果 Memento 保存的是集合或对象引用,必须在构造器中进行深拷贝。这是保证备忘录独立性的关键。
陷阱 6:Originator 没有提供恢复方法
有些同学只写了 createMemento() 保存状态,但忘记写 restoreMemento() 恢复状态。备忘录模式必须成对出现:能存就能取。
- ❌ 错误:
java
class Originator {
public Memento createMemento() { ... }
// 没有 restoreMemento 方法!
}
- ✅ 正确:
java
class Originator {
public Memento createMemento() { ... }
public void restoreMemento(Memento m) { ... }
}
软考代码填空时,如果给出了 createMemento(),通常需要补全的就是 restoreMemento()。
陷阱 7:Memento 设计得过于复杂
Memento 应该只保存 必要的状态信息,不应该包含业务逻辑或对外部对象的引用。如果 Memento 变得过于复杂,就失去了"轻量级状态快照"的意义。
- ❌ 错误:
java
class Memento {
private String state;
private DatabaseConnection db; // 不应该持有外部资源引用!
private Logger logger; // 不应该持有工具类!
}
- ✅ 正确:
java
class Memento {
private String state; // 只保存纯状态数据
}
十一、总结
| 要点 | 内容 |
|---|---|
| 定义 | 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态 |
| 分类 | 行为型模式(GoF 23 正式成员) |
| 核心三角色 | Originator(原发器)、Memento(备忘录)、Caretaker(负责人) |
| 核心思想 | 将对象状态封装到备忘录中,由 Originator 创建和恢复,由 Caretaker 保管,不破坏封装 |
| 封装性 | Caretaker 不能访问 Memento 的内部状态,只能保存和传递 |
| Originator 方法 | createMemento()(创建)、restoreMemento()(恢复) |
| Memento 设计 | 通常设计为 不可变对象,构造器传入状态,只提供 getter |
| 宽接口 vs 窄接口 | Originator 可访问 Memento 内部(宽接口);Caretaker 只能持有引用(窄接口) |
| 与原型模式区别 | 备忘录:保存状态用于 恢复 ;原型:复制状态用于 创建 新对象 |
| 与命令模式区别 | 备忘录:直接恢复状态快照;命令:通过 undo() 反向执行操作 |
| 深拷贝 | Memento 保存引用类型时必须深拷贝,确保状态独立性 |
| 适用场景 | ① 需要撤销/重做 ② 需要保存/恢复状态 ③ 状态需要封装 |
| 不适用场景 | 对象状态极大(内存消耗高);状态变化极频繁 |
| 软考重点 | 代码填空(补全 createMemento + restoreMemento + Caretaker 保存);与原型、命令模式的辨析;封装性设计 |
| 答题技巧 | 看到 "保存状态恢复"、"不破坏封装"、"快照"、"撤销" → 备忘录;看到 Caretaker 只存不读 → 确认备忘录 |
系列预告 :下一篇将讲 解释器模式 ------ 当需要为某种语言定义文法,并基于该文法解释句子时,如何将文法规则表示为类层次结构。解释器模式与组合模式、访问者模式的结合是软考高频考点,咱们下回重点拆解。咱们下回见。