Skip to content

备忘录模式(Memento Pattern) ​

一、模式概述 ​

备忘录模式是一种行为型设计模式,它在不暴露对象实现细节的情况下,保存和恢复对象之前的状态。就像游戏中的存档功能,你可以随时回到之前的状态。

核心思想:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后可以将该对象恢复到原先保存的状态。

二、角色说明 ​

角色说明
Originator(原发器)需要保存状态的对象,负责创建备忘录和恢复状态
Memento(备忘录)存储 Originator 的内部状态
Caretaker(管理者)负责保存备忘录,但不能对备忘录的内容进行操作或检查

三、生活中的例子 ​

  • 游戏存档:保存游戏进度,随时可以读档回到之前的状态
  • 文档撤销:Ctrl+Z 撤销操作,回到之前的编辑状态
  • 数据库备份:定期备份数据,出问题时可以恢复
  • Git 版本控制:每次 commit 就是创建一个备忘录
  • 购物车:保存用户选中的商品,可以恢复到之前的状态

四、示例1:游戏存档系统 ​

问题场景 ​

RPG 游戏支持多存档位,玩家可以保存和加载游戏进度。

java
import java.util.ArrayList;
import java.util.List;

// ========== 备忘录 ==========

/**
 * 游戏存档(备忘录)
 * 封装游戏状态,对管理者(Caretaker)不透明
 */
class GameSave {
    private final String playerName;
    private final int level;
    private final int health;
    private final int experience;
    private final String currentMap;
    private final int gold;
    private final String timestamp;

    public GameSave(String playerName, int level, int health,
                    int experience, String currentMap, int gold) {
        this.playerName = playerName;
        this.level = level;
        this.health = health;
        this.experience = experience;
        this.currentMap = currentMap;
        this.gold = gold;
        this.timestamp = java.time.LocalDateTime.now()
                .format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }

    // 只提供 getter,不提供 setter(不可变对象)
    public String getPlayerName() { return playerName; }
    public int getLevel() { return level; }
    public int getHealth() { return health; }
    public int getExperience() { return experience; }
    public String getCurrentMap() { return currentMap; }
    public int getGold() { return gold; }
    public String getTimestamp() { return timestamp; }

    @Override
    public String toString() {
        return String.format("[%s] 玩家:%s 等级:%d 血量:%d 经验:%d 地图:%s 金币:%d",
                timestamp, playerName, level, health, experience, currentMap, gold);
    }
}

// ========== 原发器 ==========

/**
 * 游戏角色(原发器)
 * 负责创建备忘录和从备忘录恢复状态
 */
class GameCharacter {
    private String playerName;
    private int level;
    private int health;
    private int experience;
    private String currentMap;
    private int gold;

    public GameCharacter(String playerName) {
        this.playerName = playerName;
        this.level = 1;
        this.health = 100;
        this.experience = 0;
        this.currentMap = "新手村";
        this.gold = 100;
    }

    /**
     * 保存当前状态为备忘录
     */
    public GameSave save() {
        System.out.println("  [存档] 保存 " + playerName + " 的游戏进度...");
        return new GameSave(playerName, level, health, experience, currentMap, gold);
    }

    /**
     * 从备忘录恢复状态
     */
    public void load(GameSave save) {
        this.playerName = save.getPlayerName();
        this.level = save.getLevel();
        this.health = save.getHealth();
        this.experience = save.getExperience();
        this.currentMap = save.getCurrentMap();
        this.gold = save.getGold();
        System.out.println("  [读档] 恢复游戏进度: " + save);
    }

    // 模拟游戏操作
    public void fight() {
        health -= 20;
        experience += 50;
        gold += 30;
        System.out.println("  [战斗] 战斗胜利!血量:" + health + " 经验:" + experience + " 金币:" + gold);
    }

    public void heal() {
        health = Math.min(health + 50, 100);
        gold -= 20;
        System.out.println("  [治疗] 恢复血量!血量:" + health + " 金币:" + gold);
    }

    public void changeMap(String map) {
        this.currentMap = map;
        System.out.println("  [移动] 进入地图: " + map);
    }

    public void levelUp() {
        level++;
        health = 100;
        experience = 0;
        System.out.println("  [升级] 升到 " + level + " 级!");
    }

    public void showStatus() {
        System.out.println(String.format(
            "  [状态] 玩家:%s 等级:%d 血量:%d 经验:%d 地图:%s 金币:%d",
            playerName, level, health, experience, currentMap, gold));
    }
}

// ========== 管理者 ==========

/**
 * 存档管理器(管理者)
 * 负责保存多个存档,不关心存档的具体内容
 */
class SaveManager {
    private final List<GameSave> saveSlots;
    private final int maxSlots;

    public SaveManager(int maxSlots) {
        this.maxSlots = maxSlots;
        this.saveSlots = new ArrayList<>();
    }

    /**
     * 保存存档
     */
    public boolean saveGame(GameSave save) {
        if (saveSlots.size() >= maxSlots) {
            System.out.println("  [存档管理] 存档已满(最多 " + maxSlots + " 个),请删除旧存档");
            return false;
        }
        saveSlots.add(save);
        System.out.println("  [存档管理] 存档成功,当前存档数: " + saveSlots.size());
        return true;
    }

    /**
     * 读取存档
     */
    public GameSave loadGame(int slotIndex) {
        if (slotIndex < 0 || slotIndex >= saveSlots.size()) {
            System.out.println("  [存档管理] 无效的存档位置!");
            return null;
        }
        GameSave save = saveSlots.get(slotIndex);
        System.out.println("  [存档管理] 读取存档 #" + (slotIndex + 1));
        return save;
    }

    /**
     * 删除存档
     */
    public boolean deleteSave(int slotIndex) {
        if (slotIndex < 0 || slotIndex >= saveSlots.size()) {
            return false;
        }
        saveSlots.remove(slotIndex);
        System.out.println("  [存档管理] 存档已删除");
        return true;
    }

    /**
     * 显示所有存档
     */
    public void showAllSaves() {
        System.out.println("  [存档管理] 当前存档列表:");
        if (saveSlots.isEmpty()) {
            System.out.println("    (空)");
            return;
        }
        for (int i = 0; i < saveSlots.size(); i++) {
            System.out.println("    存档 #" + (i + 1) + ": " + saveSlots.get(i));
        }
    }

    public int getSaveCount() {
        return saveSlots.size();
    }
}

// ========== 客户端代码 ==========

public class GameSaveDemo {
    public static void main(String[] args) {
        GameCharacter player = new GameCharacter("勇者小明");
        SaveManager saveManager = new SaveManager(3);  // 最多3个存档位

        System.out.println("===== 游戏开始 =====");
        player.showStatus();

        System.out.println("\n===== 战斗、探索 =====");
        player.fight();
        player.fight();
        player.changeMap("黑暗森林");
        player.fight();

        System.out.println("\n===== 保存游戏(存档1) =====");
        saveManager.saveGame(player.save());
        saveManager.showAllSaves();

        System.out.println("\n===== 继续冒险 =====");
        player.fight();
        player.heal();
        player.changeMap("龙之谷");

        System.out.println("\n===== 保存游戏(存档2) =====");
        saveManager.saveGame(player.save());

        System.out.println("\n===== 继续冒险(遇到强敌) =====");
        player.fight();
        player.fight();
        player.showStatus();

        System.out.println("\n===== 战斗失败,读取存档1 =====");
        GameSave save1 = saveManager.loadGame(0);
        if (save1 != null) {
            player.load(save1);
            player.showStatus();
        }

        System.out.println("\n===== 重新挑战 =====");
        player.changeMap("龙之谷");
        player.heal();
        player.fight();
    }
}

五、示例2:文本编辑器状态备忘录 ​

问题场景 ​

文本编辑器支持多级撤销/重做,使用备忘录模式保存编辑历史。

java
import java.util.Stack;
import java.util.ArrayList;
import java.util.List;

// ========== 备忘录 ==========

/**
 * 编辑器快照(不可变备忘录)
 */
class EditorSnapshot {
    private final String content;
    private final int cursorPosition;
    private final String description;
    private final long timestamp;

    public EditorSnapshot(String content, int cursorPosition, String description) {
        this.content = content;
        this.cursorPosition = cursorPosition;
        this.description = description;
        this.timestamp = System.currentTimeMillis();
    }

    public String getContent() { return content; }
    public int getCursorPosition() { return cursorPosition; }
    public String getDescription() { return description; }
    public long getTimestamp() { return timestamp; }
}

// ========== 原发器 ==========

/**
 * 文本编辑器(原发器)
 */
class TextEditor {
    private StringBuilder content = new StringBuilder();
    private int cursorPosition = 0;

    public void type(String text) {
        content.insert(cursorPosition, text);
        cursorPosition += text.length();
        System.out.println("  [输入] \"" + text + "\" -> \"" + content + "\"");
    }

    public void delete(int count) {
        int start = Math.max(0, cursorPosition - count);
        content.delete(start, cursorPosition);
        cursorPosition = start;
        System.out.println("  [删除] -> \"" + content + "\"");
    }

    public void moveCursor(int position) {
        this.cursorPosition = Math.max(0, Math.min(position, content.length()));
        System.out.println("  [光标] 移动到位置 " + cursorPosition);
    }

    /**
     * 创建当前状态的快照
     */
    public EditorSnapshot createSnapshot(String description) {
        return new EditorSnapshot(content.toString(), cursorPosition, description);
    }

    /**
     * 从快照恢复状态
     */
    public void restoreFromSnapshot(EditorSnapshot snapshot) {
        this.content = new StringBuilder(snapshot.getContent());
        this.cursorPosition = snapshot.getCursorPosition();
        System.out.println("  [恢复] " + snapshot.getDescription()
                + " -> \"" + content + "\"");
    }

    public String getContent() { return content.toString(); }
    public int getCursorPosition() { return cursorPosition; }
}

// ========== 管理者 ==========

/**
 * 编辑历史管理器(管理者)
 * 支持多级撤销/重做
 */
class EditorHistory {
    private final Stack<EditorSnapshot> undoStack = new Stack<>();
    private final Stack<EditorSnapshot> redoStack = new Stack<>();
    private final TextEditor editor;

    public EditorHistory(TextEditor editor) {
        this.editor = editor;
    }

    /**
     * 保存当前状态
     */
    public void saveState(String description) {
        EditorSnapshot snapshot = editor.createSnapshot(description);
        undoStack.push(snapshot);
        redoStack.clear();  // 新操作清空重做栈
        System.out.println("  [历史] 保存: " + description);
    }

    /**
     * 撤销
     */
    public boolean undo() {
        if (undoStack.isEmpty()) {
            System.out.println("  [历史] 没有可撤销的操作");
            return false;
        }
        // 先保存当前状态到重做栈
        EditorSnapshot currentState = editor.createSnapshot("当前状态");
        redoStack.push(currentState);

        // 恢复上一个状态
        EditorSnapshot previousState = undoStack.pop();
        editor.restoreFromSnapshot(previousState);
        System.out.println("  [历史] 撤销: " + previousState.getDescription());
        return true;
    }

    /**
     * 重做
     */
    public boolean redo() {
        if (redoStack.isEmpty()) {
            System.out.println("  [历史] 没有可重做的操作");
            return false;
        }
        // 先保存当前状态到撤销栈
        EditorSnapshot currentState = editor.createSnapshot("当前状态");
        undoStack.push(currentState);

        // 恢复重做状态
        EditorSnapshot nextState = redoStack.pop();
        editor.restoreFromSnapshot(nextState);
        System.out.println("  [历史] 重做: " + nextState.getDescription());
        return true;
    }

    /**
     * 显示历史状态
     */
    public void showHistory() {
        System.out.println("  [历史] 撤销栈 (" + undoStack.size() + " 步):");
        for (int i = undoStack.size() - 1; i >= 0; i--) {
            System.out.println("    " + undoStack.get(i).getDescription()
                    + " -> \"" + undoStack.get(i).getContent() + "\"");
        }
        System.out.println("  [历史] 重做栈 (" + redoStack.size() + " 步):");
        for (EditorSnapshot snapshot : redoStack) {
            System.out.println("    " + snapshot.getDescription()
                    + " -> \"" + snapshot.getContent() + "\"");
        }
    }
}

// ========== 客户端代码 ==========

public class EditorMementoDemo {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();
        EditorHistory history = new EditorHistory(editor);

        System.out.println("===== 编辑操作 =====");
        history.saveState("初始状态");
        editor.type("Hello");

        history.saveState("输入Hello");
        editor.type(" World");

        history.saveState("输入World");
        editor.type(" Java");

        System.out.println("\n===== 撤销操作 =====");
        history.undo();
        history.undo();

        System.out.println("\n===== 重做操作 =====");
        history.redo();

        System.out.println("\n===== 新操作(清空重做栈) =====");
        history.saveState("新操作前");
        editor.type(" Design Pattern");

        System.out.println("\n===== 显示历史 =====");
        history.showHistory();
    }
}

六、UML 类图 ​

┌────────────┐  保存/恢复  ┌──────────────┐  管理  ┌────────────┐
│   Client    │──────────▶│  Originator   │◀──────│  Caretaker  │
└────────────┘           │  (原发器)      │       │  (管理者)    │
                         │  +save()      │       │ -mementos:  │
                         │  +restore()   │       │   List      │
                         └───────┬───────┘       └─────┬──────┘
                                 │ 创建                 │ 保存
                                 ▼                     ▼
                         ┌──────────────┐       ┌──────────────┐
                         │   Memento    │       │   Memento    │
                         │  (备忘录)     │◀──────│   备忘录对象  │
                         │ -state       │       └──────────────┘
                         │ +getState()  │
                         └──────────────┘

七、宽接口 vs 窄接口 ​

java
/**
 * 窄接口(Narrow Interface)
 * 只有特定类(Originator)可以访问备忘录的内容
 * Caretaker 只能看到备忘录对象,无法访问内部状态
 */

// 窄接口:Caretaker 看到的
// Memento 只是一个标记类,没有公共方法

/**
 * 宽接口(Wide Interface)
 * Originator 可以访问备忘录的所有内容
 * 但需要防止 Caretaker 访问
 */

// Java 中实现窄接口的方式:使用包级私有访问
// class Memento {
//     private String state;
//     // 只有同包的 Originator 能访问
//     String getState() { return state; }  // 包级私有
// }

// 实际上,Java 中常用的方式是:
// 1. 将 Memento 设为 Originator 的内部类
// 2. 将 Memento 的 getter 设为私有,通过 Originator 的方法访问

八、备忘录模式的实际应用 ​

java
/**
 * 1. 数据库事务回滚
 *
 * 数据库的 undo log 就是备忘录模式的体现
 * 事务开始前保存数据的旧版本
 * 回滚时恢复旧版本
 */

/**
 * 2. Git 版本控制
 *
 * git commit = 创建备忘录
 * git revert = 恢复到之前的备忘录
 * git stash = 临时保存工作状态
 */

/**
 * 3. React 状态管理
 *
 * Redux 的 Store 快照就是备忘录
 * 每次 dispatch action 都会创建新的状态快照
 * 可以通过 time-travel debugging 回到任意状态
 */

/**
 * 4. Spring Session 管理
 *
 * Session 的创建和销毁就是备忘录的应用
 * 每个 Session 保存用户的登录状态
 */

九、优缺点分析 ​

优点 ​

  1. 保持封装性:不暴露对象的内部实现细节
  2. 简化原发器:原发器不需要管理状态的保存和恢复逻辑
  3. 支持多级撤销:通过保存多个备忘录实现多级撤销/重做
  4. 状态快照:可以保存对象的历史状态,方便回溯

缺点 ​

  1. 内存消耗:保存大量备忘录会消耗大量内存
  2. 性能开销:频繁创建备忘录可能影响性能
  3. 封装破坏:如果备忘录使用序列化实现,可能会破坏封装性
  4. 状态管理复杂:需要管理多个备忘录的状态

十、使用场景 ​

  • 文本编辑器:撤销/重做功能
  • 游戏存档:保存和恢复游戏进度
  • 数据库事务:回滚操作
  • 版本控制:Git 的 commit 和 revert
  • 状态机:保存状态机的历史状态
  • 事务管理:Spring 的声明式事务

十一、最佳实践 ​

  1. 使用不可变备忘录:备忘录对象应该是不可变的,防止被修改
  2. 控制备忘录数量:限制备忘录的最大数量,避免内存溢出
  3. 使用序列化:对于大对象,可以使用序列化将备忘录持久化到磁盘
  4. 考虑增量快照:只保存变化的部分,减少内存占用
  5. 使用原型模式:使用原型模式创建备忘录的深拷贝
java
// 推荐:使用不可变对象作为备忘录
class ImmutableMemento {
    private final String state;
    private final int version;

    public ImmutableMemento(String state, int version) {
        this.state = state;
        this.version = version;
    }

    // 只有 getter,没有 setter
    public String getState() { return state; }
    public int getVersion() { return version; }
}

总结:备忘录模式的核心是"保存和恢复对象状态"。它在不破坏封装性的前提下,捕获对象的内部状态,并在需要时恢复。在实际应用中,文本编辑器的撤销/重做、游戏存档、数据库事务回滚等都是备忘录模式的典型应用。使用时要注意控制备忘录的数量,避免内存溢出。