XingPiaoLiang's

Back

备忘录模式#

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

问题引入#

现在你正在开发一款文字编辑器,你需要让你的编辑器支持非常常见又重要的的功能:撤销和回滚操作。用户可以通过 Ctrl+Z 进行撤销回到当前历史操作。

当然最简单而直接的方式就是:使用「状态快照」进行记录所有操作前的对象状态,将其封装在一个额外的「历史栈」中,当用户需要撤销某个操作的时候就将栈顶的状态元素直接弹出。

「状态快照」,需要保存哪些数据呢?我们需要遍历所有,可以代表当前编辑器状态的类的成员变量,但随之而来的便是权限控制问题,有些成员变量并不对外开放,所以我们无法访问。那么我们如果放开了权限,显然这是不可取的,我们走进了一条死胡同。

问题解决#

上文我们碰到的问题,都是由于「封装破损」造成的,一些对象试图超出其职责范围,试图访问某些对象的私有空间,来完成他所需的工作(创建历史记录),而不是让这些对象本身来完成这个工作。

备忘录模式思想是:将创建「状态快照」的工作委派给实际状态的拥有者「原发器」(Originator)。这样一来,其他对象就不需要从外部复制编辑器当前状态了,因为编辑器类本身(原发器)拥有自己状态的完全访问权,所以可以自行生成快照(SnapShot)。

根据备忘录模式,编辑器对象状态的副本应该保存在一个名为「备忘录」(Memento)特殊对象中,备忘录的内部快照具体信息只能被创建该备忘录的对象访问,其他外部类只能通过受权限控制的接口,获得快照的一些元数据(创建时间和操作名称等)。

这样的限制策略,允许我们将备忘录保存在称为「负责人」(Caretakers)的对象中。

在我们的文本编辑器问题中,我们创建一个独立的历史(History)类作为 Caretakers 编辑器在每次执行操作之前,存储在 History 类中的备忘录栈都会生长。当用户触发撤销操作,History 类取出最近的备忘录,将其传递给编辑器以请求进行回滚。编辑器根据备忘录中获取到的元数据来直接替换自身的原状态。结构如下:

代码示例#

package test

import "testing"

type Snapshot string

type Originator struct {
	Snapshot Snapshot
}

// Originator create the Memento by self
func (o *Originator) CreateMemento() *Memento {
	return &Memento{
		Snapshot: o.Snapshot,
	}
}

func (e *Originator) restoreMemento(m *Memento) {
	e.Snapshot = m.GetSavedSnapshot()
}

type Caretakers struct {
	MementoArray []*Memento
}

func (c *Caretakers) GetMemento(index int) *Memento {
	if index >= len(c.MementoArray) {
		return nil
	}
	return c.MementoArray[index]
}

func (c *Caretakers) AddMemento(m *Memento) {
	c.MementoArray = append(c.MementoArray, m)
}

type Memento struct {
	Snapshot Snapshot
}

func (m *Memento) GetSavedSnapshot() Snapshot {
	return m.Snapshot
}

func TestMemento(t *testings.T) {
    caretaker := &Caretaker{
        mementoArray: make([]*Memento, 0),
    }

    originator := &Originator{
        Snapshot: "A",
    }

    fmt.Printf("Originator Current Snapshot: %s\n", originator.getSnapshot())
    caretaker.addMemento(originator.createMemento())

    originator.setSnapshot("B")
    fmt.Printf("Originator Current Snapshot: %s\n", originator.getSnapshot())
    caretaker.addMemento(originator.createMemento())

    originator.setSnapshot("C")
    fmt.Printf("Originator Current Snapshot: %s\n", originator.getSnapshot())
    caretaker.addMemento(originator.createMemento())

    originator.restoreMemento(caretaker.getMemento(1))
    fmt.Printf("Restored to Snapshot: %s\n", originator.getSnapshot())

    originator.restoreMemento(caretaker.getMemento(0))
    fmt.Printf("Restored to Snapshot: %s\n", originator.getSnapshot())

}
go

总结#

  • 当你需要创建对象状态快照来恢复其之前的状态时, 可以使用备忘录模式。
  • 当直接访问对象的成员变量、 获取器或设置器将导致封装被突破时, 可以使用该模式。

How To Implement#

  1. 确定担任原发器角色的类。 重要的是明确程序使用的一个原发器中心对象, 还是多个较小的对象。
  2. 创建备忘录类。 逐一声明对应每个原发器成员变量的备忘录成员变量。
  3. 将备忘录类设为不可变。 备忘录只能通过构造函数一次性接收数据。 该类中不能包含设置器。
  4. 如果你所使用的编程语言支持嵌套类, 则可将备忘录嵌套在原发器中; 如果不支持, 那么你可从备忘录类中抽取一个空接口, 然后让其他所有对象通过接口来引用备忘录。 你可在该接口中添加一些元数据操作, 但不能暴露原发器的状态。
  5. 在原发器中添加一个创建备忘录的方法。 原发器必须通过备忘录构造函数的一个或多个实际参数来将自身状态传递给备忘录。
  6. 该方法返回结果的类型必须是你在上一步中抽取的接口 (如果你已经抽取了)。 实际上, 创建备忘录的方法必须直接与备忘录类进行交互。
  7. 在原发器类中添加一个用于恢复自身状态的方法。 该方法接受备忘录对象作为参数。 如果你在之前的步骤中抽取了接口, 那么可将接口作为参数的类型。 在这种情况下, 你需要将输入对象强制转换为备忘录, 因为原发器需要拥有对该对象的完全访问权限。
  8. 无论负责人是命令对象、 历史记录或其他完全不同的东西, 它都必须要知道何时向原发器请求新的备忘录、 如何存储备忘录以及何时使用特定备忘录来对原发器进行恢复。
  9. 负责人与原发器之间的连接可以移动到备忘录类中。 在本例中, 每个备忘录都必须与创建自己的原发器相连接。 恢复方法也可以移动到备忘录类中, 但只有当备忘录类嵌套在原发器中, 或者原发器类提供了足够多的设置器并可对其状态进行重写时, 这种方式才能实现。
设计模式-行为型-备忘录
https://astro-pure.js.org/blog/memento
Author erasernoob
Published at June 5, 2025
Comment seems to stuck. Try to refresh?✨