Skip to content

备忘录模式 (Memento)

一、定义

一句话概括:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复。

官方定义:Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later.

二、解决的问题

2.1 问题场景

许多应用需要撤销(Undo)功能,将对象恢复到之前的状态。例如:

  • 文本编辑器:撤销输入的文字
  • 游戏:保存游戏进度,死亡后读取存档
  • 数据库事务:回滚到事务开始前的状态
  • 版本控制:Git 回退到历史版本

2.2 不用备忘录模式会怎样?

java
// 反例:直接暴露内部状态
class Game {
    public int level;
    public int score;
    public int health;
    // 暴露所有状态,破坏封装
}

// 客户端保存状态
Game game = new Game();
int savedLevel = game.level;
int savedScore = game.score;
// 恢复时逐个赋值
game.level = savedLevel;
game.score = savedScore;

问题:

  1. 破坏了对象的封装性,暴露了内部细节
  2. 客户端需要了解对象的所有状态字段
  3. 新增状态字段需要修改所有保存/恢复代码
  4. 无法保存私有状态

三、结构

3.1 角色组成

角色说明
Originator(发起人)需要保存状态的对象,创建备忘录并从备忘录恢复状态
Memento(备忘录)存储 Originator 的内部状态,防止 Originator 以外的对象访问
Caretaker(负责人)保存备忘录,但不能修改或查看备忘录的内容

3.2 类图(ASCII)

┌──────────────┐    创建     ┌──────────────┐    保存     ┌──────────────┐
│  Originator  │───────────►│   Memento    │◄────────────│  Caretaker   │
├──────────────┤            ├──────────────┤            ├──────────────┤
│ - state      │            │ - state      │            │ - memento    │
│+ createMemento()│          │+ getState()  │            │+ save()      │
│+ restore(m)  │            │ (包级私有)    │            │+ undo()      │
└──────────────┘            └──────────────┘            └──────────────┘

四、代码实现

4.1 基础实现

java
// ==================== 备忘录 ====================
class GameMemento {
    private final int level;
    private final int score;
    private final int health;

    // 包级私有构造函数,只有 Originator 能创建
    GameMemento(int level, int score, int health) {
        this.level = level;
        this.score = score;
        this.health = health;
    }

    // 包级私有 getter,只有 Originator 能读取
    int getLevel() { return level; }
    int getScore() { return score; }
    int getHealth() { return health; }
}

// ==================== 发起人 ====================
class Game {
    private int level;
    private int score;
    private int health;

    public Game() {
        this.level = 1;
        this.score = 0;
        this.health = 100;
    }

    public void play() {
        level++;
        score += 100;
        health -= 10;
        System.out.println("当前状态 - 等级:" + level
                + " 分数:" + score + " 生命:" + health);
    }

    // 创建备忘录
    public GameMemento save() {
        return new GameMemento(level, score, health);
    }

    // 从备忘录恢复
    public void restore(GameMemento memento) {
        this.level = memento.getLevel();
        this.score = memento.getScore();
        this.health = memento.getHealth();
    }
}

// ==================== 负责人 ====================
class GameCaretaker {
    private GameMemento memento;

    public void save(GameMemento memento) {
        this.memento = memento;
    }

    public GameMemento getMemento() {
        return memento;
    }
}

// ==================== 客户端 ====================
public class MementoDemo {
    public static void main(String[] args) {
        Game game = new Game();
        GameCaretaker caretaker = new GameCaretaker();

        game.play(); // 等级:2 分数:100 生命:90
        caretaker.save(game.save()); // 保存

        game.play(); // 等级:3 分数:200 生命:80
        game.play(); // 等级:4 分数:300 生命:70

        game.restore(caretaker.getMemento()); // 恢复到保存点
        System.out.println("回档后 - 等级:" + game.getLevel()); // 恢复为 2
    }
}

4.2 进阶实现

4.2.1 白箱备忘录 vs 黑箱备忘录

白箱备忘录:备忘录对 Originator 完全透明,Originator 可以随意访问备忘录内部状态(如上面的示例,通过包级私有访问)。

黑箱备忘录:备忘录对 Originator 也不透明,通过宽接口/窄接口实现。

java
// 黑箱备忘录:使用内部类实现
class GameV2 {
    private int level;
    private int score;

    // 宽接口:Originator 可访问全部状态
    private class MementoImpl implements Memento {
        private int level;
        private int score;

        MementoImpl(int level, int score) {
            this.level = level;
            this.score = score;
        }
    }

    // 窄接口:Caretaker 只能看到标记接口
    interface Memento { }

    public Memento save() {
        return new MementoImpl(level, score);
    }

    public void restore(Memento memento) {
        MementoImpl impl = (MementoImpl) memento;
        this.level = impl.level;
        this.score = impl.score;
    }
}

4.2.2 多状态快照(版本管理)

java
class DocumentCaretaker {
    private List<DocumentMemento> history = new ArrayList<>();
    private int currentVersion = -1;

    public void save(DocumentMemento memento) {
        // 如果当前不是最新版本,删除之后的版本
        if (currentVersion < history.size() - 1) {
            history = history.subList(0, currentVersion + 1);
        }
        history.add(memento);
        currentVersion++;
    }

    public DocumentMemento undo() {
        if (currentVersion > 0) {
            currentVersion--;
            return history.get(currentVersion);
        }
        return null;
    }

    public DocumentMemento redo() {
        if (currentVersion < history.size() - 1) {
            currentVersion++;
            return history.get(currentVersion);
        }
        return null;
    }
}

// 文档类
class Document {
    private String content;

    public Document(String content) { this.content = content; }

    public void append(String text) {
        content += text;
    }

    public DocumentMemento save() {
        return new DocumentMemento(content);
    }

    public void restore(DocumentMemento memento) {
        this.content = memento.getContent();
    }

    public String getContent() { return content; }
}

class DocumentMemento {
    private final String content;
    DocumentMemento(String content) { this.content = content; }
    String getContent() { return content; }
}

// 使用
public class VersionControlDemo {
    public static void main(String[] args) {
        Document doc = new Document("Hello");
        DocumentCaretaker caretaker = new DocumentCaretaker();

        caretaker.save(doc.save()); // v0: "Hello"

        doc.append(" World");
        caretaker.save(doc.save()); // v1: "Hello World"

        doc.append("!");
        System.out.println(doc.getContent()); // "Hello World!"

        doc.restore(caretaker.undo()); // 撤销
        System.out.println(doc.getContent()); // "Hello World"

        doc.restore(caretaker.redo()); // 重做
        System.out.println(doc.getContent()); // "Hello World!"
    }
}

4.3 生产级实现

Spring Boot 表单草稿保存

java
// ==================== 表单数据 ====================
@Data
class FormData {
    private String title;
    private String content;
    private List<String> tags;
    private LocalDateTime updatedAt;

    public FormDataMemento save() {
        return new FormDataMemento(title, content,
                new ArrayList<>(tags != null ? tags : List.of()), updatedAt);
    }

    public void restore(FormDataMemento memento) {
        this.title = memento.getTitle();
        this.content = memento.getContent();
        this.tags = new ArrayList<>(memento.getTags());
        this.updatedAt = memento.getUpdatedAt();
    }
}

// ==================== 备忘录 ====================
class FormDataMemento implements Serializable {
    private static final long serialVersionUID = 1L;
    private final String title;
    private final String content;
    private final List<String> tags;
    private final LocalDateTime updatedAt;

    FormDataMemento(String title, String content, List<String> tags, LocalDateTime updatedAt) {
        this.title = title;
        this.content = content;
        this.tags = tags;
        this.updatedAt = updatedAt;
    }

    String getTitle() { return title; }
    String getContent() { return content; }
    List<String> getTags() { return tags; }
    LocalDateTime getUpdatedAt() { return updatedAt; }
}

// ==================== 草稿服务 ====================
@Service
class DraftService {
    private final Map<String, Deque<FormDataMemento>> draftStore = new ConcurrentHashMap<>();
    private static final int MAX_HISTORY = 20;

    public void saveDraft(String userId, FormData data) {
        Deque<FormDataMemento> history = draftStore.computeIfAbsent(userId,
                k -> new ArrayDeque<>());
        if (history.size() >= MAX_HISTORY) {
            history.removeFirst(); // 移除最旧的草稿
        }
        history.addLast(data.save());
    }

    public boolean restoreDraft(String userId, FormData data) {
        Deque<FormDataMemento> history = draftStore.get(userId);
        if (history != null && !history.isEmpty()) {
            data.restore(history.getLast());
            return true;
        }
        return false;
    }

    public boolean undo(String userId, FormData data) {
        Deque<FormDataMemento> history = draftStore.get(userId);
        if (history != null && history.size() > 1) {
            history.removeLast(); // 移除当前状态
            data.restore(history.getLast());
            return true;
        }
        return false;
    }
}

// ==================== Controller ====================
@RestController
@RequestMapping("/api/draft")
class DraftController {
    @Autowired private DraftService draftService;

    @PostMapping("/save")
    public String save(@RequestHeader("userId") String userId,
                       @RequestBody FormData data) {
        data.setUpdatedAt(LocalDateTime.now());
        draftService.saveDraft(userId, data);
        return "草稿已保存";
    }

    @PostMapping("/restore")
    public FormData restore(@RequestHeader("userId") String userId) {
        FormData data = new FormData();
        draftService.restoreDraft(userId, data);
        return data;
    }

    @PostMapping("/undo")
    public FormData undo(@RequestHeader("userId") String userId) {
        FormData data = new FormData();
        draftService.undo(userId, data);
        return data;
    }
}

五、优缺点

优点

  1. 封装性:不破坏 Originator 的封装,状态保存和恢复逻辑在 Originator 内部
  2. 简化 Originator:状态管理职责从 Originator 中分离
  3. 易于扩展:新增状态字段只需修改 Memento 和 Originator

缺点

  1. 内存开销:频繁创建备忘录对象会消耗大量内存
  2. 维护成本:Caretaker 需要管理备忘录的生命周期
  3. 性能影响:保存和恢复大对象状态可能耗时

六、适用场景

  1. 撤销/重做功能:文本编辑器、IDE、图片处理软件
  2. 游戏存档:保存和恢复游戏进度
  3. 数据库事务回滚:事务开始前保存快照
  4. 表单草稿:Web 表单的自动保存和恢复
  5. 版本控制:Git 的 commit 和 revert
  6. 虚拟机快照:VMware 的快照功能

七、JDK / Spring 框架中的实际应用

框架应用位置说明
JDKjava.util.DateDate 对象可通过 getTime() 保存快照
JDKjava.io.Serializable序列化是实现备忘录模式的一种方式
SpringStateManageableMessageContextSpring Web Flow 的状态管理
SpringHibernate 一级缓存保存实体对象的快照,用于脏检查
MyBatis缓存保存查询结果的快照

八、与其他模式的关系

与命令模式

  • 命令模式常与备忘录模式结合使用,备忘录保存接收者的状态,用于实现撤销操作

与原型模式

  • 备忘录模式可以通过原型模式实现(克隆对象作为备忘录),但需要注意深拷贝和浅拷贝的问题

与状态模式

  • 状态模式可以用备忘录模式保存状态历史,实现状态回退

与迭代器模式

  • 迭代器内部的状态(当前位置)可以看作备忘录,支持遍历的暂停和恢复

九、面试常见问题

Q1:备忘录模式如何保证封装性?

A:通过包级私有或内部类机制。备忘录的状态字段和访问方法只对 Originator 可见(包级私有),Caretaker 只能持有备忘录的引用但无法访问其内部状态。在 Java 中,可以利用内部类实现窄接口(标记接口)和宽接口(具体实现类)。

Q2:如何处理大对象的备忘录内存开销?

A:(1) 增量保存:只保存变化的部分,而不是整个对象;(2) 压缩存储:序列化后压缩;(3) 持久化到磁盘:将不常用的备忘录持久化到数据库或文件;(4) 限制历史数量:只保留最近 N 个备忘录。

Q3:备忘录模式和原型模式在实现撤销时有什么区别?

A:备忘录模式通过保存对象的状态快照来实现撤销,不暴露对象内部结构;原型模式通过克隆对象来实现撤销,需要实现深拷贝,可能暴露内部结构。备忘录模式更符合封装原则,但需要额外的 Memento 类。

Q4:Hibernate 是如何使用备忘录模式进行脏检查的?

A:Hibernate 在加载实体时(一级缓存),会保存实体的原始状态快照。在 flush 时,将当前状态与快照进行比较,如果发现变化(脏数据),则生成对应的 UPDATE SQL 语句。这个快照就是备忘录模式中的 Memento。