Skip to content

备忘录模式(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 1

2.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 World

2.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);
    }
}

七、全文总结

备忘录模式的核心是 捕获对象内部状态,实现撤销、回滚、检查点功能

核心要点

  1. 备忘录存储原发器状态,原发器创建和恢复,负责人保管
  2. 注意引用类型的深拷贝,避免快照被外部修改
  3. 限制保存数量,防止内存爆炸
  4. 适用于撤销、游戏存档、事务回滚等场景
  5. 可结合增量保存、序列化持久化优化