Java 23 种设计模式:从踩坑到精通 | 番外:备忘录模式 —— 订单草稿快照实战

摘要:备忘录模式在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复。它通过“发起人自己保存、自己恢复”的机制,在不暴露实现细节的前提下实现撤销和回滚。本文结合电商订单草稿自动保存与恢复的场景,完整展示如何用备忘录模式实现任意历史版本的回退,并与命令模式、原型模式深度对比,帮你掌握“状态快照”的设计精髓。

🗺️ 本文阅读地图(3 分钟速览)

  • 为什么不能直接把订单状态保存到外部?
  • 备忘录三角色:发起人、备忘录(不可变)、管理者
  • 手写草稿管理器:自动保存历史版本,支持回退
  • 备忘录 vs 命令:状态快照 vs 操作记录
  • 面试必问:“管理者能修改备忘录吗?为什么备忘录要不可变?”

📖 《Java 23 种设计模式:从踩坑到精通》
开篇:系列介绍与目录 | 正篇:Memento 备忘录模式 —— 快照与撤销,给对象装一个“后悔药” | 当前:番外 · 备忘录模式 × 订单草稿快照
🔗 返回系列总目录

1. 订单草稿自动保存的痛点

在电商系统中,用户填写订单草稿时,系统需要定时保存历史版本,支持“撤销”回退到任意历史版本。如果直接把订单状态保存到外部变量:

// 直接把内部状态暴露给外部
Map<String, String> history = new HashMap<>();
history.put("v1", order.getState());  // 破坏了封装性

这种方式迫使订单类暴露内部状态,破坏了封装性。更糟糕的是,外部代码可以随意修改保存的状态,导致快照被污染。

备忘录模式的解决思路:让订单类(发起人)自己创建状态快照(备忘录),管理者只负责存取快照,无法查看或修改快照内容。恢复时,订单类根据快照自行恢复。

1.1 你的场景该不该用备忘录?

判断标准 是 → 用备忘录 否 → 用其他方式
需要保存对象的历史状态,支持撤销/回退
希望不破坏封装地实现状态保存
对象状态复杂,不能简单用几个变量记录
对象状态简单,只需记录一两个字段 直接保存字段值即可

2. 备忘录模式 UML(订单草稿快照场景)

在这里插入图片描述


3. 完整源码实现

3.1 备忘录类 (Memento)

/**
 * 备忘录:存储发起人的状态快照
 * 对外仅暴露窄接口(只能获取状态,不能修改)
 */
public class Memento {
    private final String state;  // 状态快照,不可变

    public Memento(String state) {
        this.state = state;
    }

    public String getState() { return state; }
}

💬 白话:备忘录是一个“只读快照”——state 字段是 final,一旦创建就不能修改。这就是为什么管理者无法污染历史版本。

3.2 发起人:订单草稿 (Originator)

/**
 * 发起人:订单草稿类,负责创建和恢复备忘录
 */
public class Originator {
    private String state;   // 当前草稿内容
    private String orderId;

    public Originator(String orderId) {
        this.orderId = orderId;
        this.state = "";
    }

    /** 创建当前状态的备忘录快照 */
    public Memento createMemento() {
        System.out.println("💾 保存草稿快照:" + state);
        return new Memento(state);
    }

    /** 从备忘录恢复之前的状态 */
    public void restore(Memento memento) {
        this.state = memento.getState();
        System.out.println("🔄 已恢复到快照状态:" + state);
    }

    /** 修改状态(模拟用户编辑草稿) */
    public void setState(String state) {
        this.state = state;
        System.out.println("✏️ 编辑草稿内容:" + state);
    }

    public String getState() { return state; }
    public String getOrderId() { return orderId; }
}

💬 白话:订单草稿自己知道该保存什么状态、怎么恢复。外部完全不知道草稿内部有哪些字段——这就是封装性的关键。

3.3 管理者:草稿管理器 (Caretaker)

import java.util.*;

/**
 * 管理者:草稿管理器,负责存储和保管备忘录
 */
public class Caretaker {
    private List<Memento> mementos = new ArrayList<>();
    private String orderId;

    public Caretaker(String orderId) {
        this.orderId = orderId;
    }

    public void add(Memento memento) {
        mementos.add(memento);
        System.out.println("📦 草稿已保存到历史版本,当前共 " + mementos.size() + " 个版本\n");
    }

    public Memento get(int index) {
        if (index < 0 || index >= mementos.size()) {
            throw new IndexOutOfBoundsException("版本索引超出范围");
        }
        return mementos.get(index);
    }

    public Memento getLatest() {
        return mementos.isEmpty() ? null : mementos.get(mementos.size() - 1);
    }

    public void remove(int index) { mementos.remove(index); }

    public int getVersionCount() { return mementos.size(); }
}

💬 白话:管理者就像“档案管理员”——只负责存取档案,从不打开看内容。它用的是 List,可以按索引回退到任意版本。

3.4 客户端测试

public class Client {
    public static void main(String[] args) {
        Originator order = new Originator("20240723001");
        Caretaker caretaker = new Caretaker("20240723001");

        System.out.println("=== 场景:用户编辑订单草稿,系统自动保存历史版本 ===\n");

        // v1
        order.setState("商品:iPhone 15, 数量:1");
        caretaker.add(order.createMemento());

        // v2
        order.setState("商品:iPhone 15, 数量:2, 颜色:黑色");
        caretaker.add(order.createMemento());

        // v3
        order.setState("商品:iPhone 15, 数量:2, 颜色:黑色, 赠品:耳机");
        caretaker.add(order.createMemento());

        System.out.println("=== 场景:用户误操作,需要撤销到版本1 ===\n");

        // 恢复到版本1
        order.restore(caretaker.get(0));

        System.out.println("\n=== 场景:用户重新编辑,保存新版本 ===\n");
        order.setState("商品:iPhone 15, 数量:1, 颜色:白色");
        caretaker.add(order.createMemento());

        System.out.println("\n=== 最终状态 ===");
        System.out.println("当前草稿内容:" + order.getState());
        System.out.println("历史版本总数:" + caretaker.getVersionCount());
    }
}

4. 运行结果

=== 场景:用户编辑订单草稿,系统自动保存历史版本 ===

✏️ 编辑草稿内容:商品:iPhone 15, 数量:1
💾 保存草稿快照:商品:iPhone 15, 数量:1
📦 草稿已保存到历史版本,当前共 1 个版本

✏️ 编辑草稿内容:商品:iPhone 15, 数量:2, 颜色:黑色
💾 保存草稿快照:商品:iPhone 15, 数量:2, 颜色:黑色
📦 草稿已保存到历史版本,当前共 2 个版本

✏️ 编辑草稿内容:商品:iPhone 15, 数量:2, 颜色:黑色, 赠品:耳机
💾 保存草稿快照:商品:iPhone 15, 数量:2, 颜色:黑色, 赠品:耳机
📦 草稿已保存到历史版本,当前共 3 个版本

=== 场景:用户误操作,需要撤销到版本1 ===

🔄 已恢复到快照状态:商品:iPhone 15, 数量:1

=== 场景:用户重新编辑,保存新版本 ===

✏️ 编辑草稿内容:商品:iPhone 15, 数量:1, 颜色:白色
💾 保存草稿快照:商品:iPhone 15, 数量:1, 颜色:白色
📦 草稿已保存到历史版本,当前共 4 个版本

=== 最终状态 ===
当前草稿内容:商品:iPhone 15, 数量:1, 颜色:白色
历史版本总数:4

5. 核心角色回顾

角色 职责 对应代码
Originator 创建快照、恢复快照 Originator
Memento 不可变快照,封装内部状态 Memento
Caretaker 存取快照,不查看不修改 Caretaker

6. 备忘录模式 vs 命令模式 vs 原型模式

对比项 备忘录模式 命令模式 原型模式
核心目的 保存/恢复对象状态 封装请求为对象 克隆对象
是否保存状态 是,主动保存快照 可选,通过命令存储操作前状态 是,创建副本
典型操作 createMemento() / restore() execute() / undo() clone()
典型应用 订单草稿回退、游戏存档 仓储设备控制、事务回滚 报表模板复制

💡 一句话记忆:备忘录是“拍照留底”,命令是“执行+反悔”,原型是“复制粘贴”。三种模式常配合使用——命令负责记录操作历史,备忘录保存操作前状态,原型通过克隆快速创建快照。

7. 备忘录模式的优缺点

优点 缺点
不破坏封装:内部状态对外界完全隐藏 频繁保存快照会消耗大量内存
可恢复性强:支持任意历史版本回退 快照保存整个对象状态,大对象有性能开销
三角色职责清晰 管理者“无知”——只能存取,不能查看

8. 六大设计原则体现

原则 体现
单一职责 发起人管业务,备忘录管快照,管理者管存储
开闭原则 新增保存策略无需修改发起人
里氏替换 不同备忘录实现可替换
依赖倒置 管理者依赖抽象 Memento
接口隔离 备忘录对管理者只暴露窄接口(getState
迪米特法则 管理者只与备忘录交互,不知发起人存在

附 备忘录模式 UML源码(订单草稿快照场景)

@startuml
title Java 23 种设计模式:从踩坑到精通
footer 折哥 | 智能物流与Java实战

' 1. 全局样式配置
skinparam backgroundColor #FEFEFE
skinparam shadowing false
skinparam classBorderColor #333333
skinparam classFontColor #1A1A1A
skinparam classFontSize 14
skinparam noteFontSize 12
skinparam noteFontColor #555555
skinparam arrowColor #555555
skinparam classBackgroundColor #F9F9F9

' 2. 发起人
class Originator {
    - state : String
    + createMemento() : Memento
    + restore(Memento) : void
}
note right of Originator
  <b>发起人</b>
  --
  创建备忘录保存自身状态快照
  通过备忘录恢复之前的状态
end note

' 3. 备忘录(不可变快照)
class Memento {
    - state : String
    + getState() : String
}
note right of Memento
  <b>备忘录</b>
  --
  存储发起人的内部状态快照
  状态对外部不可见(窄接口)
  保证封装性不被破坏
end note

' 4. 管理者
class Caretaker {
    - mementos : List<Memento>
    + add(Memento) : void
    + get(int) : Memento
    + remove(int) : void
}
note right of Caretaker
  <b>管理者</b>
  --
  负责存储和保管备忘录
  不能修改或查看备忘录内容
  仅提供存取操作
end note

' 5. 关系连线
Originator ..down> Memento : 创建
Caretaker o-down-> Memento : 持有

@enduml

🧭 《Java 23 种设计模式:从踩坑到精通》快速导航

🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 25 篇文章彻底吃透设计模式。
📦 福利预告:全系列代码及 UML 源码将在完结时统一打包开放,点击「关注」「收藏」第一时间获取。

📌 除了设计模式,我也在深挖智能物流实战(WMS、托盘调度、机器学习落地)。欢迎点击头像,看看专栏 《出版社物流WMS智能调度实战》《电商多平台电子面单对接实战》。技术相通,思路可鉴。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐