备忘录模式:设计与实践

38 阅读14分钟

备忘录模式:设计与实践

一、什么是备忘录模式

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策略或最大版本数限制,自动清理过期快照,平衡存储成本和回溯需求

备忘录模式通过“安全捕获状态、按需恢复”的思想,为支付系统中状态管理提供了可靠解决方案,既保证了对象封装性,又实现了灵活的状态回溯,是构建高可用、可追溯支付框架的重要模式。