文章

设计模式解密:备忘录模式的实践指南

模式定义

备忘录模式(Memento Pattern)是行为型设计模式,用于在不破坏对象封装性的前提下捕获和恢复其内部状态。该模式通过"快照"机制实现状态回滚,是撤销操作和事务管理的核心实现方案。

核心思想

  1. 状态封装:将对象状态独立存储

  2. 历史管理:支持多版本状态保存

  3. 权限隔离:原发器外无法直接访问状态细节

  4. 无痕恢复:实现状态的安全回滚

适用场景

  • 需要撤销/重做功能(如文本编辑器)

  • 系统快照与恢复(虚拟机快照)

  • 事务回滚机制(数据库事务)

  • 游戏存档/读档系统

模式结构

  • 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(&copyState)
	
	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
*/

模式优缺点

优点

  • 保持对象封装边界

  • 简化原发器职责

  • 支持多版本状态管理

  • 实现状态历史追溯

缺点

  • 可能消耗大量内存

  • 频繁保存影响性能

  • 增加系统复杂度

  • 需要处理深拷贝问题


不同语言实现差异

特性

PHP

Go

状态克隆

序列化实现深拷贝

encoding/gob实现深拷贝

访问控制

内部类保护状态

包级私有实现封装

历史管理

数组存储对象引用

切片存储接口实现

内存管理

需手动释放旧状态

GC自动回收

典型应用

表单草稿保存

游戏状态存档


模式演进方向

  1. 增量快照
    仅存储变化部分减少内存占用

  2. 持久化存储
    将快照保存到数据库或文件系统

  3. 版本对比
    实现不同版本状态差异分析

  4. 自动快照
    定时自动保存状态

  5. 分布式快照
    实现跨节点的状态一致性保存


备忘录模式VS其他模式

对比模式

关键区别

原型模式

克隆完整对象 vs 保存特定状态

命令模式

记录操作 vs 记录状态

事务模式

业务操作原子性 vs 状态恢复机制


最佳实践指南

  1. 控制快照频率
    避免无限制保存历史版本

  2. 状态序列化
    使用高效序列化方案(如MessagePack)

  3. 敏感数据处理
    加密存储重要状态信息

  4. 存储优化
    对大型状态使用外部存储

  5. 版本兼容
    处理状态结构变更的兼容性问题


总结

备忘录模式通过状态快照机制,实现了状态管理业务逻辑的优雅分离。该模式在以下场景表现卓越:

  • 需要实现撤销/重做功能

  • 系统需要状态回滚能力

  • 需要保存对象历史状态

  • 实现系统快照功能

PHP与Go的实现对比体现了不同语言的设计哲学:

  • PHP利用面向对象特性实现严格的封装

  • Go通过接口和组合实现灵活的状态管理

实际应用中需注意:

  • 合理控制快照的生命周期

  • 处理大型状态的存储效率问题

  • 确保状态恢复的完整性

  • 在功能需求与系统资源之间做好平衡

掌握备忘录模式的关键在于理解"状态封装"的设计理念,这种将运行时状态持久化的思想,是构建可靠撤销系统和事务管理的重要基础。当需要实现状态追溯和回滚功能时,备忘录模式能提供优雅的解决方案。

License:  CC BY 4.0