
20、Python - 备忘录模式
你有没有遇到过这样的场景:写了一下午的文档,手滑按了删除键,几千字瞬间消失;玩游戏打到最终Boss前忘记存档,被Boss秒杀后只能从头再来;线上系统执行了一个错误的批量操作,数据被改得面目全非,却无法一键回滚。
如果这些场景让你感到痛苦,那么备忘录模式就是你的"后悔药"。它能在不破坏对象封装的前提下,随时保存对象的状态快照,并在需要时精确恢复到任意历史状态。
本文将从痛点出发,带你彻底搞懂备忘录模式的原理、实现、企业级应用以及面试考点,让小白也能看懂并直接上手。
文章目录
- [20、Python - 备忘录模式](#20、Python - 备忘录模式)
-
- 一、痛点场景描述
- 二、痛点的解决方案
- 三、备忘录模式是什么
-
- [3.1 官方定义](#3.1 官方定义)
- [3.2 核心角色](#3.2 核心角色)
- [3.3 大白话解释](#3.3 大白话解释)
- [3.4 生活案例](#3.4 生活案例)
- 四、为什么要用备忘录模式
-
- [4.1 解决了什么问题](#4.1 解决了什么问题)
- [4.2 优点](#4.2 优点)
- [4.3 缺点](#4.3 缺点)
- [4.4 什么时候用,什么时候不用](#4.4 什么时候用,什么时候不用)
- 五、备忘录模式是怎么演进过来的
-
- [5.1 起源:GoF的经典定义](#5.1 起源:GoF的经典定义)
- [5.2 发展:从桌面软件到游戏行业](#5.2 发展:从桌面软件到游戏行业)
- [5.3 成熟:企业级应用与云原生](#5.3 成熟:企业级应用与云原生)
- [5.4 Python中的实践演进](#5.4 Python中的实践演进)
- 六、备忘录模式怎么用
-
- [6.1 标准实现(三角色完整版本)](#6.1 标准实现(三角色完整版本))
- [6.2 Pythonic简化版(利用深拷贝)](#6.2 Pythonic简化版(利用深拷贝))
- [6.3 支持撤销和重做(双栈实现)](#6.3 支持撤销和重做(双栈实现))
- [6.4 持久化备忘录(保存到文件)](#6.4 持久化备忘录(保存到文件))
- 七、企业项目中如何使用
-
- [7.1 电商订单系统的订单修改与撤销](#7.1 电商订单系统的订单修改与撤销)
- [7.2 工作流引擎中的审批撤回](#7.2 工作流引擎中的审批撤回)
- [7.3 配置中心的版本管理与回滚](#7.3 配置中心的版本管理与回滚)
- 八、与最相关的几个竞品对比
-
- [8.1 核心对比表](#8.1 核心对比表)
- [8.2 详细辨析](#8.2 详细辨析)
- [8.3 如何选择](#8.3 如何选择)
- 九、常用场景总结
-
- [9.1 文本编辑器的撤销/重做](#9.1 文本编辑器的撤销/重做)
- [9.2 游戏存档/读档系统](#9.2 游戏存档/读档系统)
- [9.3 数据库事务回滚](#9.3 数据库事务回滚)
- [9.4 工作流审批撤回](#9.4 工作流审批撤回)
- [9.5 图形设计软件的历史记录](#9.5 图形设计软件的历史记录)
- [9.6 配置中心版本管理](#9.6 配置中心版本管理)
- [9.7 代码版本控制](#9.7 代码版本控制)
- [9.8 虚拟机/容器快照](#9.8 虚拟机/容器快照)
- 十、面试官高频面试题
- 十一、总结
一、痛点场景描述

场景一:文本编辑器的噩梦
假设你正在开发一个在线文档编辑器,用户要求支持撤销功能。最朴素的做法是什么?每次用户输入时,把整个文档内容复制一份存到列表里,撤销时就取上一份覆盖回去。
python
# 反例:朴素的撤销实现
class NaiveEditor:
def __init__(self):
self.content = ""
self.history = [] # 存储所有历史内容
def write(self, text):
self.history.append(self.content) # 每次修改前保存完整内容
self.content += text
def undo(self):
if self.history:
self.content = self.history.pop()
看起来没问题?但仔细想想:如果文档有10万字,用户每敲一个字就存一份完整副本,操作1000次就是1亿字的内存占用。而且,如果文档对象里还有光标位置、选中文本、格式化样式等十几个属性,难道要一个个手动复制吗?
更严重的是,为了保存状态,你不得不把所有内部属性都暴露成public,让外部代码可以读写,这直接破坏了面向对象的封装性。
场景二:游戏存档的困境
开发一款RPG游戏,玩家需要在关键节点存档。游戏角色的状态极其复杂:生命值、魔法值、装备列表、背包物品、任务进度、地图坐标、技能冷却......加起来几十个字段。
如果让外部系统直接读取和写入这些状态,不仅代码混乱,而且很容易出现"只恢复了一半状态"的bug。比如恢复了生命值却忘了恢复装备,导致玩家穿着不存在的装备战斗。
场景三:企业系统的回滚需求
在电商订单系统中,用户提交订单后可以在支付前修改订单。如果修改后发现不对,需要撤销修改回到之前的状态。订单对象包含商品列表、收货地址、优惠券、价格计算结果等复杂嵌套结构,简单的字段赋值根本无法正确回滚。
这些场景的共同痛点是:需要保存和恢复对象的完整状态,但又不想破坏封装性,也不想手动处理大量字段的复制逻辑。
二、痛点的解决方案
面对上述痛点,备忘录模式给出了优雅的解决方案:
- 谁的状态谁负责:由对象自己(Originator发起人)创建自己的状态快照(Memento备忘录),外部不需要知道对象内部有哪些字段。
- 快照独立封装:备忘录对象封装了状态的所有细节,对外只暴露一个"黑盒"引用,外部无法读取或修改其中的内容。
- 管理者统一保管:由专门的管理者(Caretaker)负责保存和取出备忘录,它只负责存储,不关心备忘录里装了什么。
- 恢复走原路径:恢复状态时,把备忘录交还给发起人,由发起人自己从备忘录中读取状态并恢复,确保恢复逻辑的正确性。
用大白话来说:备忘录模式就像你去银行租保险箱。你(发起人)把贵重物品(状态)装进箱子(备忘录)锁好,银行(管理者)只负责帮你保管箱子,它没有钥匙也不知道里面装了什么。需要取用时,你拿自己的钥匙打开箱子,东西原封不动地拿出来。
三、备忘录模式是什么

3.1 官方定义
备忘录模式(Memento Pattern),又叫快照模式(Snapshot Pattern)或Token模式,是GoF(四人帮)定义的23种设计模式之一,属于行为型模式。
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。
3.2 核心角色
备忘录模式包含三个核心角色:
| 角色 | 英文名 | 职责 |
|---|---|---|
| 发起人 | Originator | 创建当前状态的备忘录,也可以从备忘录恢复状态 |
| 备忘录 | Memento | 存储发起人对象的内部状态,防止外部访问 |
| 管理者 | Caretaker | 负责保存备忘录对象,但不操作也不检查备忘录内容 |
3.3 大白话解释
- 发起人:就是那个需要保存状态的对象,比如文档编辑器、游戏角色、订单。它知道自己有哪些状态需要保存,也知道怎么从快照中恢复。
- 备忘录:就是那个装着状态快照的"盒子"。它本身很简单,就是把状态存起来,而且只有发起人能打开它。
- 管理者:就是那个"储物柜"。它把一个个备忘录按顺序存起来,需要的时候取出来。它从来不打开备忘录看里面是什么。
3.4 生活案例
生活中最贴切的例子就是游戏存档:
- 你的游戏角色就是发起人,它拥有生命值、装备、等级等状态。
- 当你按下"存档"按钮,游戏系统把当前所有状态写入一个存档文件,这个存档文件就是备忘录。
- 你的硬盘或云存档服务就是管理者,它负责保存这些存档文件,但不会去修改存档内容。
- 当你游戏失败选择"读档"时,游戏从存档文件中读取状态,恢复到存档时的样子。
另一个例子是Photoshop的历史记录面板:每一步操作都会生成一个画布状态的快照,存在历史记录列表里,点击任意一条就能恢复到那个状态。
四、为什么要用备忘录模式
4.1 解决了什么问题
备忘录模式主要解决以下问题:
- 封装性问题:不需要暴露对象的内部属性就能保存和恢复状态。对象的字段可以保持private,外部代码无法直接访问。
- 状态一致性问题:由对象自己负责保存和恢复,确保所有相关字段被一起保存和恢复,不会出现"恢复了一半"的情况。
- 历史管理问题:可以方便地保存多个历史状态,支持多级撤销、时间线回溯等功能。
- 关注点分离:状态的保存/恢复逻辑与业务逻辑分离,管理者只负责存储,职责清晰。
4.2 优点
- 提供恢复机制:用户可以方便地回到任意历史状态,支持撤销、回滚、重做等功能。
- 封装性良好:备忘录对象对外部是"黑盒",只有发起人能访问其内部状态,保护了对象的封装性。
- 简化发起人:状态管理的职责交给了管理者,发起人不需要维护复杂的历史记录逻辑。
- 支持多状态点:可以保存任意多个状态快照,实现多级撤销和版本管理。
4.3 缺点
- 内存消耗大:如果对象状态很大,每次保存都会占用较多内存。频繁保存可能导致内存溢出。
- 类数量增加:需要为每个发起人创建对应的备忘录类,增加了代码的复杂度。
- 深拷贝开销:如果状态包含复杂的嵌套对象,需要进行深拷贝,可能带来性能开销。
4.4 什么时候用,什么时候不用
适用场景:
- 需要实现撤销/重做(Undo/Redo)功能
- 需要保存对象的快照以便后续恢复
- 需要实现事务的回滚机制
- 需要保护对象状态的封装性,不希望外部直接访问内部状态
不适用场景:
- 对象状态非常大,保存快照的内存成本过高
- 状态变化极其频繁,每次变化都保存快照不现实
- 只需要简单的字段回退,不需要完整的状态快照
五、备忘录模式是怎么演进过来的

5.1 起源:GoF的经典定义
备忘录模式最早由Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides四人(俗称"四人帮"GoF)在1995年出版的《设计模式:可复用面向对象软件的基础》一书中正式定义。
在这本书中,备忘录模式被归类为行为型模式,其核心思想是:在不破坏封装的前提下捕获和外部化对象的内部状态,使对象可以在以后恢复到这个状态。
GoF提出这个模式的背景是:当时很多软件系统需要实现撤销功能,但常见的做法要么是暴露对象内部状态(破坏封装),要么是用命令模式记录操作反向执行(复杂且不通用)。备忘录模式提供了第三种选择------直接保存状态快照。
5.2 发展:从桌面软件到游戏行业
2000年代,随着桌面软件的普及,备忘录模式在文本编辑器、图形设计软件中得到广泛应用。Microsoft Word的撤销功能、Adobe Photoshop的历史记录面板,背后都有备忘录模式的思想。
2010年代,游戏行业爆发,存档/读档成为游戏的标配功能。备忘录模式的思想被大量应用于游戏引擎中,用于保存玩家进度、实现检查点系统。很多游戏引擎甚至内置了状态序列化和快照机制。
5.3 成熟:企业级应用与云原生
2020年代以来,备忘录模式在企业级系统中扮演越来越重要的角色:
- 工作流引擎:审批流程中的撤回、回退功能,本质上就是保存流程实例的状态快照并恢复。
- 分布式事务:Saga模式中的补偿操作,很多时候需要保存事务参与者的状态以便回滚。
- 配置中心:配置版本管理和回滚功能,保存每个版本的配置快照。
- 云原生:容器的快照、虚拟机的检查点(Checkpoint),都是备忘录模式思想在基础设施层的体现。
5.4 Python中的实践演进
在Python中,备忘录模式的实现也经历了演进:
- 早期:手动实现三个角色,每个类都要写save和restore方法。
- 进阶 :利用
copy.deepcopy实现通用的深拷贝快照,减少重复代码。 - 现代 :结合
dataclass、typing等特性实现类型安全的备忘录,或者用pickle序列化实现状态的持久化存储。
六、备忘录模式怎么用

6.1 标准实现(三角色完整版本)
我们以文本编辑器为例,实现标准的备忘录模式。
python
from typing import List, Optional
class Memento:
"""备忘录类:保存编辑器的状态快照"""
def __init__(self, content: str, cursor_pos: int):
# 使用私有属性存储状态,外部无法直接访问
self._content = content
self._cursor_pos = cursor_pos
def get_content(self) -> str:
"""只有发起人能调用这个方法获取状态"""
return self._content
def get_cursor_pos(self) -> int:
return self._cursor_pos
class TextEditor:
"""发起人类:文本编辑器,负责创建和恢复备忘录"""
def __init__(self):
self._content = ""
self._cursor_pos = 0
def write(self, text: str):
"""写入文本"""
self._content = self._content[:self._cursor_pos] + text + self._content[self._cursor_pos:]
self._cursor_pos += len(text)
def delete(self, length: int = 1):
"""删除光标前的字符"""
if self._cursor_pos >= length:
self._content = self._content[:self._cursor_pos - length] + self._content[self._cursor_pos:]
self._cursor_pos -= length
def save(self) -> Memento:
"""创建当前状态的备忘录"""
return Memento(self._content, self._cursor_pos)
def restore(self, memento: Memento):
"""从备忘录恢复状态"""
self._content = memento.get_content()
self._cursor_pos = memento.get_cursor_pos()
def show(self):
"""显示当前内容和光标位置"""
print(f"内容: '{self._content}'")
print(f"光标位置: {self._cursor_pos}")
print("-" * 40)
class HistoryManager:
"""管理者类:负责保存和管理备忘录"""
def __init__(self, max_size: int = 50):
self._history: List[Memento] = []
self._max_size = max_size
def push(self, memento: Memento):
"""保存一个备忘录"""
self._history.append(memento)
# 限制历史记录数量,防止内存溢出
if len(self._history) > self._max_size:
self._history.pop(0)
def pop(self) -> Optional[Memento]:
"""取出最近的备忘录(撤销)"""
if self._history:
return self._history.pop()
return None
def size(self) -> int:
return len(self._history)
# 使用示例
if __name__ == "__main__":
editor = TextEditor()
history = HistoryManager()
# 第一次编辑
editor.write("Hello")
history.push(editor.save())
editor.show()
# 第二次编辑
editor.write(" World")
history.push(editor.save())
editor.show()
# 删除操作
editor.delete(5)
editor.show()
# 撤销一次
memento = history.pop()
if memento:
editor.restore(memento)
editor.show()
# 再撤销一次
memento = history.pop()
if memento:
editor.restore(memento)
editor.show()
运行输出:
text
内容: 'Hello'
光标位置: 5
----------------------------------------
内容: 'Hello World'
光标位置: 11
----------------------------------------
内容: 'Hello '
光标位置: 6
----------------------------------------
内容: 'Hello World'
光标位置: 11
----------------------------------------
内容: 'Hello'
光标位置: 5
----------------------------------------
6.2 Pythonic简化版(利用深拷贝)
Python中可以利用copy.deepcopy实现更简洁的通用备忘录,不需要为每个类手写备忘录类。
python
import copy
from typing import List, Optional, Any
class Snapshot:
"""通用备忘录:保存对象的深拷贝快照"""
def __init__(self, state: dict):
# 深拷贝确保快照不受后续修改影响
self._state = copy.deepcopy(state)
def get_state(self) -> dict:
return self._state
class OriginatorMixin:
"""发起人混入类:为任意对象添加保存和恢复能力"""
def save(self) -> Snapshot:
"""保存当前所有属性的快照"""
return Snapshot(self.__dict__.copy())
def restore(self, snapshot: Snapshot):
"""从快照恢复所有属性"""
self.__dict__.clear()
self.__dict__.update(snapshot.get_state())
class GameCharacter(OriginatorMixin):
"""游戏角色:继承混入类即可获得存档能力"""
def __init__(self, name: str):
self.name = name
self.hp = 100
self.mp = 50
self.level = 1
self.inventory = ["新手剑", "红药水"]
self.position = {"x": 0, "y": 0}
def take_damage(self, damage: int):
self.hp = max(0, self.hp - damage)
def use_mp(self, amount: int):
self.mp = max(0, self.mp - amount)
def level_up(self):
self.level += 1
self.hp += 20
self.mp += 10
def move(self, x: int, y: int):
self.position["x"] = x
self.position["y"] = y
def show(self):
print(f"[{self.name}] Lv.{self.level} HP:{self.hp} MP:{self.mp}")
print(f" 背包: {self.inventory}")
print(f" 位置: ({self.position['x']}, {self.position['y']})")
print("-" * 50)
class SaveManager:
"""存档管理器"""
def __init__(self):
self._saves: List[Snapshot] = []
def save_game(self, character: GameCharacter):
self._saves.append(character.save())
print(f"已存档,当前共有 {len(self._saves)} 个存档")
def load_game(self, character: GameCharacter, index: int = -1):
if 0 <= abs(index) <= len(self._saves):
character.restore(self._saves[index])
print("读档成功")
else:
print("存档不存在")
# 使用示例
if __name__ == "__main__":
player = GameCharacter("勇者小明")
save_manager = SaveManager()
player.show()
# 第一个存档点
save_manager.save_game(player)
# 游戏进行中
player.move(10, 20)
player.take_damage(30)
player.level_up()
player.inventory.append("屠龙刀")
player.show()
# 第二个存档点
save_manager.save_game(player)
# 继续游戏,被Boss秒杀
player.take_damage(100)
player.show()
# 读档回到第二个存档点
save_manager.load_game(player)
player.show()
# 读档回到第一个存档点
save_manager.load_game(player, 0)
player.show()
6.3 支持撤销和重做(双栈实现)
很多场景需要同时支持撤销(Undo)和重做(Redo),可以用两个栈来实现。
python
from typing import List, Optional
class UndoRedoManager:
"""支持撤销和重做的管理者"""
def __init__(self, max_history: int = 100):
self._undo_stack: List[Any] = []
self._redo_stack: List[Any] = []
self._max_history = max_history
def record(self, memento: Any):
"""记录新状态,清空重做栈"""
self._undo_stack.append(memento)
self._redo_stack.clear() # 新操作后之前的重做记录失效
if len(self._undo_stack) > self._max_history:
self._undo_stack.pop(0)
def undo(self) -> Optional[Any]:
"""撤销:返回上一个状态"""
if self._undo_stack:
memento = self._undo_stack.pop()
self._redo_stack.append(memento)
return memento
return None
def redo(self) -> Optional[Any]:
"""重做:返回被撤销的状态"""
if self._redo_stack:
memento = self._redo_stack.pop()
self._undo_stack.append(memento)
return memento
return None
def can_undo(self) -> bool:
return len(self._undo_stack) > 0
def can_redo(self) -> bool:
return len(self._redo_stack) > 0
6.4 持久化备忘录(保存到文件)
有时候需要把备忘录持久化到磁盘,比如游戏存档、文档自动保存。Python中可以用pickle序列化。
python
import pickle
import os
from typing import Optional
class PersistentMemento:
"""可持久化的备忘录"""
def __init__(self, state: dict):
self._state = state
def get_state(self) -> dict:
return self._state
def save_to_file(self, filepath: str):
"""将备忘录保存到文件"""
with open(filepath, 'wb') as f:
pickle.dump(self._state, f)
@classmethod
def load_from_file(cls, filepath: str) -> Optional['PersistentMemento']:
"""从文件加载备忘录"""
if not os.path.exists(filepath):
return None
with open(filepath, 'rb') as f:
state = pickle.load(f)
return cls(state)
七、企业项目中如何使用

7.1 电商订单系统的订单修改与撤销
在电商系统中,用户提交订单后可以在支付前修改订单信息(如更换商品、修改收货地址)。如果用户修改后想撤销修改,需要恢复到修改前的状态。
python
import copy
import uuid
from datetime import datetime
from typing import List, Dict, Optional
class OrderMemento:
"""订单备忘录:保存订单的完整状态"""
def __init__(self, order_id: str, items: List[Dict], address: str,
coupon_id: Optional[str], total_price: float, status: str):
self.order_id = order_id
self.items = copy.deepcopy(items)
self.address = address
self.coupon_id = coupon_id
self.total_price = total_price
self.status = status
self.snapshot_time = datetime.now()
class Order:
"""订单类(发起人)"""
def __init__(self, user_id: str):
self.order_id = str(uuid.uuid4())
self.user_id = user_id
self.items: List[Dict] = []
self.address = ""
self.coupon_id: Optional[str] = None
self.total_price = 0.0
self.status = "pending" # pending, paid, shipped, completed, cancelled
self.created_at = datetime.now()
def add_item(self, product_id: str, name: str, price: float, quantity: int):
"""添加商品"""
for item in self.items:
if item["product_id"] == product_id:
item["quantity"] += quantity
break
else:
self.items.append({
"product_id": product_id,
"name": name,
"price": price,
"quantity": quantity
})
self._recalculate_price()
def remove_item(self, product_id: str):
"""移除商品"""
self.items = [item for item in self.items if item["product_id"] != product_id]
self._recalculate_price()
def update_address(self, address: str):
"""更新收货地址"""
self.address = address
def apply_coupon(self, coupon_id: str, discount: float):
"""使用优惠券"""
self.coupon_id = coupon_id
self.total_price = max(0, self.total_price - discount)
def _recalculate_price(self):
"""重新计算总价"""
self.total_price = sum(item["price"] * item["quantity"] for item in self.items)
def save_snapshot(self) -> OrderMemento:
"""创建订单快照"""
return OrderMemento(
order_id=self.order_id,
items=self.items,
address=self.address,
coupon_id=self.coupon_id,
total_price=self.total_price,
status=self.status
)
def restore_snapshot(self, memento: OrderMemento):
"""从快照恢复订单"""
self.items = copy.deepcopy(memento.items)
self.address = memento.address
self.coupon_id = memento.coupon_id
self.total_price = memento.total_price
self.status = memento.status
def show(self):
print(f"订单号: {self.order_id}")
print(f"状态: {self.status}")
print(f"收货地址: {self.address}")
print(f"优惠券: {self.coupon_id}")
print(f"总价: ¥{self.total_price:.2f}")
print("商品列表:")
for item in self.items:
print(f" - {item['name']} x{item['quantity']} = ¥{item['price'] * item['quantity']:.2f}")
print("=" * 50)
class OrderHistoryManager:
"""订单历史管理器(管理者)"""
def __init__(self):
self._snapshots: Dict[str, List[OrderMemento]] = {}
def save_snapshot(self, order: Order):
"""保存订单快照"""
if order.order_id not in self._snapshots:
self._snapshots[order.order_id] = []
self._snapshots[order.order_id].append(order.save_snapshot())
print(f"订单 {order.order_id} 已保存快照,共 {len(self._snapshots[order.order_id])} 个历史版本")
def undo(self, order: Order) -> bool:
"""撤销到上一个版本"""
snapshots = self._snapshots.get(order.order_id, [])
if len(snapshots) >= 2:
snapshots.pop() # 移除当前版本
previous = snapshots[-1] # 取上一个版本
order.restore_snapshot(previous)
print("撤销成功")
return True
print("没有可撤销的历史版本")
return False
def get_history(self, order_id: str) -> List[OrderMemento]:
return self._snapshots.get(order_id, [])
# 企业项目使用示例
if __name__ == "__main__":
# 创建订单
order = Order(user_id="user_1001")
history_manager = OrderHistoryManager()
# 添加商品
order.add_item("P001", "机械键盘", 299.0, 1)
order.add_item("P002", "鼠标垫", 29.9, 2)
order.update_address("北京市朝阳区xxx街道")
history_manager.save_snapshot(order) # 快照1:初始订单
order.show()
# 用户修改订单:添加显示器
order.add_item("P003", "27寸显示器", 1599.0, 1)
history_manager.save_snapshot(order) # 快照2:添加显示器后
order.show()
# 用户使用优惠券
order.apply_coupon("COUPON_100", 100.0)
history_manager.save_snapshot(order) # 快照3:使用优惠券后
order.show()
# 用户觉得显示器太贵,撤销修改
history_manager.undo(order) # 回到快照2
order.show()
# 再撤销一次,回到初始订单
history_manager.undo(order) # 回到快照1
order.show()
7.2 工作流引擎中的审批撤回
在OA审批系统中,审批人可以撤回已经提交的审批,回到上一个节点。这本质上就是保存流程实例的状态快照。
python
from enum import Enum
from typing import List, Dict, Any
import copy
class ProcessStatus(Enum):
DRAFT = "草稿"
PENDING = "审批中"
APPROVED = "已通过"
REJECTED = "已拒绝"
WITHDRAWN = "已撤回"
class ProcessMemento:
"""流程实例备忘录"""
def __init__(self, status: ProcessStatus, current_node: str,
approvers: List[str], approval_history: List[Dict],
form_data: Dict[str, Any]):
self.status = status
self.current_node = current_node
self.approvers = copy.deepcopy(approvers)
self.approval_history = copy.deepcopy(approval_history)
self.form_data = copy.deepcopy(form_data)
class WorkflowInstance:
"""工作流实例(发起人)"""
def __init__(self, process_id: str, applicant: str):
self.process_id = process_id
self.applicant = applicant
self.status = ProcessStatus.DRAFT
self.current_node = "start"
self.approvers: List[str] = []
self.approval_history: List[Dict] = []
self.form_data: Dict[str, Any] = {}
def submit(self, form_data: Dict[str, Any]):
"""提交审批"""
self.form_data = form_data
self.status = ProcessStatus.PENDING
self.current_node = "manager_approval"
self.approvers = ["manager_001"]
def approve(self, approver: str, comment: str = ""):
"""审批通过"""
self.approval_history.append({
"approver": approver,
"action": "approve",
"comment": comment
})
if self.current_node == "manager_approval":
self.current_node = "director_approval"
self.approvers = ["director_001"]
elif self.current_node == "director_approval":
self.status = ProcessStatus.APPROVED
self.current_node = "end"
self.approvers = []
def reject(self, approver: str, comment: str = ""):
"""审批拒绝"""
self.approval_history.append({
"approver": approver,
"action": "reject",
"comment": comment
})
self.status = ProcessStatus.REJECTED
self.current_node = "end"
def save_snapshot(self) -> ProcessMemento:
return ProcessMemento(
status=self.status,
current_node=self.current_node,
approvers=self.approvers,
approval_history=self.approval_history,
form_data=self.form_data
)
def restore_snapshot(self, memento: ProcessMemento):
self.status = memento.status
self.current_node = memento.current_node
self.approvers = copy.deepcopy(memento.approvers)
self.approval_history = copy.deepcopy(memento.approval_history)
self.form_data = copy.deepcopy(memento.form_data)
def show(self):
print(f"流程: {self.process_id}")
print(f"状态: {self.status.value}")
print(f"当前节点: {self.current_node}")
print(f"当前审批人: {self.approvers}")
print(f"审批历史: {len(self.approval_history)} 条")
print("-" * 40)
class WorkflowHistory:
"""工作流历史管理器"""
def __init__(self):
self._history: Dict[str, List[ProcessMemento]] = {}
def record(self, instance: WorkflowInstance):
if instance.process_id not in self._history:
self._history[instance.process_id] = []
self._history[instance.process_id].append(instance.save_snapshot())
def withdraw(self, instance: WorkflowInstance) -> bool:
"""撤回到上一个状态"""
history = self._history.get(instance.process_id, [])
if len(history) >= 2:
history.pop()
instance.restore_snapshot(history[-1])
print("撤回成功")
return True
print("无法撤回")
return False
# 使用示例
if __name__ == "__main__":
wf = WorkflowInstance("WF_001", "zhangsan")
history = WorkflowHistory()
# 提交申请
wf.submit({"title": "出差申请", "days": 3})
history.record(wf)
wf.show()
# 经理审批通过
wf.approve("manager_001", "同意")
history.record(wf)
wf.show()
# 发现有误,撤回
history.withdraw(wf)
wf.show()
7.3 配置中心的版本管理与回滚
在微服务架构中,配置中心需要支持配置版本管理和一键回滚。
python
import json
import time
from typing import Dict, List, Optional
class ConfigSnapshot:
"""配置快照"""
def __init__(self, version: int, config: Dict, operator: str):
self.version = version
self.config = config.copy()
self.operator = operator
self.timestamp = time.time()
class ConfigCenter:
"""配置中心(发起人)"""
def __init__(self, app_name: str):
self.app_name = app_name
self._config: Dict[str, str] = {}
self._version = 0
def set(self, key: str, value: str):
self._config[key] = value
def delete(self, key: str):
self._config.pop(key, None)
def get(self, key: str) -> Optional[str]:
return self._config.get(key)
def get_all(self) -> Dict[str, str]:
return self._config.copy()
def publish(self, operator: str) -> ConfigSnapshot:
"""发布配置,生成快照"""
self._version += 1
return ConfigSnapshot(self._version, self._config, operator)
def rollback(self, snapshot: ConfigSnapshot):
"""回滚到指定版本"""
self._config = snapshot.config.copy()
self._version = snapshot.version
def show(self):
print(f"[{self.app_name}] 当前版本: v{self._version}")
for k, v in self._config.items():
print(f" {k} = {v}")
print("-" * 40)
class ConfigVersionManager:
"""配置版本管理器"""
def __init__(self):
self._versions: Dict[str, List[ConfigSnapshot]] = {}
def publish(self, config_center: ConfigCenter, operator: str):
snapshot = config_center.publish(operator)
if config_center.app_name not in self._versions:
self._versions[config_center.app_name] = []
self._versions[config_center.app_name].append(snapshot)
print(f"配置发布成功,当前版本 v{snapshot.version}")
def rollback_to(self, config_center: ConfigCenter, version: int) -> bool:
versions = self._versions.get(config_center.app_name, [])
for snapshot in versions:
if snapshot.version == version:
config_center.rollback(snapshot)
print(f"已回滚到版本 v{version}")
return True
print(f"版本 v{version} 不存在")
return False
def list_versions(self, app_name: str) -> List[ConfigSnapshot]:
return self._versions.get(app_name, [])
# 使用示例
if __name__ == "__main__":
cc = ConfigCenter("order-service")
vm = ConfigVersionManager()
# v1 初始配置
cc.set("db.host", "10.0.0.1")
cc.set("db.port", "3306")
cc.set("cache.ttl", "300")
vm.publish(cc, "admin")
cc.show()
# v2 修改配置
cc.set("db.host", "10.0.0.2")
cc.set("cache.ttl", "600")
cc.set("log.level", "debug")
vm.publish(cc, "admin")
cc.show()
# v3 出问题了,回滚到v1
vm.rollback_to(cc, 1)
cc.show()
八、与最相关的几个竞品对比

备忘录模式、命令模式和原型模式都涉及"保存状态"或"复制对象"的概念,经常被混淆。下面从多个维度进行详细对比。
8.1 核心对比表
| 对比维度 | 备忘录模式 (Memento) | 命令模式 (Command) | 原型模式 (Prototype) |
|---|---|---|---|
| 核心目的 | 保存和恢复对象状态 | 将请求封装为对象,支持操作的参数化、队列化和撤销 | 通过克隆已有对象创建新对象 |
| 是否保存状态 | 是,主动保存完整状态快照 | 可选,命令对象可存储操作前的部分状态 | 是,创建对象的完整副本 |
| 撤销实现方式 | 直接恢复状态快照 | 通过执行反向操作(undo方法) | 不直接支持撤销 |
| 状态可见性 | 备忘录对外部是黑盒,只有发起人能访问 | 命令对象的状态通常是公开的 | 克隆对象的状态完全公开 |
| 典型场景 | 文本编辑器撤销、游戏存档、事务回滚 | 菜单按钮、操作队列、宏命令、多级撤销 | 创建成本高的对象、批量创建相似对象 |
| 内存消耗 | 高,每个快照保存完整状态 | 中,只保存操作相关参数 | 高,每个克隆都是完整对象 |
| 类数量 | 每个发起人需要对应备忘录类 | 每个操作需要一个命令类 | 每个原型类需要实现克隆方法 |
| 优点 | 封装性好,恢复简单直接 | 解耦调用者和执行者,支持操作队列和日志 | 创建对象高效,避免重复初始化 |
| 缺点 | 内存消耗大,状态大时不适用 | 类数量膨胀,反向操作实现复杂 | 深拷贝实现复杂,嵌套对象需注意 |
8.2 详细辨析
备忘录模式 vs 命令模式:
两者都能实现撤销功能,但思路完全不同:
- 备忘录模式是"状态派":我不管你做了什么操作,我直接把操作前的完整状态存下来,撤销时用旧状态覆盖新状态。简单粗暴,但内存消耗大。
- 命令模式是"操作派":我把每一步操作封装成命令对象,每个命令对象知道自己怎么执行(execute),也知道怎么反向执行(undo)。撤销时不是恢复状态,而是执行反向操作。内存消耗小,但每个操作都要写反向逻辑。
举个例子:你在画板上画了一个红色的圆。
- 备忘录模式:保存画圆之前整个画布的像素快照。撤销时直接用旧画布覆盖。
- 命令模式:保存"在(x,y)位置画一个半径r的红色圆"这个命令。撤销时执行"在(x,y)位置擦除一个半径r的圆"。
备忘录模式 vs 原型模式:
两者都涉及对象复制,但目的不同:
- 备忘录模式:复制是为了"保存",将来要恢复到原对象。复制出来的备忘录是被动存储的,不会被当作独立对象使用。
- 原型模式:复制是为了"创建",复制出来的是一个全新的独立对象,可以被修改和使用,与原对象互不影响。
简单说:备忘录是"备份",原型是"克隆"。备份是为了还原,克隆是为了得到一个新个体。
8.3 如何选择
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| 需要保存完整状态,状态结构复杂 | 备忘录模式 | 自动保存所有字段,不会遗漏 |
| 操作可逆,且反向操作容易实现 | 命令模式 | 内存消耗小,支持操作队列 |
| 需要创建大量相似对象,初始化成本高 | 原型模式 | 克隆比重新创建高效 |
| 同时需要撤销和操作队列 | 命令模式 + 备忘录模式 | 命令记录操作,备忘录保存关键状态 |
| 大对象的频繁撤销 | 命令模式 | 避免每次保存完整快照的内存开销 |
九、常用场景总结

9.1 文本编辑器的撤销/重做
这是备忘录模式最经典的应用场景。每输入一段文字或执行一个操作,就保存当前文档的状态快照到历史栈中。按下Ctrl+Z时弹出最近的快照恢复,Ctrl+Y则重做。
实际应用: Microsoft Word、Google Docs、Notion、飞书文档等所有现代文档编辑器。
9.2 游戏存档/读档系统
游戏角色的状态非常复杂(生命值、装备、背包、任务进度、位置等),备忘录模式可以一次性保存完整状态,读档时完整恢复。
实际应用: 单机游戏的存档系统、MMORPG的检查点、云存档服务。
9.3 数据库事务回滚
在数据库事务中,如果执行过程中出现错误,需要回滚到事务开始前的状态。数据库的Undo Log本质上就是备忘录模式的思想------保存修改前的数据以便回滚。
实际应用: MySQL的InnoDB引擎Undo Log、分布式事务Saga模式的补偿操作。
9.4 工作流审批撤回
在OA系统中,审批流程可以撤回,回到上一个审批节点。需要保存流程实例在每个节点的状态(当前节点、审批人、表单数据等)。
实际应用: 飞书审批、钉钉审批、企业OA系统。
9.5 图形设计软件的历史记录
Photoshop、Figma等设计软件支持多级撤销,每一步操作(画图形、改颜色、移动图层)都会生成画布状态的快照。
实际应用: Adobe Photoshop、Figma、Sketch、Canva。
9.6 配置中心版本管理
微服务配置中心需要保存每个版本的配置快照,支持一键回滚到任意历史版本。
实际应用: Nacos、Apollo、Spring Cloud Config。
9.7 代码版本控制
Git等版本控制系统的核心思想就是备忘录模式:每次commit保存整个代码库的状态快照,可以checkout到任意历史版本。
实际应用: Git、SVN、Mercurial。
9.8 虚拟机/容器快照
云计算中的虚拟机快照、容器检查点(Checkpoint)功能,保存整个运行时状态,支持快速恢复。
实际应用: VMware快照、Docker Checkpoint、AWS EC2 AMI。
十、面试官高频面试题

面试题1:备忘录模式有哪些核心角色?各自的职责是什么?
参考答案:
备忘录模式有三个核心角色:
-
发起人(Originator):需要保存状态的对象。负责创建一个包含当前内部状态的备忘录对象,也负责从备忘录中恢复状态。它是唯一能够访问备忘录内部状态的对象。
-
备忘录(Memento):存储发起人对象的内部状态。备忘录应该保护其内容不被发起人以外的对象访问。通常设计为不可变对象,构造时传入状态,只提供getter方法。
-
管理者(Caretaker):负责保存备忘录对象,但不对备忘录的内容进行操作或检查。它只负责存储和提供备忘录,不知道备忘录里装了什么。常见实现是用一个列表或栈来保存多个备忘录。
面试题2:如何保证备忘录的封装性不被破坏?
参考答案:
保证封装性的关键是让备忘录的内部状态只能被发起人访问,管理者和其他对象只能持有备忘录的引用,不能读取或修改其内容。具体手段包括:
-
将备忘录设计为不可变对象:构造函数传入状态后,不再提供setter方法,状态只能在创建时设置。
-
使用私有内部类:在Java等语言中,可以将备忘录类作为发起人的私有内部类,这样外部完全无法访问备忘录的类定义和方法。
-
窄接口和宽接口分离:对管理者提供窄接口(只有空接口或标记接口),对发起人提供宽接口(可以访问所有状态)。管理者只能通过窄接口持有备忘录引用。
-
Python中的约定:Python没有真正的私有,但可以通过以下方式约定:
- 属性名前加双下划线(如
_state),触发名称改写 - 备忘录类的方法文档中注明"仅发起人可调用"
- 使用
@property只提供getter,不提供setter
- 属性名前加双下划线(如
python
# Python中保证封装性的示例
class Memento:
def __init__(self, state):
self.__state = state # 双下划线名称改写
def _get_state(self): # 约定仅内部使用
return self.__state
面试题3:备忘录模式和命令模式都能实现撤销,有什么区别?
参考答案:
两者实现撤销的思路完全不同:
| 维度 | 备忘录模式 | 命令模式 |
|---|---|---|
| 撤销原理 | 恢复之前保存的完整状态快照 | 执行操作的反向操作(undo) |
| 内存消耗 | 高,每个快照保存完整状态 | 低,只保存操作参数 |
| 实现复杂度 | 低,保存和恢复状态即可 | 高,每个操作都要写反向逻辑 |
| 适用场景 | 状态结构复杂、反向操作难实现 | 操作可逆、状态变化大、频繁撤销 |
| 状态可见性 | 备忘录是黑盒,封装性好 | 命令对象状态通常公开 |
举例说明: 在文本编辑器中删除一段文字:
- 备忘录模式:保存删除前的完整文档内容,撤销时用旧内容覆盖。
- 命令模式:保存"在位置p删除长度为n的文字"这个命令,撤销时执行"在位置p插入被删除的文字"。
实际项目中,两者经常结合使用:命令模式负责记录操作和实现撤销逻辑,对于复杂操作(如滤镜效果),命令对象内部会用备忘录模式保存操作前的状态快照。
面试题4:备忘录模式有什么缺点?如何优化内存消耗?
参考答案:
主要缺点:
- 内存消耗大:如果对象状态很大,每次保存都会占用较多内存。保存大量快照可能导致内存溢出。
- 类数量增加:需要为每个发起人创建对应的备忘录类,增加代码复杂度。
- 深拷贝开销:状态包含复杂嵌套对象时,深拷贝有性能开销。
内存优化策略:
- 限制历史记录数量:设置最大保存数量,超过后删除最旧的快照(环形缓冲区)。
python
class BoundedHistory:
def __init__(self, max_size=20):
self._history = []
self._max_size = max_size
def push(self, memento):
self._history.append(memento)
if len(self._history) > self._max_size:
self._history.pop(0) # 移除最旧的
-
增量快照:只保存与上一个状态的差异(delta),而不是完整状态。恢复时从基线状态依次应用差异。
-
状态压缩:对备忘录对象进行序列化和压缩(如使用gzip、zlib),用CPU时间换内存空间。
-
持久化存储:将不常用的历史快照写入磁盘或数据库,需要时再加载到内存。
-
引用共享:对于状态中不变的大对象(如图片、配置),使用引用共享而不是复制,只复制变化的部分(写时复制COW)。
-
选择合适的时机保存:不是每次状态变化都保存,而是在关键节点(如用户操作完成后、事务提交前)保存。
面试题5:深拷贝和浅拷贝在备忘录模式中如何选择?
参考答案:
这是一个非常重要的问题,选择错误会导致严重的bug。
浅拷贝(copy.copy):只复制对象本身,不复制内部引用的对象。原对象和拷贝对象共享内部子对象。
深拷贝(copy.deepcopy):递归复制对象及其所有引用的子对象。原对象和拷贝对象完全独立。
选择原则:
- 如果状态只包含不可变对象(int、str、tuple、frozenset),浅拷贝即可,因为不可变对象无法被修改,共享引用没有风险。
- 如果状态包含可变对象(list、dict、set、自定义对象),必须使用深拷贝。否则修改原对象的可变属性会影响备忘录中的状态,导致恢复时状态不正确。
python
import copy
class GameCharacter:
def __init__(self):
self.hp = 100 # 不可变
self.inventory = [] # 可变!
self.position = {"x": 0, "y": 0} # 可变!
# 错误:浅拷贝
def save_wrong(self):
return copy.copy(self.__dict__) # inventory和position还是共享的
# 正确:深拷贝
def save_correct(self):
return copy.deepcopy(self.__dict__) # 完全独立
深拷贝的注意事项:
- 深拷贝会递归复制所有对象,对于包含循环引用的对象可能出问题(Python的deepcopy已经处理了循环引用)。
- 深拷贝性能开销大,大对象要注意。
- 某些对象不可拷贝(如文件句柄、网络连接、线程对象),深拷贝会报错,需要自定义
__deepcopy__方法。
面试题6:请设计一个支持撤销和重做的文本编辑器
参考答案:
这是一个经典的系统设计题,完整的实现需要用到备忘录模式 + 双栈结构:
python
import copy
class TextEditor:
def __init__(self):
self.content = ""
self.cursor_pos = 0
self._undo_stack = []
self._redo_stack = []
def _create_snapshot(self):
"""创建当前状态的快照"""
return {
"content": self.content,
"cursor_pos": self.cursor_pos
}
def _restore_snapshot(self, snapshot):
"""从快照恢复状态"""
self.content = snapshot["content"]
self.cursor_pos = snapshot["cursor_pos"]
def _record_state(self):
"""记录当前状态到撤销栈,清空重做栈"""
self._undo_stack.append(self._create_snapshot())
self._redo_stack.clear()
# 限制历史数量
if len(self._undo_stack) > 100:
self._undo_stack.pop(0)
def insert(self, text):
self._record_state()
self.content = self.content[:self.cursor_pos] + text + self.content[self.cursor_pos:]
self.cursor_pos += len(text)
def delete(self, length=1):
self._record_state()
if self.cursor_pos >= length:
self.content = self.content[:self.cursor_pos - length] + self.content[self.cursor_pos:]
self.cursor_pos -= length
def undo(self):
if self._undo_stack:
# 当前状态存入重做栈
self._redo_stack.append(self._create_snapshot())
# 从撤销栈取出上一个状态
snapshot = self._undo_stack.pop()
self._restore_snapshot(snapshot)
return True
return False
def redo(self):
if self._redo_stack:
# 当前状态存入撤销栈
self._undo_stack.append(self._create_snapshot())
# 从重做栈取出状态
snapshot = self._redo_stack.pop()
self._restore_snapshot(snapshot)
return True
return False
def can_undo(self):
return len(self._undo_stack) > 0
def can_redo(self):
return len(self._redo_stack) > 0
设计要点:
- 每次执行修改操作前,先保存当前状态到撤销栈。
- 新操作会清空重做栈,因为之前的重做路径已经失效。
- 撤销时,当前状态存入重做栈,然后从撤销栈弹出上一个状态恢复。
- 重做时,当前状态存入撤销栈,然后从重做栈弹出状态恢复。
- 限制历史记录数量,防止内存溢出。
十一、总结
备忘录模式是一个看似简单但非常实用的设计模式。它的核心思想只有一句话:在不破坏封装的前提下,保存对象的状态快照,以便后续恢复。
回顾本文的要点:
- 三个角色:发起人(Originator)创建和恢复状态,备忘录(Memento)存储状态,管理者(Caretaker)保管备忘录。
- 核心价值:封装性保护 + 状态一致性 + 历史管理。
- 适用场景:撤销重做、游戏存档、事务回滚、审批撤回、配置版本管理。
- 与命令模式的区别:备忘录是"恢复状态",命令是"反向执行"。
- 内存优化:限制数量、增量快照、状态压缩、持久化、写时复制。
- 深拷贝必须:状态包含可变对象时必须深拷贝,否则备忘录会被污染。
掌握备忘录模式,你就掌握了给程序"吃后悔药"的能力。无论是做编辑器、游戏还是企业系统,你都会发现它的用武之地。
转载声明:本文为原创文章,如需转载,请联系作者获得授权,并注明出处。