📌 PDF :AI人工智能 --- 大模型微调与部署实战项目
大白话说Java设计模式-40-备忘录模式(业务实战篇):大白商城购物车快照的"状态保存"
📌 一句话本质:备忘录模式就是"保存对象快照",把状态保存起来,需要时可恢复,不破坏封装性。
🏷️ 标签:备忘录模式 / Java 设计模式 / 状态保存 / 撤销 / 大白商城 🎯 适合:初中级后端 / 经常被"状态恢复"困扰的工程师
目录
- 一、业务场景引入:为什么需要"状态保存"?
- 二、反面教材:硬编码状态的"灾难现场"
- [三、模式原理:备忘录的"发起人 + 备忘录 + 管理者"](#三、模式原理:备忘录的"发起人 + 备忘录 + 管理者")
- 四、实战代码:大白商城购物车快照完整实现
- [五、备忘录 vs 序列化 / 备份](#五、备忘录 vs 序列化 / 备份)
- [六、Spring @Transactional 怎么用备忘录?](#六、Spring @Transactional 怎么用备忘录?)
- [七、备忘录 + 命令模式怎么组合?](#七、备忘录 + 命令模式怎么组合?)
- [八、工程决策 Checklist](#八、工程决策 Checklist)
- 九、与其他模式协作
- [十、本篇小结 + 下篇预告](#十、本篇小结 + 下篇预告)
一、业务场景引入:为什么需要"状态保存"?
大白商城 2024 年用户运营团队提出一个需求:
"用户在购物车修改商品(加/删/改数量)后,30 分钟内可一键恢复到任一历史版本!"
我打开代码一看:
java
/**
* ❌ 大白商城老的购物车代码
*/
public class CartServiceV0 {
public void updateCart(String userId, CartDTO cart) {
// ❌ 直接覆盖,没有历史记录
cartDao.update(userId, cart);
}
}
这种代码的痛点:
| 序号 | 问题 | 后果 |
|---|---|---|
| ① | 没有历史记录 | 无法恢复 |
| ② | 修改后无法撤销 | 用户改错无法回退 |
| ③ | 多版本对比困难 | 运营无法分析 |
| ④ | 数据丢失风险 | 误操作无法找回 |
老板要的"用户可恢复历史购物车",怎么搞?
答案就是------备忘录模式。
1.1 大白话讲透备忘录
继续打比方:
场景:用户改购物车(加/删/改数量),想恢复到任一历史版本。
- 错误做法:直接覆盖,没有历史
- 正确做法 :
- 每次修改前 ,先保存一个快照(Memento)
- 快照存入历史栈
- 用户点"恢复"时,从栈里取快照
- 完整恢复购物车
备忘录 = 保存对象快照,需要时可恢复,不破坏封装性。
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 个核心要点)
- 本质 :备忘录 = 保存对象快照 ,Originator + Memento + Caretaker。
- 场景 :购物车快照/编辑器撤销/事务回滚,需要保存/恢复历史状态。
- 对比:备忘录 vs 序列化(状态 vs 字节)/ 备忘录 vs 备份(细粒度 vs 粗粒度)。
- Spring @Transactional :用备忘录思想,事务前后快照。
- 避坑:内存爆炸 / 深拷贝漏掉 / 备忘录可修改 / 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 中的备忘录实现
下一篇我们会深入源码,回答三个问题:
- Spring
StateMachine怎么用备忘录保存状态? - JDK 的
java.io.Serializable怎么用备忘录思想? - 数据库事务怎么用备忘录实现回滚?
并附完整的源码解读 + 流程图 + 大白商城的"抄作业"实践。
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯