【软考设计模式】备忘录模式:对象状态的捕获与无损恢复精讲

系列定位:软考软件设计师 / 系统架构设计师 --- 行为型模式专题第 6 讲

考察分值:上午题 1-2 分,下午题常作为代码填空或类图识别出现

难度等级:⭐⭐⭐☆☆(结构清晰,Originator、Memento、Caretaker 三者的封装关系是理解重点)


一、考纲定位与模式定义
1.1 考纲要求

备忘录模式在软考中属于 行为型模式 的基础内容。考察形式包括:

  • 上午选择题:判断场景描述所属模式;识别备忘录模式与原型模式、命令模式的区别;判断 Originator、Memento、Caretaker 三个角色;识别 "在不破坏封装性的前提下捕获和恢复对象内部状态" 的设计意图

  • 下午设计题 :补全 OriginatorcreateMemento()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 从备忘录恢复状态的标准方法名。如果写 loadMementorecover 可能给分,但 restoreMemento 是标准答案。

  • (4) 必须是 restoreMemento。客户端从 Caretaker 取回备忘录后,调用 Originator 的恢复方法还原状态。如果写 undoload零分(除非 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 创建备忘录的返回类型。如果写 MementoObject零分(除非题目定义了接口)。

  • (2) 必须是 ConfigMemento。这是 restoreMemento 方法的参数类型。如果写 StringObject零分

本题的核心考点是 封装性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() 是通过反向操作(如 insertundodelete)实现的 → 命令模式。如果类图里有 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 只存不读 → 确认备忘录

系列预告 :下一篇将讲 解释器模式 ------ 当需要为某种语言定义文法,并基于该文法解释句子时,如何将文法规则表示为类层次结构。解释器模式与组合模式、访问者模式的结合是软考高频考点,咱们下回重点拆解。咱们下回见。

相关推荐
2501_914245932 小时前
C语言设计模式详解:从理论到实践的完整指南
c语言·开发语言·设计模式
CaffeinePro6 小时前
四⼤极简架构原则KISS/DRY/YAGNI/LOD
设计模式·架构
霸道流氓气质7 小时前
SpringBoot中设计模式组合使用示例-策略、模板、观察者、门面、工厂、单例。
spring boot·后端·设计模式
ttod_qzstudio8 小时前
【软考设计模式】观察者模式:一对多依赖的自动通知与订阅解耦精讲
观察者模式·设计模式
ttod_qzstudio10 小时前
【软考设计模式】命令模式:请求封装与调用接收解耦的精讲
设计模式·命令模式
春卷同学10 小时前
HarmonyOS掌上记账APP开发实践第5篇:鸿蒙 EventBus 设计模式 — MoneyTrack 跨模块通信解密
华为·设计模式·harmonyos
春卷同学13 小时前
HarmonyOS掌上记账APP开发实践第3篇:接口与实现分离 — MoneyTrack Types.ets 与 Models.ets 的双层设计模式
设计模式
NG4771 天前
【软件设计与体系结构】-策略设计模式
java·设计模式·软件工程
CaffeinePro1 天前
SOLID五大设计原则:重构优雅代码与架构的底层规范
设计模式·架构