备忘录模式:设计与实践
一、什么是备忘录模式
1. 基本定义
备忘录模式(Memento Pattern)是一种行为型设计模式,由《设计模式:可复用面向对象软件的基础》(GOF著作)定义为:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
该模式通过引入备忘录对象存储原发器(Originator)的内部状态,当需要回溯时,由原发器从备忘录中恢复状态,同时确保状态的访问权限被严格控制(只有原发器可访问备忘录中的状态)。核心是实现“状态的安全存储与回溯”,同时避免暴露对象的内部结构。
2. 核心思想
备忘录模式的核心在于状态捕获与安全恢复。当对象的状态需要被记录和回溯(如撤销操作、版本回滚)时,直接暴露对象的内部状态会破坏封装性,而备忘录模式通过以下方式解决:
- 原发器负责创建备忘录(存储当前状态)和从备忘录恢复状态;
- 备忘录仅允许原发器访问其内部状态,其他对象无法修改;
- 负责人(Caretaker)负责管理备忘录的生命周期(存储、获取),但不参与状态的读写。
这种设计既保证了状态的可追溯性,又维护了对象的封装性,是处理“状态回溯”场景的最佳实践。
二、备忘录模式的特点
1. 状态封装
备忘录封装了原发器的内部状态,仅允许原发器访问,其他对象无法直接修改,确保状态安全性。
2. 不破坏封装性
原发器无需暴露内部状态的getter/setter方法,通过备忘录间接实现状态的存储和恢复,符合封装原则。
3. 状态快照
备忘录本质是对象在某一时刻的状态快照,可用于在未来任意时间点恢复到该状态。
4. 可回溯性
支持多次状态存储,通过负责人管理的备忘录列表,可实现多版本回溯(如撤销多步操作)。
5. 职责分离
- 原发器:负责业务逻辑和状态管理;
- 备忘录:负责状态存储;
- 负责人:负责备忘录的管理; 三者职责清晰,符合单一职责原则。
| 特点 | 说明 |
|---|---|
| 状态封装 | 备忘录仅允许原发器访问状态,确保安全 |
| 不破坏封装性 | 原发器无需暴露内部状态的访问接口 |
| 状态快照 | 记录对象某一时刻的完整状态 |
| 可回溯性 | 支持基于备忘录恢复到历史状态 |
| 职责分离 | 原发器、备忘录、负责人各司其职 |
三、备忘录模式的标准代码实现
1. 模式结构
备忘录模式包含三个核心角色:
- 原发器(Originator):需要被记录状态的对象,提供创建备忘录(
createMemento)和从备忘录恢复状态(restoreFromMemento)的方法。 - 备忘录(Memento):存储原发器的内部状态,仅允许原发器访问其状态数据。
- 负责人(Caretaker):管理备忘录的存储和获取,不直接访问备忘录的内部状态。
2. 代码实现示例
2.1 原发器与备忘录
import java.util.Date;
/**
* 原发器:需要保存状态的对象
*/
public class Originator {
// 内部状态(可能包含多个属性)
private String state;
private Date lastModified;
public Originator(String initialState) {
this.state = initialState;
this.lastModified = new Date();
}
/**
* 业务操作:修改状态
*/
public void updateState(String newState) {
this.state = newState;
this.lastModified = new Date();
}
/**
* 创建备忘录(保存当前状态)
*/
public Memento createMemento() {
return new Memento(this.state, this.lastModified);
}
/**
* 从备忘录恢复状态
*/
public void restoreFromMemento(Memento memento) {
this.state = memento.getState();
this.lastModified = memento.getLastModified();
}
// 辅助方法:展示当前状态
public String showState() {
return String.format("当前状态:%s,最后修改时间:%s", state, lastModified);
}
/**
* 备忘录:私有内部类,确保只有Originator可访问
*/
public class Memento {
private final String state;
private final Date lastModified;
// 构造方法私有,仅允许Originator创建
private Memento(String state, Date lastModified) {
this.state = state;
this.lastModified = new Date(lastModified.getTime()); // 深拷贝,避免外部修改
}
// 状态获取方法仅允许Originator访问(通过私有访问控制)
private String getState() {
return state;
}
private Date getLastModified() {
return new Date(lastModified.getTime()); // 返回副本,防止外部修改
}
}
}
2.2 负责人
import java.util.ArrayList;
import java.util.List;
/**
* 负责人:管理备忘录
*/
public class Caretaker {
// 存储备忘录的列表(支持多版本回溯)
private List<Originator.Memento> mementos = new ArrayList<>();
/**
* 保存备忘录
*/
public void saveMemento(Originator.Memento memento) {
mementos.add(memento);
}
/**
* 获取指定索引的备忘录
*/
public Originator.Memento getMemento(int index) {
if (index >= 0 && index < mementos.size()) {
return mementos.get(index);
}
throw new IndexOutOfBoundsException("无效的备忘录索引");
}
/**
* 获取最近的备忘录
*/
public Originator.Memento getLatestMemento() {
if (mementos.isEmpty()) {
throw new IllegalStateException("没有保存的备忘录");
}
return mementos.get(mementos.size() - 1);
}
}
2.3 客户端使用示例
/**
* 客户端:使用备忘录模式
*/
public class Client {
public static void main(String[] args) {
// 1. 创建原发器并初始化状态
Originator originator = new Originator("初始状态");
Caretaker caretaker = new Caretaker();
// 2. 保存初始状态
caretaker.saveMemento(originator.createMemento());
System.out.println("初始状态:" + originator.showState());
// 3. 修改状态并保存快照
originator.updateState("第一次修改后的状态");
caretaker.saveMemento(originator.createMemento());
System.out.println("第一次修改后:" + originator.showState());
// 4. 再次修改状态
originator.updateState("第二次修改后的状态(错误状态)");
System.out.println("第二次修改后:" + originator.showState());
// 5. 恢复到最近的正确状态(第一次修改后)
originator.restoreFromMemento(caretaker.getLatestMemento());
System.out.println("恢复后:" + originator.showState());
// 6. 恢复到初始状态
originator.restoreFromMemento(caretaker.getMemento(0));
System.out.println("恢复到初始状态:" + originator.showState());
}
}
3. 代码实现特点总结
| 角色 | 核心职责 | 代码特点 |
|---|---|---|
| 原发器(Originator) | 管理内部状态,创建和恢复备忘录 | 包含状态属性,提供createMemento()和restoreFromMemento()方法,备忘录通常为其私有内部类 |
| 备忘录(Memento) | 存储原发器的状态 | 构造方法和状态获取方法私有,仅允许原发器访问,通过深拷贝避免状态被外部修改 |
| 负责人(Caretaker) | 管理备忘录的存储和获取 | 包含备忘录集合,提供saveMemento()和getMemento()方法,不访问备忘录的内部状态 |
四、支付框架设计中备忘录模式的运用
以风控规则配置的版本快照实现为例,说明备忘录模式在支付系统中的具体应用:
1. 场景分析
支付系统的风控规则(如交易限额、黑名单、风险评分模型)需要频繁调整以应对新型风险,但调整后可能出现误判(如过度拦截正常交易),因此需要:
- 每次规则调整后保存快照(版本)
- 支持回滚到历史版本(如回滚到上周的有效配置)
- 记录版本变更日志,便于审计和问题追溯
风控规则的状态包括:规则表达式(如amount > 10000 && userLevel < 3)、生效时间、创建人、关联策略组等。使用备忘录模式可安全存储这些状态,确保回滚操作不破坏规则的完整性和安全性。
2. 设计实现
2.1 风控规则与备忘录
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
/**
* 原发器:风控规则集
*/
public class RiskRuleSet {
private String ruleSetId; // 规则集ID
private String name; // 规则集名称
private List<String> ruleExpressions; // 规则表达式列表(如["amount>10000", "userLevel<3"])
private List<String> strategyGroups; // 关联的策略组
private LocalDateTime effectiveTime; // 生效时间
private String createdBy; // 创建人
private LocalDateTime lastModifiedTime; // 最后修改时间
public RiskRuleSet(String name, String createdBy) {
this.ruleSetId = "RULE_SET_" + UUID.randomUUID().toString().substring(0, 8);
this.name = name;
this.createdBy = createdBy;
this.lastModifiedTime = LocalDateTime.now();
}
/**
* 新增规则表达式
*/
public void addRuleExpression(String expression) {
this.ruleExpressions.add(expression);
this.lastModifiedTime = LocalDateTime.now();
}
/**
* 关联策略组
*/
public void addStrategyGroup(String groupId) {
this.strategyGroups.add(groupId);
this.lastModifiedTime = LocalDateTime.now();
}
/**
* 设置生效时间
*/
public void setEffectiveTime(LocalDateTime time) {
this.effectiveTime = time;
this.lastModifiedTime = LocalDateTime.now();
}
/**
* 创建备忘录(保存当前规则集状态)
*/
public RuleSetMemento createMemento(String versionDesc) {
return new RuleSetMemento(versionDesc);
}
/**
* 从备忘录恢复规则集状态
*/
public void restoreFromMemento(RuleSetMemento memento) {
this.ruleExpressions = memento.getRuleExpressions();
this.strategyGroups = memento.getStrategyGroups();
this.effectiveTime = memento.getEffectiveTime();
this.lastModifiedTime = LocalDateTime.now(); // 恢复操作也算修改
}
/**
* 备忘录:风控规则集快照
*/
public class RuleSetMemento {
private final String mementoId; // 快照ID
private final String ruleSetId; // 关联的规则集ID
private final String versionDesc; // 版本描述(如"调整大额交易规则")
private final List<String> ruleExpressions; // 规则表达式快照(深拷贝)
private final List<String> strategyGroups; // 策略组快照(深拷贝)
private final LocalDateTime effectiveTime; // 生效时间
private final LocalDateTime snapshotTime; // 快照创建时间
private final String createdBy; // 快照创建人
private RuleSetMemento(String versionDesc) {
this.mementoId = "SNAP_" + UUID.randomUUID().toString().substring(0, 8);
this.ruleSetId = RiskRuleSet.this.ruleSetId;
this.versionDesc = versionDesc;
// 深拷贝集合,避免外部修改原始数据
this.ruleExpressions = List.copyOf(RiskRuleSet.this.ruleExpressions);
this.strategyGroups = List.copyOf(RiskRuleSet.this.strategyGroups);
this.effectiveTime = RiskRuleSet.this.effectiveTime;
this.snapshotTime = LocalDateTime.now();
this.createdBy = RiskRuleSet.this.createdBy;
}
// 仅允许RiskRuleSet访问的状态获取方法
private List<String> getRuleExpressions() {
return ruleExpressions; // 返回不可变集合
}
private List<String> getStrategyGroups() {
return strategyGroups;
}
private LocalDateTime getEffectiveTime() {
return effectiveTime;
}
// 提供只读的版本信息(供负责人记录日志)
public String getMementoId() {
return mementoId;
}
public String getVersionDesc() {
return versionDesc;
}
public LocalDateTime getSnapshotTime() {
return snapshotTime;
}
}
// getter方法(仅暴露必要信息,不返回内部状态的可修改引用)
public String getRuleSetId() {
return ruleSetId;
}
public String getName() {
return name;
}
}
2.2 规则版本管理器(负责人)
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 负责人:风控规则版本管理器
*/
public class RuleVersionManager {
// 按规则集ID存储备忘录(支持多规则集的版本管理)
private Map<String, List<RiskRuleSet.RuleSetMemento>> versionMap = new ConcurrentHashMap<>();
/**
* 保存规则集快照(创建新版本)
*/
public void saveVersion(RiskRuleSet ruleSet, String versionDesc) {
RiskRuleSet.RuleSetMemento memento = ruleSet.createMemento(versionDesc);
// 按规则集ID分组存储
versionMap.computeIfAbsent(ruleSet.getRuleSetId(), k -> new ArrayList<>())
.add(memento);
// 记录版本日志(实际中可持久化到数据库)
logVersionChange(ruleSet, memento);
}
/**
* 获取规则集的所有版本
*/
public List<RiskRuleSet.RuleSetMemento> getVersions(String ruleSetId) {
return versionMap.getOrDefault(ruleSetId, new ArrayList<>());
}
/**
* 回滚到指定版本
*/
public void rollbackToVersion(RiskRuleSet ruleSet, int versionIndex) {
List<RiskRuleSet.RuleSetMemento> versions = versionMap.get(ruleSet.getRuleSetId());
if (versions == null || versionIndex < 0 || versionIndex >= versions.size()) {
throw new IllegalArgumentException("无效的版本索引");
}
RiskRuleSet.RuleSetMemento targetMemento = versions.get(versionIndex);
ruleSet.restoreFromMemento(targetMemento);
// 回滚后创建新的版本快照(记录回滚操作)
saveVersion(ruleSet, "回滚到版本" + versionIndex + ":" + targetMemento.getVersionDesc());
}
/**
* 获取最新版本
*/
public RiskRuleSet.RuleSetMemento getLatestVersion(String ruleSetId) {
List<RiskRuleSet.RuleSetMemento> versions = versionMap.get(ruleSetId);
if (versions == null || versions.isEmpty()) {
return null;
}
return versions.get(versions.size() - 1);
}
/**
* 记录版本变更日志
*/
private void logVersionChange(RiskRuleSet ruleSet, RiskRuleSet.RuleSetMemento memento) {
System.out.printf("[版本日志] 规则集[%s-%s]创建新版本:%s,描述:%s,时间:%s%n",
ruleSet.getRuleSetId(), ruleSet.getName(),
memento.getMementoId(), memento.getVersionDesc(),
memento.getSnapshotTime());
}
}
2.3 客户端使用(风控规则管理)
import java.time.LocalDateTime;
/**
* 风控规则管理服务(客户端)
*/
public class RiskRuleService {
private RuleVersionManager versionManager = new RuleVersionManager();
public void manageRuleSet() {
// 1. 创建初始规则集
RiskRuleSet ruleSet = new RiskRuleSet("大额交易风控规则", "admin");
ruleSet.addRuleExpression("amount > 10000"); // 金额大于10000
ruleSet.addRuleExpression("userLevel < 3"); // 用户等级小于3
ruleSet.addStrategyGroup("STRATEGY_HIGH_RISK"); // 关联高风险策略组
ruleSet.setEffectiveTime(LocalDateTime.now());
// 2. 保存初始版本(V1)
versionManager.saveVersion(ruleSet, "初始版本:拦截大额低等级用户交易");
System.out.println("创建初始规则集:" + ruleSet.getName() + ",ID:" + ruleSet.getRuleSetId());
// 3. 修改规则(添加新条件)
ruleSet.addRuleExpression("isNewUser == true"); // 新增:新用户
versionManager.saveVersion(ruleSet, "V2:新增新用户拦截条件");
System.out.println("已添加新用户拦截条件");
// 4. 再次修改(误操作:添加错误条件)
ruleSet.addRuleExpression("amount < 0"); // 错误条件:金额小于0(永远为false)
versionManager.saveVersion(ruleSet, "V3:添加金额小于0的错误条件(误操作)");
System.out.println("已添加错误条件(金额小于0)");
// 5. 发现问题,回滚到V2版本
System.out.println("\n=== 开始回滚到V2版本 ===");
versionManager.rollbackToVersion(ruleSet, 1); // 索引1对应V2
System.out.println("回滚完成,当前规则集的规则表达式:" + ruleSet.ruleExpressions);
// 6. 查看版本历史
System.out.println("\n=== 版本历史 ===");
versionManager.getVersions(ruleSet.getRuleSetId()).forEach(memento ->
System.out.printf("版本:%s,描述:%s,时间:%s%n",
memento.getMementoId(), memento.getVersionDesc(), memento.getSnapshotTime()));
}
public static void main(String[] args) {
new RiskRuleService().manageRuleSet();
}
}
3. 模式价值体现
- 安全的状态存储:备忘录通过私有访问控制确保只有
RiskRuleSet可修改规则状态,避免外部篡改导致的规则失效。 - 可靠的版本回滚:支持精确回滚到任意历史版本,解决规则调整后的误判问题,保障支付系统的稳定性。
- 完整的审计跟踪:版本管理器记录所有快照的创建时间、描述和操作人,满足支付行业的合规审计要求。
- 低耦合的扩展:新增规则属性(如风险评分阈值)时,只需修改
RiskRuleSet和备忘录的状态存储逻辑,版本管理逻辑(RuleVersionManager)无需改动。 - 高效的快照管理:备忘录仅存储规则的核心状态(而非完整对象),结合负责人的分组存储,节省存储空间。
五、开源框架中备忘录模式的运用
以Eclipse的撤销/重做(Undo/Redo)机制为例,说明备忘录模式在开源框架中的典型应用:
1. 核心实现分析
Eclipse作为Java开发IDE,其编辑区的撤销/重做功能依赖备忘录模式,每次编辑操作(如输入文字、删除代码、格式化)都会创建快照,支持多级撤销。
1.1 核心角色对应
- 原发器(Originator):
TextEditor(文本编辑器),维护当前文档的内容、光标位置、选区等状态。 - 备忘录(Memento):
IUndoableOperation的实现类(如DocumentCommand),存储编辑操作执行前的文档状态。 - 负责人(Caretaker):
UndoManager,管理撤销栈(undoStack)和重做栈(redoStack),控制操作的撤销与重做。
1.2 核心代码简化实现
/**
* 原发器:文本编辑器
*/
public class TextEditor {
private Document document; // 文档内容
private int cursorPosition; // 光标位置
/**
* 执行编辑操作(如插入文本)
*/
public IUndoableOperation insertText(String text, int position) {
// 保存操作前的状态(创建备忘录)
IUndoableOperation operation = new InsertOperation(
document.getContent(), cursorPosition, text, position);
// 执行实际插入
document.insert(text, position);
this.cursorPosition = position + text.length();
return operation;
}
/**
* 从备忘录恢复状态(撤销操作)
*/
public void undo(IUndoableOperation operation) {
if (operation instanceof InsertOperation) {
InsertOperation insertOp = (InsertOperation) operation;
// 恢复文档内容
document.setContent(insertOp.getOriginalContent());
// 恢复光标位置
this.cursorPosition = insertOp.getOriginalCursorPosition();
}
}
/**
* 重做操作
*/
public void redo(IUndoableOperation operation) {
if (operation instanceof InsertOperation) {
InsertOperation insertOp = (InsertOperation) operation;
// 重新执行插入
document.insert(insertOp.getText(), insertOp.getPosition());
this.cursorPosition = insertOp.getPosition() + insertOp.getText().length();
}
}
}
/**
* 备忘录:撤销操作接口
*/
public interface IUndoableOperation {
void undo();
void redo();
}
/**
* 具体备忘录:插入操作的快照
*/
public class InsertOperation implements IUndoableOperation {
private final String originalContent; // 操作前的文档内容
private final int originalCursorPosition; // 操作前的光标位置
private final String text; // 插入的文本
private final int position; // 插入位置
public InsertOperation(String originalContent, int originalCursorPosition,
String text, int position) {
this.originalContent = originalContent;
this.originalCursorPosition = originalCursorPosition;
this.text = text;
this.position = position;
}
@Override
public void undo() {
// 由原发器(TextEditor)实现实际撤销逻辑
}
@Override
public void redo() {
// 由原发器(TextEditor)实现实际重做逻辑
}
// getter方法(仅允许原发器访问)
String getOriginalContent() { return originalContent; }
int getOriginalCursorPosition() { return originalCursorPosition; }
String getText() { return text; }
int getPosition() { return position; }
}
/**
* 负责人:撤销管理器
*/
public class UndoManager {
private Stack<IUndoableOperation> undoStack = new Stack<>();
private Stack<IUndoableOperation> redoStack = new Stack<>();
private TextEditor editor;
public UndoManager(TextEditor editor) {
this.editor = editor;
}
/**
* 记录操作(保存备忘录)
*/
public void addOperation(IUndoableOperation operation) {
undoStack.push(operation);
redoStack.clear(); // 新操作会清除重做栈
}
/**
* 执行撤销
*/
public void undo() {
if (!undoStack.isEmpty()) {
IUndoableOperation operation = undoStack.pop();
editor.undo(operation);
redoStack.push(operation);
}
}
/**
* 执行重做
*/
public void redo() {
if (!redoStack.isEmpty()) {
IUndoableOperation operation = redoStack.pop();
editor.redo(operation);
undoStack.push(operation);
}
}
}
2. 备忘录模式在Eclipse中的价值
- 细粒度的状态控制:每次编辑操作都创建独立快照,支持精确到字符级的撤销/重做,提升编辑体验。
- 封装性保障:文档的内部状态(如字符数组、光标偏移量)通过备忘录私有存储,外部无法直接修改,确保编辑操作的安全性。
- 高效的内存管理:Eclipse的
UndoManager支持设置最大快照数量,自动清理旧快照,避免内存溢出。 - 扩展灵活性:新增编辑操作(如代码格式化、重构)时,只需实现
IUndoableOperation接口,无需修改撤销管理器,符合开闭原则。
六、总结
1. 备忘录模式的适用场景
- 当需要保存和恢复对象的内部状态(如编辑操作的撤销、配置的版本管理)时
- 当对象的内部状态需要被审计或追溯(如风控规则的变更记录)时
- 当直接暴露对象的状态访问接口会破坏封装性(如支付交易的敏感状态)时
- 当需要实现多级撤销/重做功能(如文档编辑、流程设计器)时
2. 备忘录模式与其他模式的区别
- 与原型模式:两者都涉及对象状态的复制,但原型模式复制整个对象用于创建新实例,备忘录模式复制状态用于恢复原对象,前者是“创建新对象”,后者是“恢复原对象”
- 与命令模式:命令模式封装的是“操作”,备忘录模式封装的是“状态”,两者常结合使用(命令模式中用备忘录存储操作前后的状态)
- 与序列化:序列化也可保存对象状态,但序列化会暴露所有可序列化的状态,备忘录模式可选择性保存关键状态,且访问权限更严格
3. 支付系统中的实践价值
- 保障交易连续性:支付中断时可通过备忘录恢复交易状态,避免重复支付或状态不一致
- 增强系统容错性:配置变更(如费率调整、风控规则)出错时,可快速回滚到稳定版本
- 满足合规要求:状态变更记录可用于审计,符合支付行业的监管要求(如PCI DSS)
- 提升运维效率:无需手动重建历史状态,通过备忘录一键回滚,减少故障恢复时间
4. 实践建议
- 控制备忘录大小:只保存必要状态(如风控规则的核心表达式,而非完整日志),避免占用过多内存
- 使用深拷贝:备忘录存储状态时需深拷贝(如集合、日期对象),防止外部修改影响快照的准确性
- 持久化重要快照:关键业务的备忘录(如风控规则版本)应持久化到数据库,避免系统重启后丢失
- 限制备忘录数量:通过LRU策略或最大版本数限制,自动清理过期快照,平衡存储成本和回溯需求
备忘录模式通过“安全捕获状态、按需恢复”的思想,为支付系统中状态管理提供了可靠解决方案,既保证了对象封装性,又实现了灵活的状态回溯,是构建高可用、可追溯支付框架的重要模式。