备忘录模式
大约 10 分钟约 3012 字
备忘录模式
简介
备忘录(Memento)在不破坏封装的前提下捕获对象的内部状态,以便之后恢复。理解备忘录模式,有助于实现撤销/重做、快照/回滚等功能。
备忘录模式的核心思想是"快照与回滚" —— 在不暴露对象内部结构的情况下,捕获对象在某个时刻的完整状态,并在需要时恢复到该状态。这种模式在文本编辑器的撤销/重做、游戏的存档/读档、数据库事务的回滚等场景中无处不在。
GoF 将备忘录模式描述为:"在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后恢复。" 这里的关键是"不破坏封装性" —— 客户端代码不需要知道对象内部的字段和结构,只需要能保存和恢复状态。
特点
结构分析
UML 类图
+--------------------+ +--------------------+ +--------------------+
| Originator | | Memento | | Caretaker |
| (原发器) |---->| (备忘录) | | (负责人) |
+--------------------+ +--------------------+ +--------------------+
| +Save(): Memento | | -state | | -_history: Stack |
| +Restore(memento) | | +GetState() | | +Backup() |
| +State | +--------------------+ | +Undo() |
+--------------------+ | +Redo() |
+--------------------+撤销/重做流程
编辑 "Hello" --> 备份 --> 编辑 " World" --> 备份 --> 编辑 "!"
State1 Stack: [S1] State2 Stack: [S1, S2] State3
Undo: 弹出 S2, 推入 Redo --> State2 Stack: [S1] Redo: [S2]
Undo: 弹出 S1, 推入 Redo --> State1 Stack: [] Redo: [S2, S1]
Redo: 弹出 S1, 推入 Stack --> State2 Stack: [S1] Redo: [S2]实现
文本编辑器撤销/重做
// 备忘录 — 保存编辑器状态
public record EditorMemento(string Content, int CursorPosition, string Selection)
{
public DateTime SavedAt { get; } = DateTime.UtcNow;
}
// 原发器 — 文本编辑器
public class TextEditor
{
public string Content { get; private set; } = "";
public int CursorPosition { get; private set; }
public string Selection { get; private set; } = "";
public void Type(string text)
{
Content = Content.Insert(CursorPosition, text);
CursorPosition += text.Length;
}
public void Select(int start, int length)
{
Selection = Content.Substring(start, Math.Min(length, Content.Length - start));
}
public void DeleteSelection()
{
if (!string.IsNullOrEmpty(Selection))
{
Content = Content.Replace(Selection, "");
Selection = "";
}
}
// 创建备忘录
public EditorMemento Save() => new(Content, CursorPosition, Selection);
// 恢复备忘录
public void Restore(EditorMemento memento)
{
Content = memento.Content;
CursorPosition = memento.CursorPosition;
Selection = memento.Selection;
}
public void Print() => Console.WriteLine($"内容: \"{Content}\" 光标: {CursorPosition}");
}
// 负责人 — 历史管理器
public class EditorHistory
{
private readonly Stack<EditorMemento> _undoStack = new();
private readonly Stack<EditorMemento> _redoStack = new();
private readonly TextEditor _editor;
public EditorHistory(TextEditor editor) => _editor = editor;
public void Backup() { _undoStack.Push(_editor.Save()); _redoStack.Clear(); }
public void Undo()
{
if (_undoStack.Count == 0) return;
_redoStack.Push(_editor.Save());
_editor.Restore(_undoStack.Pop());
}
public void Redo()
{
if (_redoStack.Count == 0) return;
_undoStack.Push(_editor.Save());
_editor.Restore(_redoStack.Pop());
}
public bool CanUndo => _undoStack.Count > 0;
public bool CanRedo => _redoStack.Count > 0;
}
// 使用
var editor = new TextEditor();
var history = new EditorHistory(editor);
history.Backup(); editor.Type("Hello");
history.Backup(); editor.Type(" World");
history.Backup(); editor.Type("!");
editor.Print(); // "Hello World!"
history.Undo(); editor.Print(); // "Hello World"
history.Undo(); editor.Print(); // "Hello"
history.Redo(); editor.Print(); // "Hello World"游戏状态快照
public record GameState(
int Level, int Score, int Health, int Mana,
double PlayerX, double PlayerY,
List<string> Inventory, Dictionary<string, bool> Flags)
{
// 防止外部修改 — 返回副本
public List<string> GetInventory() => new(Inventory);
public Dictionary<string, bool> GetFlags() => new(Flags);
}
public class GameCharacter
{
public int Level { get; set; } = 1;
public int Score { get; set; }
public int Health { get; set; } = 100;
public int Mana { get; set; } = 50;
public double X { get; set; }
public double Y { get; set; }
public List<string> Inventory { get; set; } = new();
public GameState Save() => new(Level, Score, Health, Mana, X, Y, new List<string>(Inventory), new());
public void Restore(GameState state)
{
Level = state.Level; Score = state.Score;
Health = state.Health; Mana = state.Mana;
X = state.PlayerX; Y = state.PlayerY;
Inventory = state.GetInventory();
}
}
public class SaveManager
{
private readonly Dictionary<string, GameState> _saves = new();
public void QuickSave(GameCharacter character) => _saves["quick"] = character.Save();
public void SlotSave(GameCharacter character, string slot) => _saves[$"slot_{slot}"] = character.Save();
public bool QuickLoad(GameCharacter character)
{
if (!_saves.TryGetValue("quick", out var state)) return false;
character.Restore(state); return true;
}
}配置快照与回滚
public class ConfigSnapshot
{
public Dictionary<string, object> Values { get; }
public string Description { get; }
public DateTime TakenAt { get; }
public ConfigSnapshot(Dictionary<string, object> values, string description)
{
Values = new Dictionary<string, object>(values);
Description = description;
TakenAt = DateTime.UtcNow;
}
}
public class ConfigurationManager
{
private readonly Dictionary<string, object> _config = new();
private readonly Stack<ConfigSnapshot> _history = new();
public void Set(string key, object value) => _config[key] = value;
public T Get<T>(string key) => (T)_config[key];
public ConfigSnapshot Snapshot(string description = "")
{
var snapshot = new ConfigSnapshot(_config, description);
_history.Push(snapshot);
return snapshot;
}
public void Rollback()
{
if (_history.Count == 0) return;
var snapshot = _history.Pop();
_config.Clear();
foreach (var kv in snapshot.Values) _config[kv.Key] = kv.Value;
}
}实战:带容量限制的历史管理器
public class BoundedHistory<T>
{
private readonly int _maxSize;
private readonly LinkedList<T> _history = new();
public BoundedHistory(int maxSize = 50) => _maxSize = maxSize;
public void Push(T item)
{
_history.AddFirst(item);
if (_history.Count > _maxSize)
_history.RemoveLast();
}
public T? Pop()
{
if (_history.Count == 0) return default;
var item = _history.First!.Value;
_history.RemoveFirst();
return item;
}
public T? Peek() => _history.Count > 0 ? _history.First!.Value : default;
public int Count => _history.Count;
public void Clear() => _history.Clear();
}增量快照与性能优化
增量快照 — 只保存变化的部分
// 全量快照 vs 增量快照
public class IncrementalSnapshot
{
public int SnapshotId { get; }
public Dictionary<string, object> Changes { get; }
public DateTime CreatedAt { get; }
public IncrementalSnapshot(int id, Dictionary<string, object> changes)
{
SnapshotId = id;
Changes = new Dictionary<string, object>(changes);
CreatedAt = DateTime.UtcNow;
}
}
public class IncrementalHistoryManager
{
private readonly Dictionary<string, object> _currentState = new();
private readonly Stack<IncrementalSnapshot> _undoStack = new();
private readonly Stack<IncrementalSnapshot> _redoStack = new();
private int _snapshotCounter;
public void Set(string key, object value)
{
// 记录变化前的值用于撤销
var oldValue = _currentState.ContainsKey(key) ? _currentState[key] : null;
_undoStack.Push(new IncrementalSnapshot(++_snapshotCounter,
new Dictionary<string, object> { { key, oldValue! } }));
_currentState[key] = value;
_redoStack.Clear(); // 新操作清空重做栈
}
public void Undo()
{
if (_undoStack.Count == 0) return;
var snapshot = _undoStack.Pop();
var redoChanges = new Dictionary<string, object>();
foreach (var change in snapshot.Changes)
{
// 保存当前值用于重做
redoChanges[change.Key] = _currentState.ContainsKey(change.Key)
? _currentState[change.Key] : null;
// 恢复旧值
if (change.Value == null)
_currentState.Remove(change.Key);
else
_currentState[change.Key] = change.Value;
}
_redoStack.Push(new IncrementalSnapshot(++_snapshotCounter, redoChanges));
}
public void Redo()
{
if (_redoStack.Count == 0) return;
var snapshot = _redoStack.Pop();
var undoChanges = new Dictionary<string, object>();
foreach (var change in snapshot.Changes)
{
undoChanges[change.Key] = _currentState.ContainsKey(change.Key)
? _currentState[change.Key] : null;
if (change.Value == null)
_currentState.Remove(change.Key);
else
_currentState[change.Key] = change.Value;
}
_undoStack.Push(new IncrementalSnapshot(++_snapshotCounter, undoChanges));
}
public T? Get<T>(string key) =>
_currentState.TryGetValue(key, out var value) ? (T)value : default;
}序列化持久化
using System.Text.Json;
public class FileMementoStorage<T>
{
private readonly string _directory;
public FileMementoStorage(string directory)
{
_directory = directory;
Directory.CreateDirectory(directory);
}
public void Save(T state, string name)
{
var json = JsonSerializer.Serialize(state, new JsonSerializerOptions
{
WriteIndented = true
});
var filePath = Path.Combine(_directory, $"{name}_{DateTime.UtcNow:yyyyMMdd_HHmmss}.json");
File.WriteAllText(filePath, json);
}
public T? Load(string filePath)
{
if (!File.Exists(filePath)) return default;
var json = File.ReadAllText(filePath);
return JsonSerializer.Deserialize<T>(json);
}
public T? LoadLatest(string name)
{
var files = Directory.GetFiles(_directory, $"{name}_*.json")
.OrderByDescending(f => f)
.FirstOrDefault();
return files != null ? Load(files) : default;
}
public List<string> ListSnapshots(string name) =>
Directory.GetFiles(_directory, $"{name}_*.json")
.OrderByDescending(f => f)
.ToList();
}带命令模式的撤销/重做
// 命令模式 + 备忘录模式 — 经典组合
public interface ICommand
{
void Execute();
void Undo();
string Description { get; }
}
public class ChangeTextCommand : ICommand
{
private readonly TextEditor _editor;
private readonly string _newText;
private EditorMemento? _beforeState;
public string Description => $"修改文本为: {_newText}";
public ChangeTextCommand(TextEditor editor, string newText)
{
_editor = editor;
_newText = newText;
}
public void Execute()
{
_beforeState = _editor.Save(); // 保存执行前状态
_editor.Type(_newText);
}
public void Undo()
{
if (_beforeState != null)
_editor.Restore(_beforeState); // 恢复到执行前状态
}
}
public class DeleteTextCommand : ICommand
{
private readonly TextEditor _editor;
private EditorMemento? _beforeState;
public string Description => "删除选中文本";
public DeleteTextCommand(TextEditor editor) => _editor = editor;
public void Execute()
{
_beforeState = _editor.Save();
_editor.DeleteSelection();
}
public void Undo()
{
if (_beforeState != null)
_editor.Restore(_beforeState);
}
}
// 命令管理器 — 管理命令栈
public class CommandHistory
{
private readonly Stack<ICommand> _undoStack = new();
private readonly Stack<ICommand> _redoStack = new();
public void Execute(ICommand command)
{
command.Execute();
_undoStack.Push(command);
_redoStack.Clear();
}
public void Undo()
{
if (_undoStack.Count == 0) return;
var command = _undoStack.Pop();
command.Undo();
_redoStack.Push(command);
}
public void Redo()
{
if (_redoStack.Count == 0) return;
var command = _redoStack.Pop();
command.Execute();
_undoStack.Push(command);
}
public void PrintHistory()
{
Console.WriteLine("撤销栈:");
foreach (var cmd in _undoStack)
Console.WriteLine($" - {cmd.Description}");
}
}
// 使用 — 完整的命令 + 备忘录撤销系统
var editor = new TextEditor();
var history = new CommandHistory();
history.Execute(new ChangeTextCommand(editor, "Hello"));
history.Execute(new ChangeTextCommand(editor, " World"));
history.Execute(new ChangeTextCommand(editor, "!"));
editor.Print(); // "Hello World!"
history.Undo(); editor.Print(); // "Hello World"
history.Undo(); editor.Print(); // "Hello"
history.Redo(); editor.Print(); // "Hello World"
history.PrintHistory(); // 显示撤销栈内容备忘录模式的性能考量
// 性能优化策略
// 1. 使用 struct 减少堆分配(小型状态)
public readonly struct MiniState
{
public readonly int X;
public readonly int Y;
public readonly int Health;
public MiniState(int x, int y, int health) { X = x; Y = y; Health = health; }
}
// 2. 使用 Span/Memory 减少拷贝
public class BufferMemento
{
private readonly byte[] _data;
public BufferMemento(ReadOnlySpan<byte> source)
{
_data = source.ToArray(); // 一次性拷贝
}
public ReadOnlySpan<byte> Data => _data;
}
// 3. 压缩大快照
public class CompressedMemento
{
private readonly byte[] _compressed;
public CompressedMemento(string state)
{
var bytes = System.Text.Encoding.UTF8.GetBytes(state);
using var ms = new MemoryStream();
using var gzip = new System.IO.Compression.GZipStream(ms,
System.IO.Compression.CompressionLevel.Fastest);
gzip.Write(bytes);
gzip.Close();
_compressed = ms.ToArray();
}
public string Restore()
{
using var ms = new MemoryStream(_compressed);
using var gzip = new System.IO.Compression.GZipStream(ms,
System.IO.Compression.CompressionMode.Decompress);
using var reader = new StreamReader(gzip);
return reader.ReadToEnd();
}
}备忘录模式 vs 命令模式
备忘录模式 命令模式
+--------+ +--------+
| 保存 | | 封装 |
| 状态 | | 操作 |
+--------+ +--------+
| 状态级 | | 操作级 |
| 快照 | | 记录 |
+--------+ +--------+
| 恢复 | | 撤销 |
| 状态 | | 操作 |
+--------+ +--------+
| 通常 | | 通常 |
| 结合 | | 结合 |
+--------+ +--------+
两者经常结合使用:命令的 Undo 用备忘录保存状态最佳实践
- 使用 record 或不可变对象:备忘录应该是不可变的,防止被意外修改。
- 深拷贝引用类型:List、Dictionary 等引用类型必须深拷贝,否则恢复后修改会污染原数据。
- 限制历史大小:设置合理的容量上限,避免内存无限增长。
- 序列化持久化:需要持久化时,使用 JSON 序列化将备忘录存储到文件或数据库。
- 考虑增量快照:对于大对象,只保存变化的部分(增量快照),减少内存使用。
优点
缺点
总结
备忘录模式通过 Save() 创建状态快照,Restore() 恢复状态。历史管理器维护撤销/重做栈。注意引用类型(List、Dictionary)需要深拷贝。可限制快照数量避免内存溢出。建议在编辑器撤销、游戏存档、配置回滚等需要状态恢复的场景使用备忘录模式。
备忘录模式的本质价值在于:当你需要在对象的整个生命周期中支持状态回退时,备忘录模式提供了一种不破坏封装性的方式来保存和恢复对象状态。配合命令模式,可以实现完整的撤销/重做功能。
关键知识点
- 模式不是目标,降低耦合和控制变化才是目标。
- 先找变化点、稳定点和协作边界,再决定是否引入模式。
- 同一个模式在不同规模下的收益和代价差异很大。
项目落地视角
- 优先画出参与对象、依赖方向和调用链,再落到代码。
- 把模式放到一个真实场景里,比如支付、规则引擎、工作流或插件扩展。
- 配合单元测试或契约测试,保证重构后的行为没有漂移。
常见误区
- 为了看起来"高级"而套模式。
- 把简单问题拆成过多抽象层,导致阅读和排障都变难。
- 只会背 UML,不会解释为什么这里需要这个模式。
进阶路线
- 继续关注模式之间的组合用法,而不是孤立记忆。
- 从业务建模、演进策略和团队协作角度看模式的适用性。
- 把模式结论沉淀为项目模板、基类或约束文档。
适用场景
- 当你准备把《备忘录模式》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在业务规则频繁变化、分支增多或对象协作复杂时引入。
- 当你希望提高扩展性,但又不想把系统拆得过度抽象时,这类主题很有参考价值。
落地建议
- 先识别变化点,再决定是否引入模式,而不是反过来套模板。
- 优先为模式的边界、依赖和调用路径画出简单结构图。
- 把模式落到一个明确场景,例如支付、规则计算、插件扩展或工作流。
排错清单
- 检查抽象层是否过多,导致调用路径和责任不清晰。
- 确认引入模式后是否真的减少了条件分支和重复代码。
- 警惕"为了模式而模式",尤其是在简单业务里。
复盘问题
- 如果把《备忘录模式》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《备忘录模式》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《备忘录模式》最大的收益和代价分别是什么?
