亦称: 快照、Snapshot、Memento

备忘录模式是一种行为设计模式,允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。

背景

假如正在开发一款文字编辑器应用程序。除了简单的文字编辑功能外,还有设置文本格式和插入内嵌图片等功能。

现在,需要开发撤销功能。当用户需要撤销某个操作时,程序将从历史记录中获取最近的快照,然后使用它来恢复所有对象的状态。

随之而来的,有这样几个问题需要解决:

  • 如何生成快照
    • 可能需要遍历对象的所有成员,并将值保存。如果存在无法访问的私有成员变量怎么办呢?
  • 如何支持后续的修改
    • 未来可能会在编辑器类中添加或删除字段,那么用来保存快照的类相应字段也要跟着修改
  • 为了读取快照对象,需要将快照的成员变量设置为公有
    • 这样一来,无论这些成员变量之前在编辑器类中是否私有,在快照中都会暴露给外部
    • 其它外部类可能会对快照类暴露的公有变量产生依赖,未来有修改时都要跟随改动

我们似乎走进了一条死胡同:要么会暴露类的所有内部细节而使其过于脆弱;要么会限制对其状态的访问权限而无法生成快照。

解决方案

前面遇到的所有问题都是封装 “破损” 造成的。

一些对象试图完成超出其职责范围的工作。由于在执行某些行为时需要获取数据,所以它们侵入了其他对象的私有空间,而不是让这些对象自己来完成相应的工作。

备忘录模式将创建状态快照(Snapshot)的工作委派给实际状态的拥有者原发器Originator对象。

这样其他对象就不再需要从 “外部” 复制编辑器状态了,编辑器类拥有其状态的完全访问权,因此可以自行生成快照。

模式建议将对象状态的副本存储在一个名为备忘录Memento的特殊对象中。除了创建备忘录的对象外,任何对象都不能访问备忘录的内容。 其他对象必须使用受限接口与备忘录进行交互,它们可以获取快照的元数据(创建时间和操作名称等),但不能获取快照中原始对象的状态。

这种限制策略允许你将备忘录保存在通常被称为负责人Caretakers的对象中。

  • 负责人仅通过受限接口与备忘录互动,无法修改存储在备忘录内部的状态
  • 原发器拥有对备忘录所有成员的访问权限,能随时恢复其以前的状态。

在文字编辑器的示例中,可以创建一个独立的历史History类作为负责人。

  • 编辑器每次执行操作前,存储在负责人中的备忘录栈都会生长。 甚至可以在应用的UI中渲染该栈,为用户显示之前的操作历史。
  • 当用户触发撤销操作时,历史类将从栈中取回最近的备忘录,并将其传递给编辑器以请求进行回滚。
  • 编辑器拥有对备忘录的完全访问权限,它可以使用从备忘录中获取的数值来替换自身的状态。

备忘录模式

适用场景

  • 当需要创建对象状态快照来恢复其之前的状态时,可以使用备忘录模式。
    • 备忘录模式允许复制对象中的全部状态(包括私有成员变量),并将其独立于对象进行保存。
    • 尽管大部分人因为 “撤销” 这个用例才记得该模式,但其实它在处理事务(比如需要在出现错误时回滚一个操作)的过程中也必不可少。
  • 当直接访问对象的成员变量、获取器或设置器会导致封装被突破时,可以使用该模式。
    • 备忘录让对象自行负责创建其状态的快照。任何其他对象都不能读取快照,这有效地保障了数据的安全性。

实现步骤

  1. 确定担任原发器角色的类,明确程序使用的是一个原发器中心对象,还是多个较小的对象。
  2. 创建备忘录类。逐一声明对应每个原发器成员变量的备忘录成员变量。
  3. 将备忘录类设为不可变。备忘录只能通过构造函数一次性接收数据。该类中不能包含设置器。
  4. 如果使用的编程语言支持嵌套类,可将备忘录嵌套在原发器中;
    • 如果不支持,可从备忘录类中抽取一个空接口,然后让其他所有对象通过接口来引用备忘录。
    • 可在该接口中添加一些元数据操作,但不能暴露原发器的状态。
  5. 在原发器中添加一个创建备忘录的方法。原发器必须通过备忘录构造函数的一个或多个实际参数来将自身状态传递给备忘录。
    • 该方法返回结果的类型必须是在上一步中抽取的接口(如果已经抽取了)。实际上,创建备忘录的方法必须直接与备忘录类进行交互。
  6. 在原发器类中添加一个用于恢复自身状态的方法。该方法接受备忘录对象作为参数。
    • 如果在之前的步骤中抽取了接口,可将接口作为参数的类型。
  7. 无论负责人是命令对象、历史记录或其他完全不同的东西,它都必须要知道何时向原发器请求新的备忘录、如何存储备忘录以及何时使用特定备忘录来对原发器进行恢复。
  8. 负责人与原发器之间的连接可以移动到备忘录类中。
    • 在本例中,每个备忘录都必须与创建自己的原发器相连接。
    • 恢复方法也可以移动到备忘录类中,但只有当备忘录类嵌套在原发器中,或者原发器类提供了足够多的设置器并可对其状态进行重写时才可实现。

优缺点

优点

  • 可以在不破坏对象封装情况的前提下创建对象状态快照。
  • 可以通过让负责人维护原发器状态历史记录来简化原发器代码。

缺点

  • 如果客户端过于频繁地创建备忘录,程序将消耗大量内存。
  • 负责人必须完整跟踪原发器的生命周期,这样才能销毁弃用的备忘录以回收内存空间。
  • 绝大部分动态编程语言(例如 PHP/Python/JavaScript)不能确保备忘录中的状态不被修改。

与其他模式的关系

  • 可以同时使用命令模式和备忘录模式来实现 “撤销”。
    • 命令用于对目标对象执行各种不同的操作,备忘录用来保存一条命令执行前该对象的状态。
  • 可以同时使用备忘录和迭代器模式来获取当前迭代器的状态,并且在需要的时候进行回滚。
  • 有时原型模式可以作为备忘录的一个简化版本,前提是你需要在历史记录中存储的对象的状态比较简单,不需要链接其他外部资源,或者链接可以方便地重建。

示例

使用备忘录模式管理《超级马里奥》游戏闯关进度

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package main

import (
	"fmt"
	"time"
)

// IMario 马里奥角色接口
type IMario interface {
	ShowInfo()       // 角色信息
	setState(IMario) // 快照恢复

	Memorable   // 可快照化
	MarioStatus // 角色状态
}

// Mario 马里奥游戏角色
type Mario struct {
	score int64
	// mario可以有多种状态,在不同状态下遇到不同物品的响应不同
	// 使用状态模式
	status MarioStatus
}

func NewMario() *Mario {
	m := &Mario{
		score: 0,
	}
	// 初始为小马里奥
	m.status = &SmallMarioStatus{
		mario: m,
	}
	return m
}

func (m *Mario) ShowInfo() {
	m.Name()
	fmt.Println("当前分数为:", m.score)
}

func (m *Mario) Snapshot() Memo {
	return &MarioMemento{
		origin: m,
		// 深拷贝,防止引用共享
		state:    m.clone(),
		CreateAt: time.Now(),
	}
}

// clone 深拷贝,原型模式
func (m *Mario) clone() *Mario {
	return &Mario{
		score:  m.score,
		status: m.status,
	}
}

// setState 恢复状态,只提供给备忘录使用
func (m *Mario) setState(imm IMario) {
	mm := imm.(*Mario)
	m.score = mm.score
	m.status = mm.status
}

func (m *Mario) Name() {
	m.status.Name()
}

func (m *Mario) ObtainMushroom() {
	m.status.ObtainMushroom()
}

func (m *Mario) ObtainCape() {
	m.status.ObtainCape()
}

func (m *Mario) MeetMonster() {
	m.status.MeetMonster()
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import "time"

// Memorable 支持快照
type Memorable interface {
	Snapshot() Memo // 创建快照
}

// Memo 备忘录接口
type Memo interface {
	Restore()
}

// MarioMemento 马里奥备忘录
// 该实现中,其他类没有任何机会从备忘录中访问原发器对象,只能访问一些元数据
type MarioMemento struct {
	origin *Mario
	// 直接把原发器对象作为状态,保存到备忘录,需要深拷贝保存
	state    *Mario
	CreateAt time.Time
}

// Restore 恢复该备份。 Memo只对外提供了无参恢复指令
func (m *MarioMemento) Restore() {
	m.origin.setState(m.state)
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package main

import "fmt"

// MarioStatus 马里奥的不同状态,使用状态模式维护
type MarioStatus interface {
	Name()           // 状态名称
	ObtainMushroom() // 获得蘑菇
	ObtainCape()     //  获得斗篷
	MeetMonster()    // 遇到怪兽
}

// SmallMarioStatus 小马里奥
type SmallMarioStatus struct {
	mario *Mario
}

func (s *SmallMarioStatus) Name() {
	fmt.Println("小马里奥")
}

// ObtainMushroom 获得蘑菇变为超级马里奥
func (s *SmallMarioStatus) ObtainMushroom() {
	s.mario.status = &SuperMarioStatus{
		mario: s.mario,
	}
	s.mario.score += 100
}

// ObtainCape 获得斗篷变为斗篷马里奥
func (s *SmallMarioStatus) ObtainCape() {
	s.mario.status = &CapeMarioStatus{
		mario: s.mario,
	}
	s.mario.score += 200
}

// MeetMonster 遇到怪兽减100
func (s *SmallMarioStatus) MeetMonster() {
	s.mario.score -= 100
}

// SuperMarioStatus 超级马里奥
type SuperMarioStatus struct {
	mario *Mario
}

func (s *SuperMarioStatus) Name() {
	fmt.Println("超级马里奥")
}

// ObtainMushroom 获得蘑菇无变化
func (s *SuperMarioStatus) ObtainMushroom() {
}

// ObtainCape 获得斗篷变为斗篷马里奥
func (s *SuperMarioStatus) ObtainCape() {
	s.mario.status = &CapeMarioStatus{
		mario: s.mario,
	}
	s.mario.score += 200
}

// MeetMonster 遇到怪兽变为小马里奥
func (s *SuperMarioStatus) MeetMonster() {
	s.mario.status = &SmallMarioStatus{
		mario: s.mario,
	}
	s.mario.score -= 200
}

// CapeMarioStatus 斗篷马里奥
type CapeMarioStatus struct {
	mario *Mario
}

func (c *CapeMarioStatus) Name() {
	fmt.Println("斗篷马里奥")
}

// ObtainMushroom 获得蘑菇无变化
func (c *CapeMarioStatus) ObtainMushroom() {
}

func (c *CapeMarioStatus) ObtainCape() {
}

func (c *CapeMarioStatus) MeetMonster() {
	c.mario.status = &SmallMarioStatus{
		mario: c.mario,
	}
	c.mario.score -= 200
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "container/list"

type Caretaker struct {
	stack *list.List
}

func NewCaretaker() *Caretaker {
	return &Caretaker{stack: list.New()}
}

func (c *Caretaker) Save(m Memo) {
	c.stack.PushBack(m)
}

func (c *Caretaker) Pop() Memo {
	e := c.stack.Back()
	c.stack.Remove(e)
	return e.Value.(Memo)
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
	"fmt"
)

func main() {
	caretaker := NewCaretaker()
	mario := NewMario()

	mario.Name()
	fmt.Println("-------------------获得蘑菇")
	mario.ObtainMushroom()

	mario.Name()
	fmt.Println("-------------------获得斗篷")
	mario.ObtainCape()

	fmt.Println("-------------------备份一下,要打怪了,当前状态为")
	mario.ShowInfo()
	caretaker.Save(mario.Snapshot())
	fmt.Println("-------------------开始打怪")

	mario.Name()
	fmt.Println("-------------------遇到怪兽")
	mario.MeetMonster()

	fmt.Println("-------------------打怪失败,目前状态为")
	mario.ShowInfo()

	fmt.Println("-------------------恢复状态,重新打怪")
	caretaker.Pop().Restore()
	mario.ShowInfo()
}

References

guru