你有没有用过 Git 的版本回退?或者游戏的存档/读档功能?这些看似不同的操作,背后其实都是同一个设计思想——备忘录模式(Memento Pattern)。
想象一下:你正在开发一个文本编辑器,用户希望随时撤销(Undo)操作。如果每次编辑都直接修改原始数据,撤销将变得极其困难。备忘录模式就是解决这类问题的利器——它让你在不破坏封装性的前提下,捕获对象的内部状态,并在之后恢复到这个状态。
今天我们就来深入理解这个"时光倒流"的设计模式。
一、什么是备忘录模式
备忘录模式(Memento Pattern) 是一种行为型设计模式,它允许在不暴露对象实现细节的情况下,保存和恢复对象的内部状态。
为什么需要备忘录模式
假设你在开发一个画板应用,用户可以在画布上绘制各种图形:
class Canvas: def __init__(self): self.shapes = [] def add_shape(self, shape): self.shapes.append(shape) def clear(self): self.shapes = []
用户画了三个矩形,然后点击"清空"。这时候他后悔了,想把之前的图形恢复回来。怎么办?
一种简单的做法是直接保存整个对象的副本:
# 保存状态backup = canvas.shapes.copy()# 清空画布canvas.clear()# 恢复状态canvas.shapes = backup
这种方式看似可行,但存在严重问题:
- 破坏封装性:直接访问和修改对象的内部属性,违反了面向对象的封装原则
- 难以扩展:如果
Canvas 新增了 background_color、grid_visible 等属性,你必须记得更新所有保存/恢复逻辑 - 职责混乱:Canvas 既要管理绘图,又要管理状态保存,违反了单一职责原则
备忘录模式通过引入一个独立的"备忘录"对象,优雅地解决了这些问题。
备忘录模式的三个角色
备忘录模式包含三个核心角色:
- Originator(原发器):需要保存状态的对象,它创建备忘录来记录当前内部状态,也可以使用备忘录来恢复状态
- Memento(备忘录):存储 Originator 内部状态的对象,它对其他对象不透明,只暴露必要的接口
- Caretaker(负责者):负责保存备忘录的对象,但从不操作或检查备忘录的内容
三者的关系如下:
- Originator 创建 Memento → Caretaker 存储 Memento
- 需要恢复时 → Caretaker 将 Memento 返还给 Originator → Originator 用 Memento 恢复自身状态
二、Python 实现备忘录模式
2.1 经典实现:三角色结构
下面是备忘录模式的经典 Python 实现:
class CanvasMemento: """备忘录——存储 Canvas 的状态""" def __init__(self, shapes, background_color): # 使用私有属性,外部无法直接访问 self._shapes = [s.copy() for s in shapes] self._background_color = background_colorclass Canvas: """原发器——需要保存状态的对象""" def __init__(self): self.shapes = [] self.background_color = "#FFFFFF" def add_shape(self, shape): self.shapes.append(shape) def set_background(self, color): self.background_color = color def clear(self): self.shapes = [] def display(self): print(f"背景色: {self.background_color}, 图形: {self.shapes}") def save(self): """创建备忘录——保存当前状态""" return CanvasMemento(self.shapes, self.background_color) def restore(self, memento): """从备忘录恢复状态""" self.shapes = memento._shapes self.background_color = memento._background_colorclass CanvasHistory: """负责者——管理备忘录的保存和恢复""" def __init__(self): self._history = [] def push(self, memento): """保存一个备忘录""" self._history.append(memento) def pop(self): """取出最近的备忘录""" if not self._history: return None return self._history.pop()
使用方式:
canvas = Canvas()history = CanvasHistory()# 初始状态canvas.add_shape("矩形A")canvas.add_shape("圆形B")canvas.display()# 输出: 背景色: #FFFFFF, 图形: ['矩形A', '圆形B']# 保存当前状态history.push(canvas.save())# 修改状态canvas.add_shape("三角形C")canvas.set_background("#F0F0F0")canvas.display()# 输出: 背景色: #F0F0F0, 图形: ['矩形A', '圆形B', '三角形C']# 撤销——恢复到之前的状态memento = history.pop()canvas.restore(memento)canvas.display()# 输出: 背景色: #FFFFFF, 图形: ['矩形A', '圆形B']
关键设计要点:
CanvasMemento 的属性使用下划线前缀(_shapes、_background_color),虽然 Python 没有真正的私有属性,但这是一种约定,表明外部不应该直接访问Canvas 负责"打包"和"解包"状态,CanvasHistory 只负责存储,完全不关心备忘录里的内容- 撤销操作非常自然:
history.pop() 取出最近的备忘录,交给 canvas.restore() 恢复
2.2 使用 dataclass 简化备忘录
Python 的 dataclass 可以让备忘录的定义更加简洁:
from dataclasses import dataclass, fieldfrom copy import deepcopy@dataclassclass CanvasMemento: """备忘录——使用 dataclass 自动生成 __init__""" shapes: list = field(default_factory=list) background_color: str = "#FFFFFF"class Canvas: def __init__(self): self.shapes = [] self.background_color = "#FFFFFF" def add_shape(self, shape): self.shapes.append(shape) def set_background(self, color): self.background_color = color def clear(self): self.shapes = [] def save(self): """创建备忘录时深拷贝,确保状态独立""" return CanvasMemento( shapes=deepcopy(self.shapes), background_color=self.background_color ) def restore(self, memento): """恢复时深拷贝,避免备忘录和当前对象共享引用""" self.shapes = deepcopy(memento.shapes) self.background_color = memento.background_color
使用 dataclass 的好处:
- 自动生成
__init__、__repr__ 等方法,减少样板代码 - 配合
deepcopy(),确保保存和恢复时状态完全独立
三、实战案例:文本编辑器的撤销功能
让我们用一个更贴近实际的案例来深入理解备忘录模式——实现一个支持多步撤销的文本编辑器:
from dataclasses import dataclassfrom copy import deepcopy@dataclassclass EditorMemento: """备忘录——保存编辑器的完整状态""" content: str cursor_position: int selection_start: int selection_end: intclass TextEditor: """原发器——文本编辑器""" def __init__(self): self.content = "" self.cursor_position = 0 self.selection_start = 0 self.selection_end = 0 def type_text(self, text): """在光标位置输入文本""" self.content = ( self.content[: self.cursor_position] + text + self.content[self.cursor_position :] ) self.cursor_position += len(text) def delete(self): """删除光标前的一个字符""" if self.cursor_position > 0: self.content = ( self.content[: self.cursor_position - 1] + self.content[self.cursor_position :] ) self.cursor_position -= 1 def move_cursor(self, position): """移动光标""" self.cursor_position = max(0, min(position, len(self.content))) def select(self, start, end): """选择文本""" self.selection_start = max(0, start) self.selection_end = min(end, len(self.content)) def display(self): """显示当前状态""" cursor_line = " " * self.cursor_position + "^" print(f"内容: [{self.content}]") print(f"光标: {cursor_line} (位置 {self.cursor_position})") def save(self): """创建备忘录""" return EditorMemento( content=self.content, cursor_position=self.cursor_position, selection_start=self.selection_start, selection_end=self.selection_end, ) def restore(self, memento): """从备忘录恢复""" self.content = memento.content self.cursor_position = memento.cursor_position self.selection_start = memento.selection_start self.selection_end = memento.selection_endclass UndoManager: """负责者——管理撤销/重做历史""" def __init__(self, max_history=50): self._undo_stack = [] self._redo_stack = [] self._max_history = max_history def save_state(self, memento): """保存状态到撤销栈""" 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): """撤销:从撤销栈取出,压入重做栈""" if len(self._undo_stack) <= 1: print("没有更多可撤销的操作") return None memento = self._undo_stack.pop() self._redo_stack.append(memento) # 返回撤销后应该恢复的状态 return self._undo_stack[-1] def redo(self): """重做:从重做栈取出,压入撤销栈""" if not self._redo_stack: print("没有更多可重做的操作") return None memento = self._redo_stack.pop() self._undo_stack.append(memento) return memento @property def can_undo(self): return len(self._undo_stack) > 1 @property def can_redo(self): return len(self._redo_stack) > 0
使用方式:
editor = TextEditor()undo_manager = UndoManager()# 初始状态undo_manager.save_state(editor.save())# 输入 "Hello"editor.type_text("Hello")undo_manager.save_state(editor.save())# 输入 " World"editor.type_text(" World")undo_manager.save_state(editor.save())# 删除一个字符editor.delete()undo_manager.save_state(editor.save())editor.display()# 输出: 内容: [Hello Worl]# 光标: ^ (位置 10)# 撤销删除state = undo_manager.undo()if state: editor.restore(state)editor.display()# 输出: 内容: [Hello World]# 光标: ^ (位置 11)# 撤销 " World" 的输入state = undo_manager.undo()if state: editor.restore(state)editor.display()# 输出: 内容: [Hello]# 光标: ^ (位置 5)# 重做state = undo_manager.redo()if state: editor.restore(state)editor.display()# 输出: 内容: [Hello World]# 光标: ^ (位置 11)
这个例子展示了备忘录模式的完整应用:
- EditorMemento:备忘录,封装了编辑器的所有状态
- TextEditor:原发器,负责创建和恢复备忘录
- UndoManager:负责者,管理撤销/重做栈,不关心备忘录内部内容
四、Pythonic 的备忘录实现方式
经典的三角色结构虽然清晰,但在 Python 中,我们可以用更适合语言特性的方式来实现。
4.1 使用 __dict__ 实现通用备忘录
如果你只需要保存对象的所有属性状态,Python 的 __dict__ 提供了一种极简的实现方式:
from copy import deepcopyclass UniversalMemento: """通用备忘录——保存任意对象的 __dict__""" def __init__(self, state_dict): self._state = state_dictclass UniversalOriginator: """通用原发器混入类""" def save(self): """保存当前状态""" return UniversalMemento(deepcopy(self.__dict__)) def restore(self, memento): """恢复状态""" self.__dict__ = deepcopy(memento._state)# 任何类都可以继承这个混入类来获得备忘录功能class GameCharacter(UniversalOriginator): def __init__(self, name, hp, mp, position): self.name = name self.hp = hp self.mp = mp self.position = position def __repr__(self): return f"GameCharacter({self.name}, HP={self.hp}, MP={self.mp}, Pos={self.position})"# 使用hero = GameCharacter("勇者", 100, 50, (0, 0))save_point = hero.save()hero.hp = 30hero.position = (5, 10)print(hero) # GameCharacter(勇者, HP=30, MP=50, Pos=(5, 10))hero.restore(save_point)print(hero) # GameCharacter(勇者, HP=100, MP=50, Pos=(0, 0))
这种方式的优点是极度简洁——任何类只需继承 UniversalOriginator,就能立即获得状态保存和恢复的能力。
但它的局限性也很明显:
- 保存了所有属性,无法选择性保存(可能有些属性不需要保存,如缓存、临时状态)
- 使用了
deepcopy,对于复杂对象嵌套较深时性能可能不佳 - 备忘录对原发器的内部结构完全透明,没有实现信息隐藏
4.2 使用 __getstate__ 和 __setstate__
Python 的 pickle 模块使用了 __getstate__ 和 __setstate__ 协议来控制序列化行为。我们可以借用这个机制实现备忘录:
import copyclass GameState: """游戏状态——支持选择性状态保存""" def __init__(self): self.level = 1 self.score = 0 self.player_hp = 100 self.player_position = (0, 0) self._cache = {} # 临时缓存,不需要保存 self._dirty = True # 脏标记,不需要保存 def __getstate__(self): """定义哪些状态需要保存""" state = self.__dict__.copy() # 排除不需要保存的属性 state.pop("_cache", None) state.pop("_dirty", None) return state def __setstate__(self, state): """恢复状态时补充默认值""" self.__dict__.update(state) self._cache = {} self._dirty = True def save(self): """创建备忘录""" return copy.deepcopy(self.__getstate__()) def restore(self, memento): """从备忘录恢复""" self.__setstate__(copy.deepcopy(memento))# 使用game = GameState()game.level = 5game.score = 3000game.player_hp = 60game._cache = {"enemies": ["goblin", "orc"]} # 临时数据save_state = game.save()# 游戏继续进行game.level = 6game.score = 5000game.player_hp = 10 # 快死了!# 恢复到之前的存档game.restore(save_state)print(f"等级: {game.level}, 分数: {game.score}, 血量: {game.player_hp}")# 输出: 等级: 5, 分数: 3000, 血量: 60print(f"缓存: {game._cache}") # 缓存被重置为空字典
这种方式的优势:
- 选择性保存:通过
__getstate__ 精确控制哪些属性需要保存 - 恢复时补充默认值:
__setstate__ 可以在恢复时为非持久化属性设置合理默认值 - 与 pickle 协议兼容:直接支持 Python 标准的序列化接口
4.3 使用装饰器实现状态快照
如果你想更细粒度地标记哪些方法需要保存状态,可以用装饰器模式配合备忘录模式:
from functools import wrapsfrom copy import deepcopydef snapshotable(method): """装饰器:在方法执行前自动保存状态快照""" @wraps(method) def wrapper(self, *args, **kwargs): # 执行前保存状态 if hasattr(self, "_snapshot_history"): self._snapshot_history.append(deepcopy(self.__dict__)) result = method(self, *args, **kwargs) return result return wrapperclass SmartDocument: """智能文档——自动快照""" def __init__(self): self.content = "" self.font_size = 14 self._snapshot_history = [] @snapshotable def type_text(self, text): self.content += text @snapshotable def set_font_size(self, size): self.font_size = size @snapshotable def clear(self): self.content = "" def undo(self): if self._snapshot_history: self.__dict__.update(self._snapshot_history.pop()) else: print("没有更多可撤销的操作") def display(self): print(f"内容: [{self.content}], 字号: {self.font_size}")# 使用doc = SmartDocument()doc.type_text("你好")doc.type_text(",世界")doc.set_font_size(16)doc.display() # 内容: [你好,世界], 字号: 16doc.undo() # 撤销字号修改doc.display() # 内容: [你好,世界], 字号: 14doc.undo() # 撤销",世界"doc.display() # 内容: [你好], 字号: 14
装饰器方式让代码更声明式——哪些方法会触发快照一目了然。
五、备忘录模式的应用场景
备忘录模式在实际开发中有广泛的应用,以下是几个典型场景:
场景 1:游戏存档/读档
这是最经典的应用场景。玩家可以在关键节点保存游戏进度,死亡后从存档点重新开始:
# 游戏角色状态保存hero = GameCharacter("勇者", 100, 50, (10, 20))save_file = hero.save() # 存档hero.hp = 0 # 角色阵亡hero.restore(save_file) # 读档,满血复活
场景 2:配置系统的版本管理
应用的配置经常需要回滚到之前的版本:
# 保存配置快照config_snapshot = app_config.save()# 修改配置(可能出问题)app_config.set("timeout", 5)app_config.set("retry", 0)# 发现问题,回滚到之前的配置app_config.restore(config_snapshot)
场景 3:事务回滚
数据库事务的回滚机制本质上就是备忘录模式的应用:
# 开启事务前保存状态before = db_state.save()try: # 执行一系列操作 db.execute("UPDATE accounts SET balance = balance - 100") db.execute("UPDATE accounts SET balance = balance + 100") db.commit()except Exception: # 出错时回滚 db_state.restore(before)
场景 4:工作流状态回退
审批流程中,可能需要将流程回退到上一步:
# 保存流程状态approval_snapshot = workflow.save_state()# 推进到下一步workflow.approve()# 发现问题,回退到之前的状态workflow.restore_state(approval_snapshot)
六、备忘录模式 vs 其他模式的对比
6.1 备忘录 vs 命令模式
命令模式和备忘录模式都可以实现撤销功能,但思路完全不同:
- 备忘录模式:保存对象的完整状态快照,恢复时直接替换。简单粗暴,但可能占用较多内存
- 命令模式:保存操作本身(执行+反向操作),撤销时执行反向操作。更精细,但需要为每个操作实现对应的逆操作
# 备忘录方式撤销:直接恢复状态memento = editor.save()editor.type_text("Hello")editor.restore(memento) # 状态直接回到之前# 命令方式撤销:执行逆操作class TypeTextCommand: def execute(self): editor.type_text(self.text) def undo(self): editor.delete(len(self.text)) # 需要实现逆操作
选择建议:
- 两者也可以结合使用:用备忘录保存关键节点的状态,用命令模式处理中间步骤
6.2 备忘录 vs 原型模式
原型模式通过克隆创建新对象,和备忘录的 deepcopy 看起来很像:
- 原型模式:目的是创建新对象,克隆后继续独立使用
- 备忘录模式:目的是保存和恢复状态,备忘录是临时存储,不是用来创建新对象的
# 原型模式:克隆一个新对象来使用prototype = Document("模板")doc1 = prototype.clone()doc1.content = "文档1"# 备忘录模式:保存状态以便恢复memento = editor.save() # 保存状态editor.type_text("新内容") # 修改editor.restore(memento) # 恢复到之前的状态
七、最佳实践与注意事项
7.1 控制备忘录的内存消耗
备忘录模式最大的风险是内存消耗。每次保存状态都会创建一份深拷贝,如果对象很大或保存频率很高,内存会迅速增长。
解决方案:限制历史记录数量,并采用增量保存。
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) # 移除最旧的记录 def pop(self): if self._history: return self._history.pop() return None def clear(self): self._history.clear()
7.2 何时使用深拷贝 vs 浅拷贝
- 深拷贝(deepcopy):当对象包含可变嵌套结构(列表、字典、自定义对象)时,必须使用深拷贝,否则备忘录和原对象会共享引用
- 浅拷贝(copy):当对象只包含不可变数据(字符串、数字、元组)时,浅拷贝就足够了,性能也更好
- 选择性拷贝:只为需要保存的属性执行深拷贝,跳过临时数据和缓存
import copyclass SelectiveMemento: """选择性深拷贝备忘录""" def __init__(self, content, metadata): # content 是可变字符串列表,需要深拷贝 self.content = copy.deepcopy(content) # metadata 是不可变元组,直接赋值即可 self.metadata = metadata
7.3 备忘录的不可变性
备忘录一旦创建,就不应该被修改。只有 Originator 才有权读取备忘录的内容。在 Python 中,可以通过以下方式增强备忘录的不可变性:
class ImmutableMemento: """不可变备忘录""" __slots__ = ("_content", "_cursor_position") def __init__(self, content, cursor_position): object.__setattr__(self, "_content", content) object.__setattr__(self, "_cursor_position", cursor_position) def __setattr__(self, name, value): raise AttributeError("备忘录是不可变的,不允许修改") @property def content(self): return self._content @property def cursor_position(self): return self._cursor_position
7.4 备忘录与性能
在高频操作场景下(比如文本编辑器每次按键都保存),保存完整的备忘录可能影响性能。这时可以考虑以下优化:
- 操作间隔保存:不是每次操作都保存,而是按时间间隔或操作次数保存
- 增量备忘录:只保存变化的属性,而非完整状态
- 延迟深拷贝:使用写时复制(Copy-on-Write)策略,只在修改时才真正执行拷贝
八、总结
备忘录模式是"时光机"般的设计模式——它让我们可以随时保存对象的状态快照,并在需要时恢复到任意历史时刻。
回顾本文要点:
- 备忘录模式的核心思想:在不破坏封装性的前提下,捕获并保存对象的内部状态,以便之后恢复
- 三个角色:Originator(原发器)创建和恢复备忘录,Memento(备忘录)存储状态,Caretaker(负责者)管理备忘录的生命周期
- Pythonic 实现:从经典三角色到
__dict__ 快照、__getstate__/__setstate__、装饰器快照,Python 提供了多种灵活的实现方式 - 典型应用:编辑器撤销/重做、游戏存档、配置回滚、事务回滚
- 与命令模式对比:备忘录保存完整状态,命令模式保存操作及其逆操作;两者可以结合使用
- 注意事项:警惕内存消耗,合理选择深拷贝/浅拷贝,保持备忘录的不可变性
掌握了备忘录模式,你就拥有了让对象"穿越时空"的能力——无论是撤销操作、回滚状态,还是存档读档,都可以优雅地实现。
如果这篇文章对你有帮助,欢迎点赞、在看、转发,你的支持是我持续创作的动力!