配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为 com.hanserwei.patterns.memento

系列导航:Java 25 设计模式学习指南

上一篇:中介者 · 下一篇:观察者

草稿编辑器需要“回到刚才保存的状态”。历史管理器如果直接读取和修改编辑器的全部字段,就必须了解编辑器内部结构;每增加一个字段,都要检查历史模块是否一起更新。

备忘录让编辑器自己生成快照,也由编辑器自己恢复。历史模块只保管一个不透明对象,不读取其中的文本,不参与解释恢复规则。

从问题提炼设计意图

备忘录在保持封装的前提下捕获对象内部状态,交由外部保存,并在需要时由原发器恢复。

对象职责与协作关系

示例角色 职责
Editor 原发器,知道如何保存和恢复自身状态
Editor.Snapshot 不可变备忘录,对外不开放内容访问器
History 负责人,只维护快照栈
Demo 在修改前建立检查点,再请求恢复
classDiagram
    Editor ..> Snapshot : creates and restores
    History o-- Snapshot : stores opaque snapshots
    History --> Editor : requests restore

代码思路:变化应该落在哪个对象上

Snapshot 是 Editor 内部的 static final 类,字段私有、构造器私有,而且没有 text getter。外部可以持有这个公开类型,却无法通过正常 API 读写内容;Editor 则能访问其嵌套类的私有成员。

save 把不可变 String 文本和 owner 一起保存。restore 先检查快照属于当前编辑器,避免把另一个文档的快照误恢复到此处。这个身份校验表达了本例的业务约束,不是所有备忘录都必须采用的通用字段。

History.checkpoint 在修改前保存,undo 恢复最近快照后弹栈。历史并不知道快照里有几个业务字段,因此将来 Editor 增加光标位置时,只需更新自己的快照协议,历史栈逻辑仍可保持不变。

关键实现与独立运行

配套仓库中的包名是 com.hanserwei.patterns.memento,源码目录为 src/main/java/com/hanserwei/patterns/memento/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。

Editor.java

package com.hanserwei.patterns.memento;

import java.util.Objects;

/** 创建与恢复自身快照的原发器. */
public final class Editor {
  /** 编辑器当前文本. */
  private String text = "";

  /** 替换当前文本. */
  public void write(String text) {
    this.text = Objects.requireNonNull(text, "text");
  }

  /** 返回当前文本. */
  public String text() {
    return text;
  }

  /** 捕获当前文本,并记录快照所属编辑器. */
  public Snapshot save() {
    return new Snapshot(this, text);
  }

  /** 只恢复本编辑器创建的快照,拒绝跨对象覆盖. */
  public void restore(Snapshot snapshot) {
    Objects.requireNonNull(snapshot, "snapshot");
    if (snapshot.owner != this) {
      throw new IllegalArgumentException("Snapshot belongs to another editor");
    }
    text = snapshot.text;
  }

  /** 对外不暴露文本读写接口的不可变备忘录. */
  public static final class Snapshot {
    /** 创建快照的原发器,用于验证归属. */
    private final Editor owner;

    /** 创建瞬间的不可变文本值. */
    private final String text;

    /** 仅原发器可以创建快照. */
    private Snapshot(Editor owner, String text) {
      this.owner = owner;
      this.text = text;
    }
  }
}

History.java

package com.hanserwei.patterns.memento;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;

/** 只保管快照、不了解快照内部内容的负责人. */
public final class History {
  /** 被管理的唯一编辑器. */
  private final Editor editor;

  /** 按保存顺序压栈的快照. */
  private final Deque<Editor.Snapshot> snapshots = new ArrayDeque<>();

  /** 绑定快照的原发器. */
  public History(Editor editor) {
    this.editor = Objects.requireNonNull(editor, "editor");
  }

  /** 保存当前状态;调用者应在修改前调用. */
  public void checkpoint() {
    snapshots.push(editor.save());
  }

  /** 恢复最近保存的状态,空历史返回 false. */
  public boolean undo() {
    if (snapshots.isEmpty()) {
      return false;
    }
    editor.restore(snapshots.peek());
    snapshots.pop();
    return true;
  }
}

Demo.java 展示调用方如何装配这些对象:

package com.hanserwei.patterns.memento;

/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
  /** 禁止实例化演示入口. */
  private Demo() {}

  /** 运行独立示例;args 为未使用的命令行参数. */
  public static void main(String[] args) {
    Editor editor = new Editor();
    editor.write("Draft");
    History history = new History(editor);
    history.checkpoint();
    editor.write("Published");
    history.undo();
    System.out.println(editor.text());
  }
}

在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew

./gradlew runMemento
./gradlew test --tests 'com.hanserwei.patterns.memento.PatternTest'

示例的业务输出如下,省略 Gradle 自身的任务提示:

Draft

用测试确认模式的行为

多级检查点按逆序恢复到 A 和空文本;空历史返回 false;跨编辑器快照被拒绝。

对应测试位于 src/test/java/com/hanserwei/patterns/memento/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。

常见用法

  • 编辑器、绘图软件和配置工具恢复之前状态。
  • 游戏保存点或业务流程回退点需要保留对象封装。
  • 复杂对象有多项内部状态,不适合由外部逐字段重建。

适用边界与容易踩的坑

快照必须真正独立。本例文本是不可变 String,保存引用即可;若快照保存一个不断变化的 List 引用,就可能随着编辑器一起变化,失去历史意义。复制深度仍需要逐字段设计。

历史可能持续占用内存,应根据对象体积定义保留数量、增量快照或持久化策略。当前 owner 引用还会让快照保留编辑器对象;负责人应在文档关闭时释放不再需要的历史。

本例恢复的是内存文本,不能撤销已经写入外部系统的副作用。持久化快照还需考虑版本兼容、敏感内容和失效恢复;不透明 API 也不是面对反射或恶意代码的安全边界。

与相近模式比较

命令记录要执行的意图,可以用反向操作或备忘录完成撤销;备忘录只保存状态,不知道为什么发生变化。需要“撤销哪次操作”时由命令或历史管理器组织,需要“如何恢复内部状态”时交给原发器。

动手练习

给编辑器增加光标位置,并让快照同时保存文本和光标。要求 History 不读取这两个值也能恢复。再加入最多保留十个检查点的规则,测试最老快照何时被淘汰。

系列导航:Java 25 设计模式学习指南

上一篇:中介者 · 下一篇:观察者