深入浅出设计模式【十八、备忘录模式】
一、备忘录模式介绍
在软件开发中,经常需要实现一些需要状态回滚的功能,例如:
- 撤销 (Undo) 和 重做 (Redo) 操作。
- 游戏存档和读档。
- 事务 (Transaction) 操作中的回滚 (Rollback)。
- 浏览器会话恢复。
直接让外部对象访问并保存一个对象的内部状态,会严重破坏该对象的封装性,因为其内部实现细节可能会暴露给外部,使得未来对它的修改变得困难重重。
备忘录模式通过引入一个独立的“备忘录”对象来解决这个问题。该备忘录对象充当了原始对象状态的快照(Snapshot)的载体。原始对象(称为“原发器”)负责创建备忘录(保存状态)和从备忘录恢复状态。而另一个对象(称为“负责人”)则负责安全地存储备忘录,但它不能(也不应该)操作备忘录内部的内容。这样,状态保存和恢复的职责被清晰地分离,同时严格保证了原发器的封装性。
二、核心概念与意图
-
核心概念:
- 原发器 (Originator): 可以生成自身状态快照的对象,也可以根据快照恢复自身状态。它是需要被保存和恢复状态的那个对象。
- 备忘录 (Memento): 存储原发器内部状态的对象。备忘录的设计要防止原发器以外的对象访问自己。通常,备忘录提供两种接口:
- 宽接口 (Wide Interface): 供原发器使用,允许其访问所有状态,用于恢复。
- 窄接口 (Narrow Interface): 供负责人(Caretaker)使用,它看不到备忘录的内部细节,只能存储和传递备忘录。
- 负责人 (Caretaker): 负责保存备忘录,但不能对备忘录的内容进行操作或检查。它可以存储多个备忘录以实现多级撤销或历史记录。
-
意图:
- 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
- 从而可以在将来将该对象恢复到原先保存的状态。
三、适用场景剖析
备忘录模式在以下场景中非常有效:
- 需要提供撤销 (Undo) 和重做 (Redo) 功能时: 这是备忘录模式最经典的应用场景。用户的操作可以被记录为一系列备忘录,从而实现状态的历史回溯。
- 需要保存和恢复对象状态,但直接暴露其状态会破坏对象的封装性时: 当对象的状态包含私有细节,不希望被其他对象直接访问时。
- 需要生成对象状态快照 (Snapshot) 时: 例如,游戏中的存档功能、虚拟机快照、数据库的检查点(Checkpoint)。
- 需要实现事务回滚时: 在业务操作失败时,可以使用备忘录将参与事务的所有对象回滚到操作前的状态。
四、UML 类图解析(Mermaid)
以下UML类图清晰地展示了备忘录模式的结构和角色间的关系,特别是其保护封装性的巧妙设计:
Originator(原发器):- 拥有需要保存的内部状态 (
-state: Object)。 createMemento(): 创建备忘录。将当前自身状态信息复制到一个新的备忘录对象中,并返回该对象。restore(m: Memento): 恢复状态。传入一个备忘录对象,并将自己的状态重置为该备忘录中所保存的状态。
- 拥有需要保存的内部状态 (
Memento(备忘录):- 是保存原发器状态的对象。其核心字段 (
-state: Object) 通常通过构造函数设置,并且没有公共的setter方法,以保证状态 immutable(不可变)。 - 它提供一个宽接口(如
getSavedState())给Originator,允许其读写状态。但这个接口的可见性应设置为包级私有或通过其他方式,确保只有Originator能访问。 - 对
Caretaker等其他类,它只暴露一个窄接口(如无任何公共方法),使其无法访问内部状态,从而保护了封装性。
- 是保存原发器状态的对象。其核心字段 (
Caretaker(负责人):- 负责保存备忘录,通常使用栈 (
Stack) 或列表来管理多个备忘录,以支持多次撤销。 saveState(originator: Originator): 请求原发器创建备忘录,并将该备忘录保存到历史记录中。undo(originator: Originator): 从历史记录中取出最新的备忘录,并将其交还给原发器进行状态恢复。- 关键:
Caretaker永远不会操作或读取Memento的内部状态。它只负责存储和传递。
- 负责保存备忘录,通常使用栈 (
五、各种实现方式及其优缺点
备忘录模式的实现关键在于如何平衡“状态保存与恢复”和“封装性保护”。
1. 标准实现(基于“宽接口”和“窄接口”)
这是最经典的方式,通过控制接口的可见性来保护封装性。
// Memento class with restricted visibility
// This class is package-private (or a static nested class), so only Originator can access it fully.
class Memento {
private final Object state; // Immutable state
// Package-private constructor, only accessible within the same package (e.g., by Originator)
Memento(Object stateToSave) {
this.state = stateToSave; // Could be a deep copy for safety
}
// Package-private getter, only for Originator
Object getSavedState() {
return state;
}
}
// Originator
public class Originator {
private String state; // The state to be saved
public void setState(String state) {
this.state = state;
System.out.println("State set to: " + state);
}
public String getState() {
return state;
}
// Creates a memento, saving the current state
public Memento createMemento() {
return new Memento(state); // Pass internal state to memento
}
// Restores state from a memento
public void restoreFromMemento(Memento memento) {
state = (String) memento.getSavedState(); // Originator can access the wide interface
System.out.println("State restored to: " + state);
}
}
// Caretaker - Does NOT know the details of Memento
public class Caretaker {
private Stack<Memento> mementoStack = new Stack<>();
public void saveState(Originator originator) {
Memento m = originator.createMemento();
mementoStack.push(m);
}
public void undo(Originator originator) {
if (!mementoStack.isEmpty()) {
Memento previousMemento = mementoStack.pop();
originator.restoreFromMemento(previousMemento);
}
}
}
// Client (in a different package)
public class Client {
public static void main(String[] args) {
Originator originator = new Originator();
Caretaker caretaker = new Caretaker();
originator.setState("State #1");
caretaker.saveState(originator); // Save state
originator.setState("State #2");
caretaker.saveState(originator); // Save state
originator.setState("State #3");
System.out.println("Current State: " + originator.getState());
caretaker.undo(originator); // Undo to State #2
caretaker.undo(originator); // Undo to State #1
}
}
- 优点:
- 完美的封装性保护:
Memento的内部状态对Caretaker和Client是完全隐藏的。 - 职责清晰: 原发器负责状态,负责人负责存储,备忘录负责承载状态。
- 完美的封装性保护:
- 缺点:
- 可能产生大量对象: 如果状态很大或变更频繁,会创建大量备忘录对象,消耗内存。需要使用增量备份等技术优化。
2. 接口隔离实现(使用内部类)
利用Java内部类可以访问外部类私有成员的特性,实现更优雅的宽窄接口隔离。
public class Originator {
private String state;
public Memento createMemento() {
return new Memento(state);
}
public void restore(Memento m) {
this.state = m.getState();
}
// The Memento class is an inner class, giving it access to Originator's private fields.
// But it's public, so Caretaker can see it (narrow interface).
public class Memento {
private final String state;
private Memento(String state) { // Private constructor
this.state = state;
}
// This method is package-private or private.
// Only Originator can access it because it's in the same class.
private String getState() {
return state;
}
}
}
// Caretaker can hold Originator.Memento objects but cannot call getState().
3. 序列化实现
如果状态可以序列化,可以直接将原发器序列化后存储(例如存入文件、数据库)作为备忘录。恢复时再反序列化。
- 优点:
- 实现简单,无需手动管理状态复制。
- 易于实现持久化(存盘)。
- 缺点:
- 序列化可能效率较低。
- 可能暴露更多本应隐藏的字段(可通过
transient关键字排除)。
六、最佳实践
- 管理内存消耗: 这是备忘录模式最大的挑战。对于状态庞大或变化频繁的对象,需谨慎使用。
- 增量备份: 只保存相对于上一个备忘录的变化量,而不是完整状态。
- 设定历史深度: 只保留最近N个备忘录,防止内存无限增长。
- 考虑持久化: 如果需要支持应用重启后的状态恢复(如游戏存档),需要将备忘录序列化到磁盘或数据库中。
- 与原型模式结合: 如果原发器的状态非常复杂,克隆是其内部状态的一部分,可以使用原型模式来快速创建备忘录。
- 明确生命周期:
Caretaker只负责维护备忘录,但不应该知道其内容。确保Memento对象是不可变的,一旦创建就不能被修改,以保证状态的一致性。
七、在开发中的演变和应用
备忘录模式的思想是许多现代技术和架构的基础:
- 版本控制系统 (Git, SVN): 代码仓库的本质就是一个巨大的备忘录集合,每次提交(Commit)都是项目状态的一个备忘录。
checkout和revert命令就是状态恢复。 - 数据库事务与日志: 数据库的预写日志 (WAL) 机制。在事务提交前,更改首先被记录到日志中(创建备忘录)。如果系统崩溃,数据库可以根据日志(备忘录)进行恢复(Redo)或回滚(Undo)未完成的事务。
- 虚拟机和容器快照: VMware、VirtualBox 和 Docker 都提供了快照功能,可以保存虚拟机或容器的某一时刻的完整状态,之后可以随时恢复。
- 前端状态管理 (Redux, Vuex): 这些状态管理库的核心概念之一就是“状态是不可变的”。每次状态更新都不是修改原对象,而是生成一个新的状态对象。这本质上是在连续创建备忘录。强大的“时间旅行调试”功能正是基于此实现。
八、真实开发案例(Java语言内部、知名开源框架、工具)
-
Java Swing -
UndoManagerandUndoableEdit:- Java Swing 的文本组件支持撤销/重做。其内部使用了类似备忘录模式的结构。
UndoableEdit接口类似于Memento,代表一个可撤销的操作。UndoManager是Caretaker,它维护一个UndoableEdit的列表。- 文本组件(如
JTextComponent)是Originator,它可以生成代表文本状态变化的UndoableEdit。
-
游戏开发 - 存档/读档:
- 游戏中的角色(
Originator)拥有复杂的状态(生命值、位置、装备等)。 - 存档时,角色对象将自己的状态序列化成一个存档文件(
Memento)。 - 读档时,从存档文件中反序列化出状态,并恢复给角色对象。
- 游戏管理类是
Caretaker,负责管理存档文件。
- 游戏中的角色(
-
Spring Framework -
@Transactional与回滚:- 虽然Spring的事务管理底层基于AOP和数据库事务,但其回滚机制的理念与备忘录模式相通。
- 在事务开始时,数据库的状态可以被视为一个隐式的“备忘录”。
- 如果事务中发生异常,Spring会命令数据库引擎利用其日志和undo段(相当于备忘录)将数据回滚到事务开始前的状态。
-
Java.util.Date (历史原因):
- 早期的
java.util.Date类有一个getTime()方法(返回long时间戳)和一个接受long时间戳的构造函数。这可以看作是一种最简单的备忘录模式:时间戳就是备忘录,Date对象是原发器。
- 早期的
九、总结
| 方面 | 总结 |
|---|---|
| 模式类型 | 行为型设计模式 |
| 核心意图 | 在不破坏封装性的前提下,捕获并外部化对象的内部状态,以便日后恢复。 |
| 关键角色 | 原发器(Originator), 备忘录(Memento), 负责人(Caretaker) |
| 核心机制 | 1. 状态外部化: Originator 创建 Memento 保存状态。2. 封装保护: Memento 对 Caretaker 隐藏实现细节(窄接口)。3. 状态恢复: Originator 使用 Memento 恢复自身状态(宽接口)。 |
| 主要优点 | 1. 完美保护封装边界,状态细节不泄露。 2. 简化原发器职责,状态恢复逻辑集中。 3. 易于实现撤销/重做、快照、事务回滚等功能。 |
| 主要缺点 | 1. 内存消耗大: 频繁保存大状态对象开销巨大。 2. 潜在复杂度: Caretaker 需管理备忘录生命周期,可能引入复杂性。 |
| 适用场景 | 需要实现撤销/重做、事务回滚、快照/存档、状态恢复等功能,且对封装性要求高的场景。 |
| 最佳实践 | 管理内存(增量备份、历史深度);与持久化结合;确保备忘录不可变。 |
| 现代应用 | 版本控制系统 (Git),数据库事务与WAL,虚拟机快照,前端状态管理 (Redux)。 |
| 真实案例 | Swing撤销功能 (UndoManager),游戏存档,Spring事务回滚 (理念相通)。 |
备忘录模式是状态管理和操作可逆性的强大工具。它通过巧妙的接口设计,在提供强大功能的同时,严格维护了对象的封装性原则,体现了优秀的设计思想。然而,其内存开销要求架构师在应用时必须仔细权衡,并常需结合持久化、增量备份等策略进行优化。它在从GUI到数据库,再到系统架构的众多领域中都发挥着至关重要的作用,是实现高级功能的基石模式之一。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120769

浙公网安备 33010602011771号