设计模式解密:备忘录模式的实践指南
模式定义
备忘录模式(Memento Pattern)是行为型设计模式,用于在不破坏对象封装性的前提下捕获和恢复其内部状态。该模式通过"快照"机制实现状态回滚,是撤销操作和事务管理的核心实现方案。
核心思想
状态封装:将对象状态独立存储
历史管理:支持多版本状态保存
权限隔离:原发器外无法直接访问状态细节
无痕恢复:实现状态的安全回滚
适用场景
需要撤销/重做功能(如文本编辑器)
系统快照与恢复(虚拟机快照)
事务回滚机制(数据库事务)
游戏存档/读档系统
模式结构
Originator:需要保存状态的原发对象
Memento:存储原发器状态的备忘录
Caretaker:管理备忘录历史的守护者
PHP实现示例:文本编辑器撤销系统
<?php
// 备忘录类(仅Originator可访问)
class TextMemento {
private $content;
private $timestamp;
public function __construct(string $content) {
$this->content = $content;
$this->timestamp = date('Y-m-d H:i:s');
}
public function getContent(): string {
return $this->content;
}
public function getTimestamp(): string {
return $this->timestamp;
}
}
// 原发器:文本编辑器
class TextEditor {
private $content = '';
public function type(string $text): void {
$this->content .= $text;
}
public function save(): TextMemento {
return new TextMemento($this->content);
}
public function restore(TextMemento $memento): void {
$this->content = $memento->getContent();
}
public function show(): void {
echo "当前内容:{$this->content}\n";
}
}
// 守护者:历史管理
class HistoryKeeper {
private $mementos = [];
private $current = -1;
public function backup(TextMemento $memento): void {
// 清除当前指针后的历史
$this->mementos = array_slice($this->mementos, 0, $this->current + 1);
$this->mementos[] = $memento;
$this->current++;
}
public function undo(): ?TextMemento {
if ($this->current > 0) {
$this->current--;
return $this->mementos[$this->current];
}
return null;
}
public function redo(): ?TextMemento {
if ($this->current < count($this->mementos) - 1) {
$this->current++;
return $this->mementos[$this->current];
}
return null;
}
public function showHistory(): void {
foreach ($this->mementos as $index => $memento) {
$prefix = ($index == $this->current) ? "▶ " : " ";
echo "{$prefix}[{$memento->getTimestamp()}] 版本{$index}\n";
}
}
}
// 客户端使用
$editor = new TextEditor();
$history = new HistoryKeeper();
$editor->type("Hello");
$history->backup($editor->save());
$editor->type(" World");
$history->backup($editor->save());
$editor->type("!");
echo "---- 当前状态 ----\n";
$editor->show();
$history->showHistory();
echo "\n---- 执行撤销 ----\n";
if ($memento = $history->undo()) {
$editor->restore($memento);
}
$editor->show();
echo "\n---- 执行重做 ----\n";
if ($memento = $history->redo()) {
$editor->restore($memento);
}
$editor->show();
/* 输出:
---- 当前状态 ----
当前内容:Hello World!
[2023-08-20 15:00:00] 版本0
▶ [2023-08-20 15:00:00] 版本1
---- 执行撤销 ----
当前内容:Hello World
---- 执行重做 ----
当前内容:Hello World!
*/
Go实现示例:游戏角色状态存档
package main
import (
"encoding/gob"
"fmt"
"os"
"time"
)
// 角色状态
type PlayerState struct {
Health int
Mana int
PositionX int
PositionY int
Inventory []string
}
// 备忘录接口
type Memento interface {
GetState() PlayerState
GetDate() time.Time
}
// 具体备忘录
type GameSave struct {
state PlayerState
saveTime time.Time
}
func (g *GameSave) GetState() PlayerState {
return g.state
}
func (g *GameSave) GetDate() time.Time {
return g.saveTime
}
// 原发器:游戏角色
type GameCharacter struct {
state PlayerState
}
func (g *GameCharacter) TakeDamage(dmg int) {
g.state.Health -= dmg
}
func (g *GameCharacter) Move(x, y int) {
g.state.PositionX = x
g.state.PositionY = y
}
func (g *GameCharacter) Save() Memento {
// 深拷贝状态
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
enc.Encode(g.state)
var copyState PlayerState
dec := gob.NewDecoder(&buf)
dec.Decode(©State)
return &GameSave{
state: copyState,
saveTime: time.Now(),
}
}
func (g *GameCharacter) Restore(m Memento) {
g.state = m.GetState()
}
func (g *GameCharacter) Display() {
fmt.Printf("角色状态:\n")
fmt.Printf("生命值:%d\n", g.state.Health)
fmt.Printf("位置:(%d, %d)\n", g.state.PositionX, g.state.PositionY)
fmt.Printf("背包:%v\n", g.state.Inventory)
}
// 存档管理器
type SaveManager struct {
saves []Memento
}
func (s *SaveManager) Add(m Memento) {
s.saves = append(s.saves, m)
}
func (s *SaveManager) Get(index int) Memento {
if index < 0 || index >= len(s.saves) {
return nil
}
return s.saves[index]
}
func (s *SaveManager) ListSaves() {
fmt.Println("\n存档列表:")
for i, save := range s.saves {
fmt.Printf("%d. %s\n", i+1, save.GetDate().Format("2006-01-02 15:04:05"))
}
}
func main() {
player := &GameCharacter{
state: PlayerState{
Health: 100,
Mana: 50,
PositionX: 0,
PositionY: 0,
Inventory: []string{"药水", "钥匙"},
},
}
manager := &SaveManager{}
// 初始存档
manager.Add(player.Save())
player.Move(10, 5)
player.TakeDamage(20)
manager.Add(player.Save())
// 查看当前状态
fmt.Println("=== 当前状态 ===")
player.Display()
// 回滚到第一个存档
fmt.Println("\n=== 回滚到存档1 ===")
if save := manager.Get(0); save != nil {
player.Restore(save)
}
player.Display()
manager.ListSaves()
}
/* 输出示例:
=== 当前状态 ===
角色状态:
生命值:80
位置:(10, 5)
背包:[药水 钥匙]
=== 回滚到存档1 ===
角色状态:
生命值:100
位置:(0, 0)
背包:[药水 钥匙]
存档列表:
1. 2023-08-20 15:05:00
2. 2023-08-20 15:05:02
*/
模式优缺点
✅ 优点:
保持对象封装边界
简化原发器职责
支持多版本状态管理
实现状态历史追溯
❌ 缺点:
可能消耗大量内存
频繁保存影响性能
增加系统复杂度
需要处理深拷贝问题
不同语言实现差异
模式演进方向
增量快照
仅存储变化部分减少内存占用持久化存储
将快照保存到数据库或文件系统版本对比
实现不同版本状态差异分析自动快照
定时自动保存状态分布式快照
实现跨节点的状态一致性保存
备忘录模式VS其他模式
最佳实践指南
控制快照频率
避免无限制保存历史版本状态序列化
使用高效序列化方案(如MessagePack)敏感数据处理
加密存储重要状态信息存储优化
对大型状态使用外部存储版本兼容
处理状态结构变更的兼容性问题
总结
备忘录模式通过状态快照机制,实现了状态管理与业务逻辑的优雅分离。该模式在以下场景表现卓越:
需要实现撤销/重做功能
系统需要状态回滚能力
需要保存对象历史状态
实现系统快照功能
PHP与Go的实现对比体现了不同语言的设计哲学:
PHP利用面向对象特性实现严格的封装
Go通过接口和组合实现灵活的状态管理
实际应用中需注意:
合理控制快照的生命周期
处理大型状态的存储效率问题
确保状态恢复的完整性
在功能需求与系统资源之间做好平衡
掌握备忘录模式的关键在于理解"状态封装"的设计理念,这种将运行时状态持久化的思想,是构建可靠撤销系统和事务管理的重要基础。当需要实现状态追溯和回滚功能时,备忘录模式能提供优雅的解决方案。