备忘录模式(Memento)

备忘录模式,说白了就是给对象拍一张「快照」。你玩游戏时存档,就是备忘录模式。我当年第一次接触这个模式时,觉得它太简单了——不就是保存状态吗?后来在项目中踩了坑,才发现里面的门道比想象中多得多。

模式结构:三个角色一台戏

备忘录模式有三个核心角色,我习惯把它们比作「游戏存档」的场景:

  • 发起人(Originator)——就是游戏主角。它知道自己要保存什么状态,也能从存档中恢复。
  • 备忘录(Memento)——就是存档文件。它只负责存储状态,不关心业务逻辑。
  • 管理者(Caretaker)——就是存档管理器。它负责保存和提供备忘录,但绝不能修改备忘录内容。

你想想看,如果存档管理器能随便改存档数据,那游戏还怎么玩?这就是备忘录模式的核心约束——管理者只能看,不能改。

核心要点:备忘录模式的关键在于「封装状态的存储」,让状态保存和恢复的逻辑与业务逻辑解耦。

白箱实现 vs 黑箱实现

这里有个经典问题:备忘录的接口应该暴露多少?我见过不少新手在这里翻车。

白箱实现:简单粗暴

白箱实现中,备忘录对外暴露所有内部状态。优点是实现简单,缺点是破坏了封装性——任何对象都能直接修改备忘录内容。

// 白箱备忘录——所有字段都是public
class Memento {
    public String state;
    public Memento(String state) {
        this.state = state;
    }
}

// 发起人
class Originator {
    private String state;
    
    public Memento save() {
        return new Memento(state);
    }
    
    public void restore(Memento m) {
        this.state = m.state;  // 直接访问public字段
    }
}

我在项目中遇到过这种情况:团队里有人图省事用了白箱实现,结果测试时发现某个模块偷偷改了备忘录里的数据,导致恢复时状态错乱。排查了半天才发现问题。

避坑指南:我曾经在一个金融项目中用了白箱实现,结果审计时发现状态被非法篡改。从那以后,但凡涉及敏感状态,我坚决不用白箱。

黑箱实现:优雅但复杂

黑箱实现利用内部类或接口来限制访问权限。备忘录只对外暴露窄接口(比如空接口),只有发起人能看到完整状态。

// 窄接口——对外只暴露这个
interface Memento {}

// 发起人内部类实现备忘录
class Originator {
    private String state;
    
    // 内部类,只有Originator能访问完整方法
    private class MementoImpl implements Memento {
        private String state;
        
        MementoImpl(String state) {
            this.state = state;
        }
        
        private String getState() {
            return state;
        }
    }
    
    public Memento save() {
        return new MementoImpl(state);
    }
    
    public void restore(Memento m) {
        // 需要强制转换,但只有Originator能这么做
        MementoImpl mi = (MementoImpl) m;
        this.state = mi.getState();
    }
}

// 管理者只能操作Memento接口
class Caretaker {
    private Memento memento;
    
    public void saveMemento(Memento m) {
        this.memento = m;
    }
    
    public Memento getMemento() {
        return memento;
    }
}

嗯,这里要注意:黑箱实现虽然安全,但增加了复杂度。我个人习惯是:如果状态不敏感,用白箱省事;如果涉及用户数据或金融信息,必须上黑箱。

我的建议:大多数业务场景用白箱就够了。别为了「设计模式」而设计模式,过度设计比没有设计更可怕。

备忘录模式在Spring WebFlow中的应用

Spring WebFlow是我见过把备忘录模式用得最漂亮的开源框架之一。它用备忘录模式实现了流程状态的保存和恢复。

你想想看,一个Web流程可能包含多个步骤:填写表单→确认信息→支付→完成。如果用户在支付时失败了,系统需要能回退到确认信息那一步。这就是备忘录模式的典型场景。

Spring WebFlow中的核心实现:

  • FlowExecution——相当于发起人,保存当前流程的执行状态
  • FlowExecutionSnapshot——就是备忘录,保存某一时刻的流程快照
  • FlowExecutionRepository——管理者,负责存储和恢复快照
// Spring WebFlow中的简化实现
public class FlowExecution {
    private MutableAttributeMap conversationScope;
    private Map<String, Object> flowScope;
    
    // 创建快照——相当于save()
    public FlowExecutionSnapshot createSnapshot() {
        return new FlowExecutionSnapshot(
            new HashMap<>(conversationScope),
            new HashMap<>(flowScope)
        );
    }
    
    // 恢复快照——相当于restore()
    public void restoreFromSnapshot(FlowExecutionSnapshot snapshot) {
        this.conversationScope.putAll(snapshot.getConversationScope());
        this.flowScope.putAll(snapshot.getFlowScope());
    }
}

// 管理者——负责存储和恢复
public class FlowExecutionRepository {
    private Map<String, FlowExecutionSnapshot> snapshotStore = new HashMap<>();
    
    public void saveSnapshot(String flowId, FlowExecution execution) {
        snapshotStore.put(flowId, execution.createSnapshot());
    }
    
    public void restoreSnapshot(String flowId, FlowExecution execution) {
        FlowExecutionSnapshot snapshot = snapshotStore.get(flowId);
        if (snapshot != null) {
            execution.restoreFromSnapshot(snapshot);
        }
    }
}

我记得有一次排查线上问题,用户反馈流程走到一半刷新页面,数据全丢了。一看代码,原来是开发者没有正确调用saveSnapshot。说白了,备忘录模式再完美,用错了地方也是白搭。

关键洞察:Spring WebFlow之所以选择备忘录模式,是因为流程状态需要「原子性保存和恢复」。你不能只保存一半状态,也不能恢复时漏掉某个变量。备忘录模式天然保证了这一点。

SVG:备忘录模式核心结构图

备忘录模式核心结构 发起人 (Originator) - state: String + save(): Memento + restore(m: Memento) 备忘录 (Memento) - state: String + getState(): String + setState(s: String) 管理者 (Caretaker) - memento: Memento + saveMemento(m: Memento) + getMemento(): Memento 创建/恢复 存储/提供 白箱实现:备忘录所有字段对外可见 黑箱实现:备忘录只暴露窄接口,内部类实现细节 白箱 vs 黑箱 对比 白箱 实现简单,但破坏封装 黑箱 适合快速原型 适合敏感数据 安全但复杂

总结

备忘录模式不是什么高深的技术,但它解决了一个很实际的问题:如何在保证封装性的前提下,保存和恢复对象状态。我个人觉得,理解这个模式的关键不在于记住UML图,而在于想清楚「谁该看到什么」。

白箱实现简单直接,适合内部系统或原型开发。黑箱实现安全可靠,适合对外接口或敏感数据。至于Spring WebFlow中的应用,它告诉我们:好的框架会把设计模式融入血液,而不是贴标签式地使用。

最后一句:设计模式是工具,不是教条。用对了地方,它就是神兵利器;用错了地方,它就是画蛇添足。


公众号:蓝海资料掘金营,微信deep3321