备忘录模式在工作流草稿箱与状态回退中的实现
发布时间:2026/9/13 22:51:45 作者:尧图编辑部 阅读量:1,286

备忘录模式在工作流草稿箱与状态回退中的实现做政企协同办公或复杂审批流系统时用户经常在表单填写一半时临时退出或者在多步驳回、撤销操作时要求“一键还原到上一步编辑状态”。很多团队初期的做法简单粗暴前端本地存 localStorage或者后端每次操作直接往主业务表打 update另起一张 history_log 表记录整行的 JSON dump。这种方案在表单只有十几个扁平字段时跑得尚可一旦接入动态表单、级联审批人、附件临时凭证和条件分支网关问题接踵而至直接暴露内部状态导致外部逻辑容易破坏数据一致性深层嵌套对象的反序列化版本不兼容大字段频繁全量快照把 MySQL redo log 和 undo log 撑爆。备忘录模式Memento Pattern的核心价值在于在不破坏封装性的前提下捕获一个对象的内部状态并在外部保存使对象后续能恢复到原先的状态。业务场景与架 ground 建模在通用工作流引擎中一个待办审批实例WorkflowInstance包含当前节点、表单数据上下文FormData、审批人列表以及流转元数据。我们在设计草稿暂存与多级回退机制时定义三个核心角色发起人Originator工作流实例本身负责创建快照Snapshot和从快照中恢复状态。备忘录Memento存储发起人内部状态的不可变对象对外提供窄接口只读元信息对发起人提供宽接口完整状态读取。负责人Caretaker草稿箱与回退栈管理器负责维护快照的时间线序列控制持久化与容量淘汰。------------------ creates ---------------------- | WorkflowInstance | ---------------------- | WorkflowMemento (宽) | | (Originator) | ---------------------- | | ------------------ restores ---------------------- ^ | | operates | holds (窄) -------------------- | | DraftHistoryStack | --------------------------------- | (Caretaker) | --------------------核心实现代码为了防止并发修改污染快照所有进入 Memento 的状态必须进行深度拷贝或采用不可变集合包装。1. 备忘录接口与实体package com.example.workflow.memento; import java.io.Serializable; import java.time.LocalDateTime; import java.util.Collections; import java.util.HashMap; import java.util.Map; /** * 窄接口仅供 Caretaker 获取快照基础属性 */ public interface MementoView { String getSnapshotId(); String getStepNodeId(); LocalDateTime getCreatedAt(); String getOperatorId(); } /** * 完整备忘录内部状态对外部包私有或仅限 Originator 访问 */ public class WorkflowMemento implements MementoView, Serializable { private static final long serialVersionUID 1L; private final String snapshotId; private final String stepNodeId; private final LocalDateTime createdAt; private final String operatorId; // 内部保护状态不可直接暴露可变引用 private final MapString, Object formValues; private final String currentStatus; WorkflowMemento(String snapshotId, String stepNodeId, String operatorId, MapString, Object formValues, String currentStatus) { this.snapshotId snapshotId; this.stepNodeId stepNodeId; this.operatorId operatorId; this.createdAt LocalDateTime.now(); // 制作深度防御性拷贝 this.formValues Collections.unmodifiableMap(new HashMap(formValues)); this.currentStatus currentStatus; } Override public String getSnapshotId() { return snapshotId; } Override public String getStepNodeId() { return stepNodeId; } Override public LocalDateTime getCreatedAt() { return createdAt; } Override public String getOperatorId() { return operatorId; } MapString, Object getFormValues() { return formValues; } String getCurrentStatus() { return currentStatus; } }2. 发起人工作流实例package com.example.workflow.memento; import java.util.HashMap; import java.util.Map; import java.util.UUID; public class WorkflowInstance { private String instanceId; private String currentNodeId; private String status; private MapString, Object formData new HashMap(); public WorkflowInstance(String instanceId, String initialNodeId) { this.instanceId instanceId; this.currentNodeId initialNodeId; this.status DRAFT; } public void updateField(String key, Object value) { this.formData.put(key, value); } public void transitionTo(String nextNodeId, String nextStatus) { this.currentNodeId nextNodeId; this.status nextStatus; } /** * 创建当前状态的快照 */ public WorkflowMemento createMemento(String operatorId) { return new WorkflowMemento( UUID.randomUUID().toString(), this.currentNodeId, operatorId, this.formData, this.status ); } /** * 从快照恢复状态 */ public void restoreFromMemento(WorkflowMemento memento) { if (memento null) { throw new IllegalArgumentException(Memento cannot be null); } this.currentNodeId memento.getStepNodeId(); this.status memento.getCurrentStatus(); this.formData new HashMap(memento.getFormValues()); } public MapString, Object getFormData() { return Collections.unmodifiableMap(formData); } public String getCurrentNodeId() { return currentNodeId; } public String getStatus() { return status; } }3. 负责人草稿箱与历史栈管理器在服务端场景下草稿快照不能只存在 JVM 内存的Deque中必须配合 Redis 与持久化数据库实现分级存储。package com.example.workflow.memento; import java.util.ArrayDeque; import java.util.Deque; import java.util.Optional; public class WorkflowCaretaker { private static final int MAX_UNDO_STEPS 20; // 双端队列维护撤销与重做栈 private final DequeWorkflowMemento undoStack new ArrayDeque(); private final DequeWorkflowMemento redoStack new ArrayDeque(); public synchronized void pushSnapshot(WorkflowMemento memento) { if (undoStack.size() MAX_UNDO_STEPS) { undoStack.removeLast(); // 淘汰最早的历史快照防止内存泄漏 } undoStack.push(memento); redoStack.clear(); // 产生新分支动作时清空重做栈 } public synchronized OptionalWorkflowMemento undo(WorkflowMemento currentMemento) { if (undoStack.isEmpty()) { return Optional.empty(); } redoStack.push(currentMemento); return Optional.of(undoStack.pop()); } public synchronized OptionalWorkflowMemento redo(WorkflowMemento currentMemento) { if (redoStack.isEmpty()) { return Optional.empty(); } undoStack.push(currentMemento); return Optional.of(redoStack.pop()); } public int getHistorySize() { return undoStack.size(); } }生产落地的权衡考量1. 增量差量存储 vs 全量快照如果表单包含数百个字段或大型富文本内容每个步骤保存全量快照会造成严重的存储膨胀。在实际高频保存草稿的系统中可以引入 JSON PatchRFC 6902计算前后两次状态的 Diff草稿栈底层存储差量变更序列仅在基线节点保存全量 Snapshot。-- 生产草稿箱历史快照表结构 CREATE TABLE wf_instance_memento ( id bigint unsigned NOT NULL AUTO_INCREMENT, instance_id varchar(64) NOT NULL COMMENT 流程实例ID, snapshot_id varchar(64) NOT NULL COMMENT 快照唯一标识, node_id varchar(64) NOT NULL COMMENT 所处节点ID, operator_id varchar(64) NOT NULL COMMENT 操作人, patch_type tinyint NOT NULL DEFAULT 1 COMMENT 1-全量快照 2-JSON Patch差量, state_payload longtext NOT NULL COMMENT 序列化快照内容, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_snapshot (snapshot_id), KEY idx_inst_created (instance_id,created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2. 状态过期的清理策略草稿数据具备极强的时效性。工作流最终流转至“已归档”或“已终止”状态后应当通过异步消息解耦触发历史快照的物理清理或冷归档。同时给草稿箱存储设置合理的 TTL如 30 天未更新自动标记过期避免无休止堆积脏数据。通过备忘录模式我们将“状态获取与恢复逻辑”封闭在领域聚合根内部既杜绝了贫血模型下外部 Service 随意 set 内部字段的风险又通过 Caretaker 将时序控制、撤销栈与存储介质解耦为后续平滑接入分布式草稿同步打下扎实的基础。