1. 概述

本文将深入讲解 Memento(备忘录)设计模式的核心思想与实际应用。

先从理论入手,再通过一个贴近实际的文本编辑器示例,带你掌握如何在 Java 中优雅地实现“撤销”功能。踩过坑的都知道,状态回滚看似简单,一旦设计不好就会破坏封装、污染代码,而 Memento 模式正是为此而生。

2. 什么是 Memento 设计模式?

Memento 是 GoF(Gang of Four)提出的一种行为型设计模式,核心用途是:✅ 实现操作的撤销(Undo)机制。

它的实现思路很直观:在某个时间点保存对象的状态,在需要时恢复。整个过程涉及三个关键角色:

  • Originator(原发器):需要保存和恢复状态的对象。
  • Memento(备忘录):用于存储 Originator 的状态快照。
  • Caretaker(管理者):负责保存 Memento,并在适当时机触发恢复。

⚠️ 关键原则:Memento 对外(尤其是 Caretaker)应尽可能隐藏内部细节,避免破坏 Originator 的封装性。只有 Originator 本身才能完全读取并恢复状态。

下图展示了三者之间的关系:

Memento Design Pattern 1

可以看到:

  • Originator 可以生成(save)和消费(restore)Memento。
  • Caretaker 只负责持有 Memento,不关心其内容。
  • Originator 的内部状态对外不可见。

📌 补充说明:

  • Memento 存储的状态字段数量不限,可以是部分状态,只要足够恢复即可。
  • 不必保存 Originator 的全部字段,按需裁剪可节省内存。

3. 何时使用 Memento 模式?

✅ 推荐使用场景:

  • 需要实现撤销、重做(Undo/Redo)功能
  • 对象状态需要快照备份(如事务回滚、游戏存档)
  • 希望解耦状态保存逻辑与业务逻辑

❌ 需谨慎使用的情况:

  • Originator 状态非常庞大(如富文本编辑器整篇文档)
  • 频繁保存会导致内存暴涨或性能下降

⚠️ 踩坑提示:如果状态太大,考虑只保存差异(diff)或采用懒加载策略,避免“备忘录爆炸”。

4. Memento 模式实战

我们以一个简易文本编辑器为例,逐步实现保存和撤销功能。

4.1 基础结构

先定义两个核心类:TextEditor(编辑器)和 TextWindow(文本窗口)。

public class TextEditor {
    private TextWindow textWindow;

    public TextEditor(TextWindow textWindow) {
        this.textWindow = textWindow;
    }
}
public class TextWindow {
    private StringBuilder currentText;

    public TextWindow() {
        this.currentText = new StringBuilder();
    }

    public void addText(String text) {
        currentText.append(text);
    }
}

此时编辑器只能添加文本,无法撤销。

4.2 定义 Memento

我们要保存文本状态,因此创建 TextWindowState 类作为 Memento:

public class TextWindowState {
    private final String text;

    public TextWindowState(String text) {
        this.text = text;
    }

    public String getText() {
        return text;
    }
}

✅ 注意点:

  • 使用 String 而非 StringBuilder,防止外部篡改(不可变性)。
  • 字段设为 final,增强安全性。

4.3 实现 Originator

TextWindow 成为 Originator,添加 save()restore() 方法:

public class TextWindow {
    private StringBuilder currentText;

    public TextWindow() {
        this.currentText = new StringBuilder();
    }

    public void addText(String text) {
        currentText.append(text);
    }

    // 生成 Memento
    public TextWindowState save() {
        return new TextWindowState(currentText.toString());
    }

    // 恢复状态
    public void restore(TextWindowState save) {
        this.currentText = new StringBuilder(save.getText());
    }
}

Originator 掌控状态的序列化与反序列化,完全自主。

4.4 实现 Caretaker

TextEditor 作为管理者,负责调用保存和恢复:

public class TextEditor {
    private final TextWindow textWindow;
    private TextWindowState savedTextWindow; // 持有 Memento

    public TextEditor(TextWindow textWindow) {
        this.textWindow = textWindow;
    }

    public void write(String text) {
        textWindow.addText(text);
    }

    // 保存当前状态
    public void hitSave() {
        savedTextWindow = textWindow.save();
    }

    // 撤销到上次保存点
    public void hitUndo() {
        if (savedTextWindow != null) {
            textWindow.restore(savedTextWindow);
        }
    }

    // 辅助方法:获取当前文本
    public String print() {
        return textWindow.getCurrentText();
    }
}

📌 补充:为方便测试,给 TextWindow 添加 getCurrentText() 方法:

public String getCurrentText() {
    return currentText.toString();
}

4.5 测试验证

来一段简单粗暴的测试,验证撤销功能是否生效:

TextEditor textEditor = new TextEditor(new TextWindow());
textEditor.write("The Memento Design Pattern\n");
textEditor.write("How to implement it in Java?\n");
textEditor.hitSave(); // 保存当前状态

textEditor.write("Buy milk and eggs before coming home\n");

textEditor.hitUndo(); // 撤销

assertThat(textEditor.print()).isEqualTo("The Memento Design Pattern\nHow to implement it in Java?\n");

✅ 输出结果符合预期:最后一行“买牛奶”已被撤销。

说明 Memento 成功保存了中间状态,并在 hitUndo() 时准确恢复。

5. 总结

Memento 模式通过分离状态保存与管理逻辑,实现了干净、安全的撤销机制。核心优势在于:

  • ✅ 封装性好:外部无法窥探或修改 Memento 内容
  • ✅ 职责清晰:Originator 负责状态,Caretaker 负责流程
  • ✅ 易扩展:支持多级撤销(只需用栈管理多个 Memento)

在实际项目中,可用于富文本编辑、配置回滚、游戏存档等场景。但也要警惕状态过大带来的内存问题。

完整代码示例已托管至 GitHub:https://github.com/eugenp/tutorials/tree/master/patterns-modules/design-patterns-behavioral-2


原始标题:Memento Design Pattern in Java