备忘录模式(Memento)
前言
备忘录模式在不破坏封装性的前提下,捕获对象的内部状态,以便日后恢复。它是实现撤销功能、状态回滚、检查点机制的经典模式。本文将详细讲解备忘录模式的核心原理及实际应用。
一、核心概念
1.1 定义
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
1.2 核心角色
| 角色 | 说明 |
|---|---|
| 备忘录(Memento) | 存储原发器内部状态 |
| 原发器(Originator) | 创建备忘录,恢复状态 |
| 负责人(Caretaker) | 保管备忘录,不可修改 |
1.3 三种接口类型
| 类型 | 说明 |
|---|---|
| 白箱 | 备忘录对所有人公开 |
| 黑箱 | 备忘录只对原发器公开 |
| 双接口 | 通过内部类实现访问控制 |
二、代码实现
2.1 基础实现
php
<?php
// 备忘录:存储状态
class Memento
{
public function __construct(private string $state) {}
public function getState(): string
{
return $this->state;
}
}
// 原发器
class Originator
{
private string $state;
public function __construct(string $state)
{
$this->state = $state;
}
public function setState(string $state): void
{
echo "Originator: State set to {$state}" . PHP_EOL;
$this->state = $state;
}
public function getState(): string
{
return $this->state;
}
// 创建备忘录
public function saveToMemento(): Memento
{
echo "Originator: Saving to Memento: {$this->state}" . PHP_EOL;
return new Memento($this->state);
}
// 从备忘录恢复
public function restoreFromMemento(Memento $memento): void
{
$this->state = $memento->getState();
echo "Originator: State restored from Memento: {$this->state}" . PHP_EOL;
}
}
// 负责人:管理备忘录
class Caretaker
{
/** @var Memento[] */
private array $history = [];
public function addMemento(Memento $memento): void
{
$this->history[] = $memento;
}
public function getMemento(int $index): Memento
{
return $this->history[$index];
}
}
// 使用
$originator = new Originator("State 1");
$caretaker = new Caretaker();
$caretaker->addMemento($originator->saveToMemento());
$originator->setState("State 2");
$caretaker->addMemento($originator->saveToMemento());
$originator->setState("State 3");
echo "Current: {$originator->getState()}" . PHP_EOL; // State 3
$originator->restoreFromMemento($caretaker->getMemento(1));
echo "Current: {$originator->getState()}" . PHP_EOL; // State 2
$originator->restoreFromMemento($caretaker->getMemento(0));
echo "Current: {$originator->getState()}" . PHP_EOL; // State 12.2 实际案例:文本编辑器撤销
php
<?php
// 备忘录:编辑器状态快照
class EditorMemento
{
public function __construct(
public readonly string $content,
public readonly int $cursorPosition,
public readonly string $selection
) {}
}
// 原发器:文本编辑器
class TextEditor
{
private string $content = "";
private int $cursorPosition = 0;
private string $selection = "";
public function type(string $text): void
{
$this->content =
substr($this->content, 0, $this->cursorPosition) .
$text .
substr($this->content, $this->cursorPosition);
$this->cursorPosition += strlen($text);
}
public function delete(int $length): void
{
$this->content =
substr($this->content, 0, $this->cursorPosition - $length) .
substr($this->content, $this->cursorPosition);
$this->cursorPosition -= $length;
}
public function setCursor(int $position): void
{
$this->cursorPosition = $position;
}
public function select(int $start, int $end): void
{
$this->selection = substr($this->content, $start, $end - $start);
}
public function getContent(): string
{
return $this->content;
}
// 保存快照
public function save(): EditorMemento
{
return new EditorMemento($this->content, $this->cursorPosition, $this->selection);
}
// 恢复快照
public function restore(EditorMemento $memento): void
{
$this->content = $memento->content;
$this->cursorPosition = $memento->cursorPosition;
$this->selection = $memento->selection;
}
}
// 负责人:撤销管理器
class UndoManager
{
/** @var EditorMemento[] */
private array $undoStack = [];
/** @var EditorMemento[] */
private array $redoStack = [];
private int $maxSize = 50;
public function save(EditorMemento $memento): void
{
$this->undoStack[] = $memento;
if (count($this->undoStack) > $this->maxSize) {
array_shift($this->undoStack);
}
$this->redoStack = [];
}
public function undo(EditorMemento $current): ?EditorMemento
{
if (empty($this->undoStack)) return null;
$this->redoStack[] = $current;
return array_pop($this->undoStack);
}
public function redo(EditorMemento $current): ?EditorMemento
{
if (empty($this->redoStack)) return null;
$this->undoStack[] = $current;
return array_pop($this->redoStack);
}
}
// 使用
$editor = new TextEditor();
$undoManager = new UndoManager();
$undoManager->save($editor->save());
$editor->type("Hello ");
$undoManager->save($editor->save());
$editor->type("World");
echo $editor->getContent() . PHP_EOL; // Hello World
$editor->restore($undoManager->undo($editor->save())!);
echo $editor->getContent() . PHP_EOL; // Hello
$editor->restore($undoManager->redo($editor->save())!);
echo $editor->getContent() . PHP_EOL; // Hello World2.3 实际案例:游戏存档
php
<?php
// 备忘录:游戏状态
class GameSave
{
public function __construct(
public readonly int $level,
public readonly int $score,
public readonly int $health,
public readonly array $position,
public readonly array $inventory,
public readonly DateTime $timestamp
) {}
}
// 原发器:游戏角色
class GameCharacter
{
private int $level = 1;
private int $score = 0;
private int $health = 100;
private array $position = ['x' => 0, 'y' => 0];
private array $inventory = [];
public function levelUp(): void
{
$this->level++;
$this->score += 100;
}
public function takeDamage(int $dmg): void
{
$this->health -= $dmg;
}
public function heal(int $amount): void
{
$this->health = min(100, $this->health + $amount);
}
public function moveTo(int $x, int $y): void
{
$this->position = ['x' => $x, 'y' => $y];
}
public function addItem(string $item): void
{
$this->inventory[] = $item;
}
// 存档
public function save(): GameSave
{
return new GameSave(
$this->level,
$this->score,
$this->health,
$this->position,
$this->inventory,
new DateTime()
);
}
// 读档
public function load(GameSave $save): void
{
$this->level = $save->level;
$this->score = $save->score;
$this->health = $save->health;
$this->position = $save->position;
$this->inventory = $save->inventory;
}
public function getStatus(): string
{
return "Lvl {$this->level}, Score {$this->score}, HP {$this->health}, At ({$this->position['x']},{$this->position['y']})";
}
}
// 负责人:存档管理
class SaveManager
{
/** @var array<string, GameSave> */
private array $saves = [];
public function save(string $name, GameSave $save): void
{
$this->saves[$name] = $save;
echo "Save \"{$name}\" created at {$save->timestamp->format('Y-m-d H:i:s')}" . PHP_EOL;
}
public function load(string $name): ?GameSave
{
return $this->saves[$name] ?? null;
}
public function listSaves(): array
{
return array_keys($this->saves);
}
}
// 使用
$player = new GameCharacter();
$saveManager = new SaveManager();
$player->moveTo(10, 20);
$player->addItem("Sword");
$saveManager->save("checkpoint1", $player->save());
$player->levelUp();
$player->takeDamage(30);
$player->moveTo(50, 60);
$saveManager->save("checkpoint2", $player->save());
echo "Before load: " . $player->getStatus() . PHP_EOL;
// Lvl 2, Score 100, HP 70, At (50,60)
$save = $saveManager->load("checkpoint1");
if ($save !== null) $player->load($save);
echo "After load: " . $player->getStatus() . PHP_EOL;
// Lvl 1, Score 0, HP 100, At (10,20)三、适用场景
| 场景 | 说明 |
|---|---|
| 撤销/重做 | 编辑器、绘图工具 |
| 事务回滚 | 数据库事务 |
| 游戏存档 | 检查点机制 |
| 状态快照 | 系统状态备份 |
| 历史记录 | 操作历史 |
| 检查点 | 长流程任务 |
四、优缺点分析
| 优点 | 缺点 |
|---|---|
| 支持状态恢复 | 备忘录占用内存 |
| 保持封装性 | 频繁保存性能开销 |
| 简化原发器 | 大对象序列化开销 |
| 符合单一职责 | 负责人需管理生命周期 |
| 支持多状态快照 | 状态不可变性问题 |
五、常见踩坑与问题排查
5.1 备忘录对象被外部修改
php
<?php
// 问题:备忘录状态被外部修改
class BadMemento
{
public function __construct(public MyObject $state) {} // 公开可变对象
}
$memento = new BadMemento($myObj);
$memento->state->field = "changed"; // 破坏了快照
// 解决:使用深拷贝或不可变对象
class GoodMemento
{
public function __construct(private readonly MyObject $state) {}
public function getState(): MyObject
{
return clone $this->state; // 返回副本
}
}5.2 内存占用过大
php
<?php
// 问题:保存大量快照导致内存爆炸
class BadCaretaker
{
/** @var Memento[] */
private array $saves = [];
// 无限保存
}
// 解决:限制保存数量、压缩快照、使用增量保存
class GoodCaretaker
{
/** @var Memento[] */
private array $saves = [];
private int $maxSaves = 50;
public function add(Memento $memento): void
{
$this->saves[] = $memento;
if (count($this->saves) > $this->maxSaves) {
array_shift($this->saves);
}
}
}5.3 引用类型未深拷贝
php
<?php
// 问题:数组、对象引用未深拷贝
class BadSave
{
public function __construct(public array $items) {}
}
$items = ["a", "b"];
$save = new BadSave($items);
$items[] = "c"; // $save->items 也变了!
// 解决:深拷贝
class GoodSave
{
public function __construct(array $items)
{
$this->items = array_values($items); // 或 serialize/unserialize 深拷贝
}
}六、优化方案与进阶
6.1 增量备忘录
php
<?php
// 只保存变化的部分
class IncrementalMemento
{
/** @var array<string, mixed> */
private array $changes = [];
public function recordChange(string $field, $oldValue): void
{
$this->changes[$field] = $oldValue;
}
public function getChanges(): array
{
return $this->changes;
}
}6.2 序列化持久化
php
<?php
// 将备忘录序列化为 JSON 存储
class PersistentCaretaker
{
public function saveToFile(Memento $memento, string $filename): void
{
$json = json_encode($memento);
file_put_contents($filename, $json);
}
public function loadFromFile(string $filename): Memento
{
$json = file_get_contents($filename);
return json_decode($json);
}
}七、全文总结
备忘录模式的核心是 捕获对象内部状态,实现撤销、回滚、检查点功能。
核心要点:
- 备忘录存储原发器状态,原发器创建和恢复,负责人保管
- 注意引用类型的深拷贝,避免快照被外部修改
- 限制保存数量,防止内存爆炸
- 适用于撤销、游戏存档、事务回滚等场景
- 可结合增量保存、序列化持久化优化
