一、备忘录模式核心定义
备忘录模式是行为型设计模式的一种,核心目的是:
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便后续需要时能将该对象恢复到原先保存的状态。
简单来说:给对象拍 "快照",并将快照保存到外部,需要时可通过快照恢复对象状态。
核心解决的问题
- 状态备份与恢复 :无需暴露对象内部结构,即可保存 / 恢复对象状态(如编辑器撤销、游戏存档);
- 撤销 / 重做 :通过保存多个历史快照,实现多步撤销、重做操作;
- 数据隔离 :快照(备忘录)与原对象解耦,原对象修改不影响已保存的快照;
- 事务回滚 :保存操作前的状态,操作失败时恢复到原状态(如数据库事务、支付回滚)。
生活类比
- 场景 1 :文档编辑器的撤销功能
- 原发器(Originator):文档对象(包含内容、格式、光标位置等状态);
- 备忘录(Memento):文档的快照(保存某一时刻的所有状态);
- 负责人(Caretaker):撤销栈(管理所有快照,负责保存 / 获取快照);
- 核心:编辑文档时,每一步操作都保存快照到撤销栈,点击 "撤销" 时从栈中取出快照恢复文档。
- 场景 2 :游戏存档
- 原发器:游戏角色(等级、血量、装备、位置等状态);
- 备忘录:游戏存档文件(保存角色的所有状态);
- 负责人:存档管理器(管理多个存档,如自动存档、手动存档);
- 核心:玩家存档时,角色生成快照并保存为文件,读档时从文件恢复角色状态。
- 场景 3 :数据库事务
- 原发器:数据库表(数据记录状态);
- 备忘录:事务日志(保存操作前的数据状态);
- 负责人:事务管理器(管理日志,事务失败时恢复数据);
- 核心:执行更新操作前保存数据快照,事务回滚时通过快照恢复数据。
标准角色
| 角色 | 职责 | 类比(编辑器场景) | 代码定位 |
|---|---|---|---|
| 原发器(Originator) | 生成备忘录(保存当前状态)、从备忘录恢复状态,是需要保存状态的对象 | 文档对象(生成 / 恢复快照) | 业务核心对象(如Document、GameRole) |
| 备忘录(Memento) | 存储原发器的内部状态,对原发器暴露所有状态,对其他对象仅暴露有限接口 | 文档快照(保存内容 / 格式) | 不可变对象(只存状态,无业务逻辑) |
| 负责人(Caretaker) | 管理备忘录(保存 / 获取 / 删除),但不修改、不访问备忘录的内部状态 | 撤销栈(管理所有快照) | 管理器类(如UndoManager、SaveManager) |
核心 UML 类图

二、文档编辑器撤销
以 "简单文档编辑器" 为例,实现备忘录模式的核心逻辑 ------ 支持保存文档快照、撤销到上一版本,覆盖所有核心角色。
1. 步骤 1:定义备忘录(文档快照)
/**
* 备忘录:文档快照(存储文档的状态,不可变)
* 核心:只暴露给原发器获取状态的方法,不对外暴露修改方法
*/
public class DocumentMemento {
// 文档状态:内容、字体、字号
private final String content;
private final String font;
private final int fontSize;
// 构造函数:仅原发器可调用,初始化所有状态
public DocumentMemento(String content, String font, int fontSize) {
this.content = content;
this.font = font;
this.fontSize = fontSize;
}
// 仅提供getter,不提供setter(保证状态不可变)
public String getContent() {
return content;
}
public String getFont() {
return font;
}
public int getFontSize() {
return fontSize;
}
}
2. 步骤 2:定义原发器(文档对象)
/**
* 原发器:文档对象(生成快照、恢复快照)
*/
public class Document {
// 文档内部状态(需要保存的属性)
private String content;
private String font;
private int fontSize;
/**
* 生成备忘录(保存当前状态)
*/
public DocumentMemento createMemento() {
System.out.println("【原发器】生成文档快照:内容=" + content + ",字体=" + font + ",字号=" + fontSize);
// 将当前所有状态封装到备忘录
return new DocumentMemento(content, font, fontSize);
}
/**
* 从备忘录恢复状态
*/
public void restoreMemento(DocumentMemento memento) {
this.content = memento.getContent();
this.font = memento.getFont();
this.fontSize = memento.getFontSize();
System.out.println("【原发器】恢复文档快照:内容=" + content + ",字体=" + font + ",字号=" + fontSize);
}
// 业务方法:修改文档内容
public void setContent(String content) {
this.content = content;
}
// 业务方法:修改字体样式
public void setFontStyle(String font, int fontSize) {
this.font = font;
this.fontSize = fontSize;
}
// 打印当前状态
public void printState() {
System.out.println("【当前文档状态】内容:" + content + ",字体:" + font + ",字号:" + fontSize);
}
// Getter(仅用于测试)
public String getContent() {
return content;
}
public String getFont() {
return font;
}
public int getFontSize() {
return fontSize;
}
}
3. 步骤 3:定义负责人(撤销管理器)
import java.util.Stack;
/**
* 负责人:撤销管理器(管理所有备忘录,不修改/访问备忘录内部状态)
*/
public class UndoManager {
// 用栈存储备忘录(先进后出,支持撤销)
private final Stack<DocumentMemento> mementoStack = new Stack<>();
/**
* 保存备忘录(入栈)
*/
public void saveMemento(DocumentMemento memento) {
mementoStack.push(memento);
System.out.println("【负责人】保存快照,当前快照数量:" + mementoStack.size());
}
/**
* 获取最后一个备忘录(出栈,用于撤销)
*/
public DocumentMemento getLastMemento() {
if (mementoStack.isEmpty()) {
System.out.println("【负责人】无快照可恢复");
return null;
}
DocumentMemento memento = mementoStack.pop();
System.out.println("【负责人】获取最后一个快照,剩余快照数量:" + mementoStack.size());
return memento;
}
/**
* 清空所有快照
*/
public void clear() {
mementoStack.clear();
System.out.println("【负责人】清空所有快照");
}
}
4. 客户端(测试编辑器撤销功能)
/**
* 客户端:测试文档编辑器的撤销功能
*/
public class MementoClient {
public static void main(String[] args) {
// 1. 创建原发器(文档)
Document document = new Document();
// 2. 创建负责人(撤销管理器)
UndoManager undoManager = new UndoManager();
// 3. 第一次编辑:初始化文档
document.setContent("Hello World!");
document.setFontStyle("宋体", 12);
undoManager.saveMemento(document.createMemento()); // 保存快照1
document.printState();
// 4. 第二次编辑:修改内容
document.setContent("Hello 备忘录模式!");
undoManager.saveMemento(document.createMemento()); // 保存快照2
document.printState();
// 5. 第三次编辑:修改字体
document.setFontStyle("微软雅黑", 14);
undoManager.saveMemento(document.createMemento()); // 保存快照3
document.printState();
// 6. 第一次撤销:恢复到修改字体前
System.out.println("\n======= 第一次撤销 =======");
DocumentMemento m1 = undoManager.getLastMemento();
if (m1 != null) {
document.restoreMemento(m1);
document.printState();
}
// 7. 第二次撤销:恢复到修改内容前
System.out.println("\n======= 第二次撤销 =======");
DocumentMemento m2 = undoManager.getLastMemento();
if (m2 != null) {
document.restoreMemento(m2);
document.printState();
}
// 8. 第三次撤销:无快照
System.out.println("\n======= 第三次撤销 =======");
DocumentMemento m3 = undoManager.getLastMemento();
if (m3 != null) {
document.restoreMemento(m3);
}
}
}
输出结果
【原发器】生成文档快照:内容=Hello World!,字体=宋体,字号=12
【负责人】保存快照,当前快照数量:1
【当前文档状态】内容:Hello World!,字体:宋体,字号:12
【原发器】生成文档快照:内容=Hello 备忘录模式!,字体=宋体,字号=12
【负责人】保存快照,当前快照数量:2
【当前文档状态】内容:Hello 备忘录模式!,字体:宋体,字号:12
【原发器】生成文档快照:内容=Hello 备忘录模式!,字体=微软雅黑,字号=14
【负责人】保存快照,当前快照数量:3
【当前文档状态】内容:Hello 备忘录模式!,字体:微软雅黑,字号:14
======= 第一次撤销 =======
【负责人】获取最后一个快照,剩余快照数量:2
【原发器】恢复文档快照:内容=Hello 备忘录模式!,字体=宋体,字号=12
【当前文档状态】内容:Hello 备忘录模式!,字体:宋体,字号:12
======= 第二次撤销 =======
【负责人】获取最后一个快照,剩余快照数量:1
【原发器】恢复文档快照:内容=Hello World!,字体=宋体,字号=12
【当前文档状态】内容:Hello World!,字体:宋体,字号:12
======= 第三次撤销 =======
【负责人】获取最后一个快照,剩余快照数量:0
【原发器】恢复文档快照:内容=null,字体=null,字号=0
【当前文档状态】内容:null,字体:null,字号:0
三、Spring 实战版(订单状态快照 + 回滚)
在业务开发中,备忘录模式最核心的实战场景是订单状态快照与回滚(如支付失败回滚订单状态、订单取消恢复库存)。以下实现一个 "订单状态快照管理器",支持保存订单不同阶段的状态,失败时恢复到指定状态。
1. 依赖准备(Spring Boot)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>3.2.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2. 核心模型定义
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 订单状态枚举
*/
public enum OrderStatus {
CREATE("已创建"),
PAY("已支付"),
SHIP("已发货"),
RECEIVE("已收货"),
CANCEL("已取消");
private final String desc;
OrderStatus(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
}
/**
* 备忘录:订单状态快照(不可变)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderMemento {
private String orderId; // 订单ID
private OrderStatus status; // 订单状态
private double amount; // 订单金额
private String createTime; // 快照创建时间
private String operator; // 操作人
}
/**
* 原发器:订单对象
*/
@Data
public class Order {
private String orderId;
private OrderStatus status;
private double amount;
private String userId;
private String createTime;
/**
* 生成订单状态快照
* @param operator 操作人
* @return 订单快照
*/
public OrderMemento createMemento(String operator) {
System.out.println("【原发器-订单】生成快照 | 订单ID:" + orderId + ",状态:" + status.getDesc());
return new OrderMemento(
orderId,
status,
amount,
createTime,
operator
);
}
/**
* 从快照恢复订单状态
*/
public void restoreMemento(OrderMemento memento) {
this.status = memento.getStatus();
System.out.println("【原发器-订单】恢复快照 | 订单ID:" + orderId + ",恢复后状态:" + status.getDesc());
}
/**
* 业务方法:更新订单状态
*/
public void updateStatus(OrderStatus status) {
this.status = status;
System.out.println("【原发器-订单】更新状态 | 订单ID:" + orderId + ",新状态:" + status.getDesc());
}
}
3. 负责人(订单快照管理器,Spring Bean)
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
/**
* 负责人:订单快照管理器(Spring Bean,管理所有订单的快照)
* 核心:按订单ID分类存储快照,支持按状态恢复
*/
@Slf4j
@Component
public class OrderMementoManager {
// 存储结构:订单ID → 快照队列(按时间顺序存储)
private final Map<String, Queue<OrderMemento>> orderMementoMap = new HashMap<>();
/**
* 保存订单快照
*/
public void saveMemento(String orderId, OrderMemento memento) {
// 初始化订单的快照队列
orderMementoMap.computeIfAbsent(orderId, k -> new LinkedList<>());
// 添加快照到队列
orderMementoMap.get(orderId).offer(memento);
log.info("【负责人-快照管理器】保存订单快照 | 订单ID:{},当前快照数:{}",
orderId, orderMementoMap.get(orderId).size());
}
/**
* 根据状态恢复订单(找到指定状态的快照并恢复)
*/
public OrderMemento restoreByStatus(String orderId, OrderStatus targetStatus) {
Queue<OrderMemento> mementos = orderMementoMap.get(orderId);
if (mementos == null || mementos.isEmpty()) {
log.error("【负责人-快照管理器】订单无快照可恢复 | 订单ID:{}", orderId);
return null;
}
// 遍历快照队列,找到指定状态的快照
for (OrderMemento memento : mementos) {
if (memento.getStatus() == targetStatus) {
log.info("【负责人-快照管理器】找到指定状态快照 | 订单ID:{},状态:{}",
orderId, targetStatus.getDesc());
return memento;
}
}
log.error("【负责人-快照管理器】未找到指定状态快照 | 订单ID:{},目标状态:{}",
orderId, targetStatus.getDesc());
return null;
}
/**
* 获取订单最后一次快照(用于撤销最后一步操作)
*/
public OrderMemento getLastMemento(String orderId) {
Queue<OrderMemento> mementos = orderMementoMap.get(orderId);
if (mementos == null || mementos.isEmpty()) {
log.error("【负责人-快照管理器】订单无快照可恢复 | 订单ID:{}", orderId);
return null;
}
// 队列尾部是最后一次快照
OrderMemento lastMemento = ((LinkedList<OrderMemento>) mementos).getLast();
log.info("【负责人-快照管理器】获取最后一次快照 | 订单ID:{},状态:{}",
orderId, lastMemento.getStatus().getDesc());
return lastMemento;
}
/**
* 清空订单快照
*/
public void clearMementos(String orderId) {
orderMementoMap.remove(orderId);
log.info("【负责人-快照管理器】清空订单快照 | 订单ID:{}", orderId);
}
}
4. 订单服务(业务逻辑)
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 订单服务(业务入口,整合原发器和负责人)
*/
@Service
public class OrderService {
@Resource
private OrderMementoManager mementoManager;
/**
* 创建订单并保存初始快照
*/
public Order createOrder(String orderId, String userId, double amount) {
Order order = new Order();
order.setOrderId(orderId);
order.setUserId(userId);
order.setAmount(amount);
order.setStatus(OrderStatus.CREATE);
order.setCreateTime(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
// 保存初始快照
OrderMemento memento = order.createMemento("system");
mementoManager.saveMemento(orderId, memento);
return order;
}
/**
* 支付订单(失败则回滚到创建状态)
*/
public boolean payOrder(Order order, String operator) {
// 保存支付前的快照
mementoManager.saveMemento(order.getOrderId(), order.createMemento(operator));
try {
// 模拟支付逻辑:金额>1000时支付失败
if (order.getAmount() > 1000) {
throw new RuntimeException("支付金额超限,单笔支付最大1000元");
}
// 支付成功,更新状态
order.updateStatus(OrderStatus.PAY);
// 保存支付后的快照
mementoManager.saveMemento(order.getOrderId(), order.createMemento(operator));
return true;
} catch (Exception e) {
log.error("【订单服务】支付失败 | 订单ID:{},原因:{}", order.getOrderId(), e.getMessage());
// 恢复到创建状态
OrderMemento createMemento = mementoManager.restoreByStatus(order.getOrderId(), OrderStatus.CREATE);
if (createMemento != null) {
order.restoreMemento(createMemento);
}
return false;
}
}
/**
* 取消订单(恢复到创建状态)
*/
public void cancelOrder(Order order, String operator) {
// 保存取消前的快照
mementoManager.saveMemento(order.getOrderId(), order.createMemento(operator));
// 恢复到创建状态
OrderMemento createMemento = mementoManager.restoreByStatus(order.getOrderId(), OrderStatus.CREATE);
if (createMemento != null) {
order.restoreMemento(createMemento);
}
// 更新为取消状态
order.updateStatus(OrderStatus.CANCEL);
}
}
5. 客户端(Spring Boot 测试)
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
/**
* 客户端:测试订单快照与回滚
*/
@SpringBootApplication
public class SpringMementoDemoApplication {
public static void main(String[] args) {
// 1. 启动Spring容器
ConfigurableApplicationContext context = SpringApplication.run(SpringMementoDemoApplication.class, args);
OrderService orderService = context.getBean(OrderService.class);
// 2. 测试1:正常支付(金额<1000)
System.out.println("======= 测试1:正常支付 =======");
Order order1 = orderService.createOrder("O20240312001", "U1001", 800.0);
boolean payResult1 = orderService.payOrder(order1, "U1001");
System.out.println("支付结果:" + payResult1 + ",订单最终状态:" + order1.getStatus().getDesc());
// 3. 测试2:支付失败(金额>1000),自动回滚
System.out.println("\n======= 测试2:支付失败回滚 =======");
Order order2 = orderService.createOrder("O20240312002", "U1002", 1200.0);
boolean payResult2 = orderService.payOrder(order2, "U1002");
System.out.println("支付结果:" + payResult2 + ",订单最终状态:" + order2.getStatus().getDesc());
// 4. 测试3:取消订单(恢复到创建状态后标记为取消)
System.out.println("\n======= 测试3:取消订单 =======");
orderService.cancelOrder(order1, "U1001");
System.out.println("订单最终状态:" + order1.getStatus().getDesc());
context.close();
}
}
输出结果
======= 测试1:正常支付 =======
【原发器-订单】生成快照 | 订单ID:O20240312001,状态:已创建
【负责人-快照管理器】保存订单快照 | 订单ID:O20240312001,当前快照数:1
【原发器-订单】生成快照 | 订单ID:O20240312001,状态:已创建
【负责人-快照管理器】保存订单快照 | 订单ID:O20240312001,当前快照数:2
【原发器-订单】更新状态 | 订单ID:O20240312001,新状态:已支付
【原发器-订单】生成快照 | 订单ID:O20240312001,状态:已支付
【负责人-快照管理器】保存订单快照 | 订单ID:O20240312001,当前快照数:3
支付结果:true,订单最终状态:已支付
======= 测试2:支付失败回滚 =======
【原发器-订单】生成快照 | 订单ID:O20240312002,状态:已创建
【负责人-快照管理器】保存订单快照 | 订单ID:O20240312002,当前快照数:1
【原发器-订单】生成快照 | 订单ID:O20240312002,状态:已创建
【负责人-快照管理器】保存订单快照 | 订单ID:O20240312002,当前快照数:2
【订单服务】支付失败 | 订单ID:O20240312002,原因:支付金额超限,单笔支付最大1000元
【负责人-快照管理器】找到指定状态快照 | 订单ID:O20240312002,状态:已创建
【原发器-订单】恢复快照 | 订单ID:O20240312002,恢复后状态:已创建
支付结果:false,订单最终状态:已创建
======= 测试3:取消订单 =======
【原发器-订单】生成快照 | 订单ID:O20240312001,状态:已支付
【负责人-快照管理器】保存订单快照 | 订单ID:O20240312001,当前快照数:4
【负责人-快照管理器】找到指定状态快照 | 订单ID:O20240312001,状态:已创建
【原发器-订单】恢复快照 | 订单ID:O20240312001,恢复后状态:已创建
【原发器-订单】更新状态 | 订单ID:O20240312001,新状态:已取消
订单最终状态:已取消
四、备忘录模式的核心特点与适用场景
优点
- 封装性好:原发器的内部状态对外部隐藏,仅通过备忘录保存 / 恢复,符合迪米特法则;
- 状态可回溯:支持多步撤销、重做,可恢复到任意历史状态;
- 解耦状态管理:原发器只负责生成 / 恢复状态,状态的存储、管理由负责人完成;
- 支持事务回滚:操作失败时可快速恢复到操作前的状态,保证数据一致性;
- 快照不可变:备忘录通常设计为不可变对象,避免状态被意外修改。
缺点
- 内存开销大:如果原发器状态复杂、快照数量多,会占用大量内存(如编辑器的每一步操作都保存快照);
- 性能损耗:生成 / 恢复快照需要序列化 / 反序列化状态,频繁操作会影响性能;
- 状态同步问题:如果原发器的状态结构修改,历史快照可能无法兼容;
- 负责人无法优化快照:负责人只能管理快照,无法修改 / 压缩快照内容(如需优化需修改原发器)。
适用场景
- 撤销 / 重做功能:文档编辑器、代码编辑器、图形设计工具;
- 状态备份与恢复:游戏存档、系统配置备份、数据库备份;
- 事务回滚:数据库事务、支付流程、订单状态变更;
- 快照监控:系统运行状态快照、性能监控快照、日志快照;
- 原型模式结合:保存对象初始状态,快速复制多个相同状态的对象。
五、JDK/ Spring 中的原生应用(必须知道)
备忘录模式在框架中主要用于状态保存与恢复,以下是核心场景:
1. Java 序列化(Serializable)
- 原发器:实现Serializable的对象;
- 备忘录:序列化后的字节流 / 文件;
- 负责人:序列化工具类(如ObjectOutputStream);
- 核心逻辑:通过序列化保存对象状态,反序列化恢复状态(本质是备忘录模式的实现)。
2. Spring 事务管理(TransactionManager)
- 原发器:数据库连接 / 数据记录;
- 备忘录:事务日志(undo log/redo log);
- 负责人:PlatformTransactionManager;
- 核心逻辑:事务提交前保存数据快照,回滚时通过日志恢复数据。
3. Spring StateMachine(状态机)
- 原发器:状态机上下文(StateContext);
- 备忘录:状态快照(StateMachineAccess);
- 负责人:状态机管理器(StateMachineManager);
- 核心逻辑:保存状态机的历史状态,支持状态回滚、状态恢复。
4. Redis 持久化(RDB/AOF)
- 原发器:Redis 内存数据;
- 备忘录:RDB 快照文件 / AOF 日志;
- 负责人:Redis 持久化管理器;
- 核心逻辑:定期生成数据快照(RDB)或记录操作日志(AOF),重启时恢复数据。
5. 浏览器历史记录
- 原发器:浏览器页面(URL、滚动位置、表单数据);
- 备忘录:历史记录条目;
- 负责人:浏览器历史记录管理器;
- 核心逻辑:保存页面状态快照,支持前进 / 后退(撤销 / 重做)。
六、备忘录模式 vs 原型模式(易混淆点)
| 维度 | 备忘录模式 | 原型模式 |
|---|---|---|
| 核心目的 | 保存 / 恢复对象历史状态 | 复制对象当前状态,创建新对象 |
| 状态时效性 | 关注历史状态(过去的某个时刻) | 关注当前状态(复制时的状态) |
| 存储方式 | 外部存储(栈 / 队列 / 文件) | 内存复制(新对象) |
| 核心操作 | 保存(createMemento)、恢复(restore) | 克隆(clone) |
| 典型场景 | 撤销 / 重做、事务回滚、快照恢复 | 对象复制、原型实例、批量创建 |
总结
- 备忘录模式的核心是在不暴露对象内部结构的前提下,保存 / 恢复对象的历史状态,核心角色包括原发器(生成 / 恢复快照)、备忘录(存储状态)、负责人(管理快照);
- 备忘录需设计为不可变对象,避免状态被意外修改;负责人只管理快照,不访问 / 修改快照内部状态;
- 业务开发中,备忘录模式最实用的场景是订单状态回滚、支付事务回滚、编辑器撤销,需注意控制快照数量以减少内存开销;
- 备忘录模式常与序列化、事务管理结合使用,是实现数据一致性、状态可回溯的核心模式。
