大白话说Java设计模式-40-备忘录模式(业务实战篇)

📌 PDF :AI人工智能 --- 大模型微调与部署实战项目

大白话说Java设计模式-40-备忘录模式(业务实战篇):大白商城购物车快照的"状态保存"

📌 一句话本质:备忘录模式就是"保存对象快照",把状态保存起来,需要时可恢复,不破坏封装性。

🏷️ 标签:备忘录模式 / Java 设计模式 / 状态保存 / 撤销 / 大白商城 🎯 适合:初中级后端 / 经常被"状态恢复"困扰的工程师


目录


一、业务场景引入:为什么需要"状态保存"?

大白商城 2024 年用户运营团队提出一个需求:

"用户在购物车修改商品(加/删/改数量)后,30 分钟内可一键恢复到任一历史版本!"

我打开代码一看:

java 复制代码
/**
 * ❌ 大白商城老的购物车代码
 */
public class CartServiceV0 {

    public void updateCart(String userId, CartDTO cart) {
        // ❌ 直接覆盖,没有历史记录
        cartDao.update(userId, cart);
    }
}

这种代码的痛点

序号 问题 后果
没有历史记录 无法恢复
修改后无法撤销 用户改错无法回退
多版本对比困难 运营无法分析
数据丢失风险 误操作无法找回

老板要的"用户可恢复历史购物车",怎么搞?

答案就是------备忘录模式

1.1 大白话讲透备忘录

继续打比方:

场景:用户改购物车(加/删/改数量),想恢复到任一历史版本。

  • 错误做法:直接覆盖,没有历史
  • 正确做法
    1. 每次修改前 ,先保存一个快照(Memento)
    2. 快照存入历史栈
    3. 用户点"恢复"时,从栈里取快照
    4. 完整恢复购物车

备忘录 = 保存对象快照,需要时可恢复,不破坏封装性

1.2 备忘录模式的 3 个真实场景

大白商城里,备忘录模式用在:

场景 "快照"
购物车快照 购物车版本
编辑器撤销 文本快照
数据库事务 事务前后状态
游戏存档 角色状态
配置回滚 配置版本

任何"需要保存/恢复历史状态"的场景,都用备忘录


二、反面教材:硬编码状态的"灾难现场"

我们看 4 个反面教材,看它们是怎么一步步崩的。

2.1 反面教材 v1:直接覆盖

java 复制代码
/**
 * ❌ 反面教材 v1:直接覆盖
 */
public void updateCart(String userId, CartDTO cart) {
    cartDao.update(userId, cart);  // ❌ 覆盖,无历史
}

翻车现场

序号 问题 后果
无历史 无法恢复
覆盖丢失 数据丢失

2.2 反面教材 v2:手动备份字段

java 复制代码
/**
 * ❌ 反面教材 v2:手动备份字段
 */
public void updateCart(String userId, CartDTO cart) {
    // ❌ 手动备份所有字段
    CartDTO backup = new CartDTO();
    backup.setItems(cart.getItems());
    backup.setTotal(cart.getTotal());
    // 字段多了要改 N 处
}

翻车现场

序号 问题 后果
字段多易错 改字段要改 N 处
破坏封装 直接访问字段

2.3 反面教材 v3:版本分散

java 复制代码
/**
 * ❌ 反面教材 v3:版本分散
 */
public void saveVersion(CartDTO cart) {
    // 每种对象自己实现 saveVersion
    // 分散
}

翻车现场

序号 问题 后果
逻辑分散 难维护
无法统一 各业务方各写各的

2.4 反面教材 v4:完全没抽象

java 复制代码
/**
 * ❌ 反面教材 v4:完全没抽象
 */
public class CartServiceV4 {
    public void process() {
        // 1000 行所有逻辑
    }
}

翻车现场

序号 问题 后果
无法复用 改一处牵动全身
无法测试 单元测试跑不动

2.5 4 个反面教材的共同病根

痛点 反模式方案能不能解决?
保存历史状态 ❌ 全部没有
不破坏封装 ❌ 全部要改
统一抽象 ❌ 全部分散
可恢复 ❌ 全部无

必须上备忘录模式


三、模式原理:备忘录的"发起人 + 备忘录 + 管理者"

3.1 备忘录的 3 个核心概念

发起人(Originator)

  • 原始对象
  • 创建/使用备忘录
  • 知道怎么保存/恢复

备忘录(Memento)

  • 存储发起人状态
  • 不可变
  • 只暴露给发起人

管理者(Caretaker)

  • 持有备忘录列表
  • 不修改备忘录
  • 负责存储/获取

大白商城例子

概念 例子
发起人 ShoppingCart
备忘录 CartMemento
管理者 CartHistory(历史栈)

3.2 备忘录的 3 个核心角色

角色 职责 例子
发起人(Originator) 创建/恢复备忘录 ShoppingCart
备忘录(Memento) 存储状态 CartMemento
管理者(Caretaker) 管理备忘录 CartHistory

3.3 一张图看懂备忘录

复制代码
ShoppingCart(发起人)
  │
  │ createMemento()
  │   ↓
  │ CartMemento(备忘录)
  │   │
  │   └── 内部状态(不可变)
  │
  │ restore(memento)
  │
  └── CartHistory(管理者)
        │
        │ saveMemento(memento)
        │ getMemento(index)
        │
        └── List<CartMemento>

3.4 备忘录的"灵魂三问"

Q1:备忘录 vs 序列化,区别是什么?

答:

  • 备忘录保存对象状态(含恢复语义)
  • 序列化对象转字节(含传输语义)
  • 备忘录通常内存序列化可以存盘
  • 备忘录支持历史栈序列化单次

Q2:备忘录 vs 备份,区别是什么?

答:

  • 备忘录细粒度状态(单个对象)
  • 备份粗粒度数据(整个系统/库)
  • 备忘录应用层备份系统层
  • 备忘录实时备份定时

Q3:备忘录模式的核心价值是什么?

答:

  • 保存历史状态(多版本)
  • 不破坏封装(只暴露给 Originator)
  • 支持撤销/重做
  • 可恢复

3.5 备忘录的 3 种实现方式

方式 特点 适用
经典备忘录 三件套 标准
白箱模式 备忘录公开状态 简单
黑箱模式 备忘录只给 Originator 严格

大白商城主推黑箱模式(保护封装)。


四、实战代码:大白商城购物车快照完整实现

下面是大白商城生产环境在用的备忘录实现,全套代码可直接复制到 IDEA 跑

4.1 项目环境与依赖

pom.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
                             https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>

    <groupId>com.dabai.mall</groupId>
    <artifactId>mall-design-pattern-40</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>mall-design-pattern-40</name>
    <description>大白商城 - 设计模式 40 备忘录模式</description>

    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

4.2 备忘录:CartMemento

java 复制代码
package com.dabai.mall.memento;

import lombok.Getter;

import java.io.Serializable;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;

/**
 * ✅ 备忘录:购物车快照
 * <p>
 * - 不可变
 * - 只暴露给 Originator
 *
 * @author 大白商城技术团队
 */
@Getter
public class CartMemento implements Serializable {

    /**
     * 快照 ID
     */
    private final String snapshotId;

    /**
     * 商品列表
     */
    private final List<CartItem> items;

    /**
     * 总价
     */
    private final BigDecimal total;

    /**
     * 创建时间
     */
    private final Instant createdAt;

    /**
     * 描述
     */
    private final String description;

    public CartMemento(String snapshotId, List<CartItem> items, 
                       BigDecimal total, String description) {
        this.snapshotId = snapshotId;
        // ✅ 深拷贝:防止外部修改
        this.items = items.stream()
                .map(item -> new CartItem(item.getGoodsId(), 
                        item.getGoodsName(), item.getQuantity(), item.getPrice()))
                .toList();
        this.total = total;
        this.description = description;
        this.createdAt = Instant.now();
    }

    /**
     * 内部类:购物车项
     */
    @Getter
    public static class CartItem implements Serializable {
        private final Long goodsId;
        private final String goodsName;
        private final Integer quantity;
        private final BigDecimal price;

        public CartItem(Long goodsId, String goodsName, 
                        Integer quantity, BigDecimal price) {
            this.goodsId = goodsId;
            this.goodsName = goodsName;
            this.quantity = quantity;
            this.price = price;
        }
    }
}

4.3 发起人:ShoppingCart

java 复制代码
package com.dabai.mall.memento;

import com.dabai.mall.memento.CartMemento.CartItem;
import lombok.Getter;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

/**
 * ✅ 发起人:购物车
 * <p>
 * 知道怎么创建/恢复备忘录
 *
 * @author 大白商城技术团队
 */
@Getter
public class ShoppingCart {

    private String userId;
    private List<CartItem> items = new ArrayList<>();

    public ShoppingCart(String userId) {
        this.userId = userId;
    }

    /**
     * ✅ 添加商品
     */
    public void addItem(CartItem item) {
        items.add(item);
    }

    /**
     * ✅ 移除商品
     */
    public void removeItem(Long goodsId) {
        items.removeIf(item -> item.getGoodsId().equals(goodsId));
    }

    /**
     * ✅ 修改数量
     */
    public void updateQuantity(Long goodsId, int quantity) {
        for (CartItem item : items) {
            if (item.getGoodsId().equals(goodsId)) {
                items.set(items.indexOf(item), 
                    new CartItem(goodsId, item.getGoodsName(), 
                            quantity, item.getPrice()));
                return;
            }
        }
    }

    /**
     * ✅ 计算总价
     */
    public BigDecimal getTotal() {
        return items.stream()
                .map(item -> item.getPrice().multiply(
                    BigDecimal.valueOf(item.getQuantity())))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    /**
     * ✅ 创建备忘录
     */
    public CartMemento createMemento(String description) {
        return new CartMemento(
                "SNAP_" + System.currentTimeMillis(),
                items, getTotal(), description);
    }

    /**
     * ✅ 从备忘录恢复
     */
    public void restore(CartMemento memento) {
        this.items = new ArrayList<>(memento.getItems());
    }

    /**
     * 显示购物车
     */
    public void show() {
        log.info("【购物车】userId={}, 总价={}", userId, getTotal());
        items.forEach(item -> log.info("  - {} x {}", 
                item.getGoodsName(), item.getQuantity()));
    }
}

4.4 管理者:CartHistory

java 复制代码
package com.dabai.mall.memento;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

/**
 * ✅ 管理者:购物车历史
 * <p>
 * 持有备忘录列表,不修改备忘录
 *
 * @author 大白商城技术团队
 */
@Slf4j
public class CartHistory {

    /**
     * 备忘录栈
     */
    private final List<CartMemento> history = new ArrayList<>();

    /**
     * 当前索引(支持"恢复历史版本")
     */
    private int currentIndex = -1;

    /**
     * ✅ 保存备忘录
     */
    public void save(CartMemento memento) {
        // ✅ 新快照后,清除 redo 栈
        if (currentIndex < history.size() - 1) {
            history.subList(currentIndex + 1, history.size()).clear();
        }
        history.add(memento);
        currentIndex = history.size() - 1;
        log.info("【历史】保存快照: {}, 当前索引: {}, 总数: {}",
                memento.getSnapshotId(), currentIndex, history.size());
    }

    /**
     * ✅ 撤销:恢复到上一个
     */
    public CartMemento undo() {
        if (currentIndex <= 0) {
            log.warn("【历史】无可撤销快照");
            return null;
        }
        currentIndex--;
        return history.get(currentIndex);
    }

    /**
     * ✅ 重做
     */
    public CartMemento redo() {
        if (currentIndex >= history.size() - 1) {
            log.warn("【历史】无可重做快照");
            return null;
        }
        currentIndex++;
        return history.get(currentIndex);
    }

    /**
     * ✅ 获取指定历史版本
     */
    public CartMemento getVersion(int versionIndex) {
        if (versionIndex < 0 || versionIndex >= history.size()) {
            throw new IllegalArgumentException("版本不存在: " + versionIndex);
        }
        currentIndex = versionIndex;
        return history.get(versionIndex);
    }

    /**
     * 获取所有版本
     */
    public List<CartMemento> getAllVersions() {
        return new ArrayList<>(history);
    }
}

4.5 业务调用方

java 复制代码
package com.dabai.mall.service;

import com.dabai.mall.memento.*;
import com.dabai.mall.memento.CartMemento.CartItem;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

/**
 * ✅ 购物车服务:备忘录模式的调用方
 * <p>
 * 业务方只调 createMemento / restore,不关心快照细节
 *
 * @author 大白商城技术团队
 */
@Service
public class CartService {

    /**
     * ✅ 用户购物车 + 历史
     */
    public void processCart() {
        // 1️⃣ 创建购物车
        ShoppingCart cart = new ShoppingCart("USER_001");
        cart.addItem(new CartItem(1L, "iPhone", 1, new BigDecimal("5999")));
        cart.addItem(new CartItem(2L, "iPad", 1, new BigDecimal("3999")));

        // 2️⃣ 创建历史
        CartHistory history = new CartHistory();

        // 3️⃣ 第一次保存
        history.save(cart.createMemento("初始版本"));

        // 4️⃣ 修改
        cart.addItem(new CartItem(3L, "MacBook", 1, new BigDecimal("9999")));
        history.save(cart.createMemento("加入 MacBook"));

        // 5️⃣ 修改
        cart.removeItem(2L);
        history.save(cart.createMemento("删除 iPad"));

        // 6️⃣ 撤销:恢复上一版本
        CartMemento prev = history.undo();
        cart.restore(prev);
        cart.show();

        // 7️⃣ 重做
        CartMemento next = history.redo();
        cart.restore(next);
        cart.show();
    }
}

4.6 单元测试

java 复制代码
package com.dabai.mall.memento;

import com.dabai.mall.memento.CartMemento.CartItem;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

/**
 * 备忘录模式完整单元测试
 */
class CartMementoTest {

    @Test
    void testCreateMemento() {
        ShoppingCart cart = new ShoppingCart("USER_001");
        cart.addItem(new CartItem(1L, "iPhone", 1, new BigDecimal("5999")));

        CartMemento memento = cart.createMemento("初始");
        assertEquals(1, memento.getItems().size());
    }

    @Test
    void testRestoreMemento() {
        ShoppingCart cart = new ShoppingCart("USER_001");
        cart.addItem(new CartItem(1L, "iPhone", 1, new BigDecimal("5999")));

        CartMemento memento = cart.createMemento("初始");
        cart.addItem(new CartItem(2L, "iPad", 1, new BigDecimal("3999")));

        // ✅ 恢复
        cart.restore(memento);
        assertEquals(1, cart.getItems().size());
    }

    @Test
    void testUndo() {
        ShoppingCart cart = new ShoppingCart("USER_001");
        CartHistory history = new CartHistory();

        cart.addItem(new CartItem(1L, "iPhone", 1, new BigDecimal("5999")));
        history.save(cart.createMemento("v1"));

        cart.addItem(new CartItem(2L, "iPad", 1, new BigDecimal("3999")));
        history.save(cart.createMemento("v2"));

        // ✅ 撤销
        CartMemento undo = history.undo();
        cart.restore(undo);
        assertEquals(1, cart.getItems().size());
    }

    @Test
    void testRedo() {
        ShoppingCart cart = new ShoppingCart("USER_001");
        CartHistory history = new CartHistory();

        cart.addItem(new CartItem(1L, "iPhone", 1, new BigDecimal("5999")));
        history.save(cart.createMemento("v1"));

        cart.addItem(new CartItem(2L, "iPad", 1, new BigDecimal("3999")));
        history.save(cart.createMemento("v2"));

        history.undo();
        // ✅ 重做
        CartMemento redo = history.redo();
        cart.restore(redo);
        assertEquals(2, cart.getItems().size());
    }

    @Test
    void testGetVersion() {
        ShoppingCart cart = new ShoppingCart("USER_001");
        CartHistory history = new CartHistory();

        cart.addItem(new CartItem(1L, "iPhone", 1, new BigDecimal("5999")));
        history.save(cart.createMemento("v1"));
        cart.addItem(new CartItem(2L, "iPad", 1, new BigDecimal("3999")));
        history.save(cart.createMemento("v2"));

        // ✅ 获取指定版本
        CartMemento v1 = history.getVersion(0);
        cart.restore(v1);
        assertEquals(1, cart.getItems().size());
    }

    @Test
    void testMementoImmutability() {
        // ✅ 备忘录不可变
        ShoppingCart cart = new ShoppingCart("USER_001");
        cart.addItem(new CartItem(1L, "iPhone", 1, new BigDecimal("5999")));

        CartMemento memento = cart.createMemento("v1");
        List<CartItem> items = memento.getItems();
        int originalSize = items.size();

        // ✅ 修改 memento 不影响 cart
        items.clear();
        assertEquals(originalSize, memento.getItems().size());
    }

    @Test
    void testHistoryClear() {
        // ✅ 新快照后 redo 栈被清空
        ShoppingCart cart = new ShoppingCart("USER_001");
        CartHistory history = new CartHistory();

        cart.addItem(new CartItem(1L, "iPhone", 1, new BigDecimal("5999")));
        history.save(cart.createMemento("v1"));
        cart.addItem(new CartItem(2L, "iPad", 1, new BigDecimal("3999")));
        history.save(cart.createMemento("v2"));
        history.undo();
        history.undo();

        // ✅ 新快照清空 redo
        cart.addItem(new CartItem(3L, "Mac", 1, new BigDecimal("9999")));
        history.save(cart.createMemento("v3"));

        // ✅ redo 栈已清空
        assertNull(history.redo());
    }
}

五、备忘录 vs 序列化 / 备份

5.1 一张表看清区别

模式 关注点 粒度 适用
备忘录 状态保存 细(对象) 撤销/恢复
序列化 对象转字节 中(对象) 持久化/传输
备份 数据备份 粗(系统/库) 容灾

5.2 一个具体例子区分 3 个概念

大白商城

概念 例子
备忘录 购物车版本(内存中)
序列化 购物车存 Redis(持久化)
备份 数据库每日全量备份

5.3 决策树

复制代码
需要保存/恢复状态?
├── 是
│   ├── 内存中保存?
│   │   ├── 是 → ✅ 备忘录
│   │   └── 否
│   │       ├── 持久化对象?
│   │       │   ├── 是 → ✅ 序列化
│   │       │   └── 否 → 备份
└── 否 → 不需要

5.4 备忘录 vs 序列化:易混淆

维度 备忘录 序列化
关注 状态恢复 对象转换
位置 内存 内存/磁盘/网络
历史 多版本 单次
性能

大白商城选型

场景 选哪个 原因
购物车版本 备忘录 多版本
对象存 Redis 序列化 持久化
数据库备份 备份 容灾
事务回滚 备忘录 内存快照

六、Spring @Transactional 怎么用备忘录?

6.1 @Transactional:事务备忘录

Spring @Transactional 用备忘录思想

java 复制代码
/**
 * ✅ @Transactional:事务备忘录
 */
@Service
public class OrderService {

    @Transactional
    public void payOrder(String orderId) {
        // 1️⃣ 备忘录:保存事务前状态
        // 2️⃣ 执行
        orderDao.updateStatus(orderId, "PAID");
        stockService.decrease(...);
        // 3️⃣ 提交 / 失败回滚(恢复备忘录)
    }
}

关键点

关键点 解释
事务开始 创建备忘录
业务执行 修改状态
事务提交 销毁备忘录
失败回滚 恢复备忘录

6.2 TransactionSynchronizationManager:备忘录管理

核心源码

java 复制代码
/**
 * ✅ 事务备忘录管理
 */
public abstract class TransactionSynchronizationManager {
    
    /**
     * ✅ 资源绑定
     */
    public static void bindResource(Object key, Object value) { ... }
    
    /**
     * 获取资源
     */
    public static Object getResource(Object key) { ... }
    
    /**
     * 解绑
     */
    public static Object unbindResource(Object key) { ... }
}

6.3 大白商城的"抄作业"

java 复制代码
/**
 * 借鉴 @Transactional:通用事务管理
 */
public class SimpleTransactionManager {

    /**
     * 执行带"事务"的操作
     */
    public <T> T executeInTransaction(Supplier<T> action) {
        // 1️⃣ 备忘录:保存当前状态
        Map<String, Object> snapshot = createSnapshot();
        try {
            // 2️⃣ 执行
            T result = action.get();
            // 3️⃣ 提交:销毁快照
            return result;
        } catch (Exception e) {
            // 4️⃣ 回滚:恢复快照
            restoreSnapshot(snapshot);
            throw e;
        }
    }
}

七、备忘录 + 命令模式怎么组合?

7.1 组合 1:备忘录支持命令撤销

java 复制代码
/**
 * ✅ 备忘录 + 命令:命令撤销用备忘录
 */
public class PayOrderCommand implements OrderCommand {

    private final Order order;
    private OrderMemento backup;  // ✅ 备份快照

    @Override
    public void execute() {
        // 1️⃣ 备份
        backup = order.createMemento();
        // 2️⃣ 执行
        order.setStatus("PAID");
    }

    @Override
    public void undo() {
        // ✅ 从备忘录恢复
        order.restore(backup);
    }
}

7.2 组合 2:命令持有备忘录

java 复制代码
/**
 * ✅ 命令 + 备忘录:每个命令有自己的备忘录
 */
public abstract class AbstractCommandWithMemento implements OrderCommand {

    protected Memento backup;

    protected abstract void doExecute();
    protected abstract Memento createBackup();
    protected abstract void restoreFromBackup(Memento m);

    @Override
    public final void execute() {
        backup = createBackup();
        doExecute();
    }

    @Override
    public final void undo() {
        if (backup != null) {
            restoreFromBackup(backup);
        }
    }
}

7.3 组合 3:完整撤销/重做系统

java 复制代码
/**
 * ✅ 完整撤销/重做:命令 + 备忘录 + 历史栈
 */
@Service
public class CartUndoService {

    private final CommandInvoker invoker = new CommandInvoker();
    private final CartHistory history = new CartHistory();

    public void executeWithBackup(ShoppingCart cart, Runnable action, String desc) {
        // 1️⃣ 备份
        history.save(cart.createMemento(desc));
        // 2️⃣ 执行命令
        action.run();
    }

    public void undo(ShoppingCart cart) {
        CartMemento prev = history.undo();
        if (prev != null) {
            cart.restore(prev);
        }
    }
}

八、工程决策 Checklist

8.1 ✅ 这 5 种情况,强烈建议用备忘录

序号 场景 原因
需要保存历史状态 购物车 / 编辑器
需要支持撤销/重做 操作可回退
需要状态恢复 事务回滚
需要多版本对比 配置/文档
需要保护封装 不暴露字段

8.2 ❌ 这 5 种情况,绝对不要用备忘录

序号 场景 原因
业务简单 过度设计
不需要历史 浪费内存
状态极少变 没必要
追求简单 类多
业务经常变 难维护

8.3 ⚠️ 备忘录的 6 大常见坑

序号 表现 解决方案
内存爆炸 备忘录过多 限制栈大小
深拷贝漏掉 状态被外部修改 深拷贝
备忘录可修改 破坏不可变 final 字段
undo 错乱 顺序错 索引
序列化失败 备忘录无法存盘 Serializable
撤销范围大 性能差 增量备份

8.4 面试官视角:备忘录高频追问

Q1:备忘录 vs 序列化,区别是什么?

答:备忘录保存状态 (多版本,内存),序列化转字节 (单次,可存盘)。备忘录支持历史栈,序列化单次

Q2:备忘录模式怎么保护封装?

答:备忘录只暴露给 Originator (黑箱模式),Caretaker 只能持有,不能修改。Originator 用宽接口,外部用窄接口。

Q3:Spring @Transactional 是备忘录模式吗?

答:严格说不是经典备忘录 ,但用备忘录思想 ------事务开始保存快照,失败回滚恢复。这是备忘录 + 命令的组合


九、与其他模式协作

9.1 备忘录 + 命令 = 撤销/重做

9.2 备忘录 + 原型 = 深拷贝快照

java 复制代码
/**
 * ✅ 备忘录 + 原型:深拷贝
 */
public CartMemento createMemento() {
    // 用原型模式深拷贝
    return (CartMemento) this.clone();
}

9.3 备忘录 + 享元 = 共享快照

9.4 大白商城模式协作全景图

复制代码
                    ┌──────────────┐
                    │  备忘录      │ ← 本篇
                    └──────┬───────┘
                           │
       ┌───────────┬───────┼───────┬───────────┐
       │           │       │       │           │
   ┌───▼───┐  ┌────▼───┐ ┌▼────┐ ┌▼─────┐  ┌───▼────┐
   │命令   │  │原型    │ │享元  │ │单例  │  │ 观察者 │
   │(撤销) │  │(深拷贝)│ │(共享)│ │(快照)│  │ (事件) │
   └───────┘  └────────┘ └──────┘ └──────┘  └───────┘
   34 篇       10 篇     24 篇     02 篇     30 篇

十、本篇小结 + 下篇预告

10.1 本篇小结(5 个核心要点)

  1. 本质 :备忘录 = 保存对象快照Originator + Memento + Caretaker
  2. 场景 :购物车快照/编辑器撤销/事务回滚,需要保存/恢复历史状态
  3. 对比:备忘录 vs 序列化(状态 vs 字节)/ 备忘录 vs 备份(细粒度 vs 粗粒度)。
  4. Spring @Transactional用备忘录思想,事务前后快照。
  5. 避坑:内存爆炸 / 深拷贝漏掉 / 备忘录可修改 / undo 错乱。

10.2 一句话总结

备忘录不是"换种方式备份数据",是"用三件套保护封装+多版本" 。大白商城从购物车直接覆盖 → 备忘录栈------用户改错可恢复,完整历史可追溯

10.3 知识脑图

复制代码
备忘录模式
├── 3 个核心概念
│   ├── 发起人(Originator)
│   ├── 备忘录(Memento)
│   └── 管理者(Caretaker)
├── 3 大角色
│   ├── 发起人(ShoppingCart)
│   ├── 备忘录(CartMemento)
│   └── 管理者(CartHistory)
├── 2 大能力
│   ├── 撤销(undo)
│   └── 重做(redo)
├── 模式对比
│   ├── vs 序列化(状态 vs 字节)
│   ├── vs 备份(细粒度 vs 粗粒度)
│   └── vs 原型(保存 vs 克隆)
├── Spring @Transactional
│   ├── 事务备忘录
│   ├── TransactionSynchronizationManager
│   └── 事务回滚
└── 模式协作
    ├── + 命令(命令撤销)
    ├── + 原型(深拷贝)
    └── + 享元(共享快照)

10.4 下篇预告

第 41 篇【备忘录模式 - 源码剖析篇】:JDK / Spring 中的备忘录实现

下一篇我们会深入源码,回答三个问题:

  1. Spring StateMachine 怎么用备忘录保存状态?
  2. JDK 的 java.io.Serializable 怎么用备忘录思想?
  3. 数据库事务怎么用备忘录实现回滚?

并附完整的源码解读 + 流程图 + 大白商城的"抄作业"实践。


觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯

相关推荐
SimonKing16 分钟前
白嫖国产多模态大模型:商汤 SenseNova 接入指南
java·后端·程序员
小江的记录本16 分钟前
【ORM框架】MyBatis核心原理、ORM思想、MyBatis vs JPA
java·数据库·后端·spring·spring cloud·oracle·mybatis
_明月28 分钟前
汇川技术股份有限公司面试--Java外包岗
java·面试·职场和发展
BD_Marathon29 分钟前
Hadoop常用端口号
java·大数据·hadoop
青石路35 分钟前
好好的OceanBase官方驱动你不用,非要用第三方驱动,ArrayIndexOutOfBoundsException了吧
java·后端
长谷深风11137 分钟前
读懂 MCP:能力、传输与多 Server 连接
java·大数据·ai·ai agent·mcp·streamable http·agent设计
深念Y39 分钟前
Makefile vs go run:Go 项目构建方式对比
java·开发语言·golang·k8s·编译·流水线·cicd
我命由我123451 小时前
Android Camera - 获取当前设备屏幕的旋转角度、保存帧到外部存储的私有空间
android·java·开发语言·java-ee·android jetpack·android-studio·android runtime
niuniudengdeng1 小时前
从小程序到 WebView:一个理发店管理系统的全栈架构设计与落地实践
java·python