# Memento（备忘录模式）

Memento（备忘录模式）属于行为型模式，是针对如何捕获与恢复对象内部状态的设计模式。

**意图：在不破坏封装性的前提下，捕获一个对象的内部状态，并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。**

其实备忘录模式思想非常简单，其核心是定义了一个 Memoto（备忘录） 封装对象，由这个对象处理原始对象的状态捕获与还原，其他地方不需要感知其内部数据结构和实现原理，而且 Memoto 对象本身结构也非常简单，只有 `getState` 与 `setState` 一存一取两个方法，后面会详细讲解。

## 举例子

如果看不懂上面的意图介绍，没有关系，设计模式需要在日常工作里用起来，结合例子可以加深你的理解，下面我准备了三个例子，让你体会什么场景下会用到这种设计模式。

### 撤销重做

如果撤销重做涉及到大量复杂对象，每个对象内部状态的存储结构都不同，如果一个一个处理，很容易写出 case by case 的冗余代码，而且在拓展一种新对象结构时（如嵌入 ppt），还需要在撤销重做时对相应结构做处理。备忘录思维相当于一种统一封装思维，不管这个对象结构如何，都可以保存在一个 Memoto 对象中，通过 `setState` 设置对象状态与 `getState` 获取对象状态，这样对于任何类型的对象，画布都可以通过统一的 API 操作进行存取了。

### 游戏保存

玩过游戏的同学都知道，许多游戏支持设置与读取多种存档，如果转换为代码模式，我们可能希望有这样一种 API 进行多存档管理：

```typescript
// 创建一盘游戏。
const game = new Game()
// 玩一会。
game.play()
// 设置一个存档(archive) 1。
const gameArchive1 = game.createArchive()
// 再玩一会。
game.play()
// 设置一个存档(archive) 2。
const gameArchive2 = game.createArchive()
// 再玩一会。
game.play()
// 这个时候角色挂了，提示 “请读取存档”，玩家此时选择了存档 1。
game.loadArchive(gameArchive1)
// 此时游戏恢复存档 1 状态，又可以愉快的玩耍了。
```

其实在游戏保存的例子中，存档就是备忘录（Memoto），而主进程管理游戏状态时，只是简单调用了 `createArchive` 创建存档，与 `load` 读取存档，即可实现复杂的游戏保存与读取功能，全程是不需要关心游戏内部状态到底有多少，以及这么多状态需要如何一一恢复的，这就是得益于备忘录模式的设计。

### 文章草稿保存

富文本编辑器的文档草稿保存也是一样的原理，简单一点只需要一个 Memoto 对象即可，如果要实现复杂一点的多版本状态管理，只需要类似游戏保存机制，存储多个 Memoto 存档即可。

## 意图解释

看到这里，会发现备忘录模式与前端状态管理的保存与恢复很像。以 Redux 类比：

`setState` 就像 `reducer` 处理的最终 `state` 状态一样，对 redux 全局状态来说，它不用关心业务逻辑（有多少 `reducer`，以及每个 `reducer` 做了什么），它只需要知道任何 `reducer` 最后处理完后都是一个 `state` 对象，将其生成出来并存下来即可。

恢复也是一样，`initState` 就类似 `getState`，只要将上一次生成的 `state` 灌进来，就可以完全还原某个时刻的状态，而不需要关心这个状态内部是怎样的。

所以其实备忘录模式早已得到广泛的应用，仔细去理解后，会发现没必要去扣的太细，以及原始设计模式是如何定义的，因为经过几十年的演化，这些设计模式思路早已融入了编程框架的方方面面。

但依照惯例，我们还是再咬文嚼字解释一下意图：

**意图：在不破坏封装性的前提下，捕获一个对象的内部状态，并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。**

重点在于 “不破坏封装性” 这几个字上，程序的可维护性永远是设计模式关注的重点，无论是游戏存档的例子，还是 Redux 的例子，上层框架使用状态时，都不需要知道具体对象状态的细节，而实现这一点的就是 Memoto 这个抽象的备忘录类。

## 结构图

<img width=600 src="https://img.alicdn.com/imgextra/i2/O1CN01ByabMq1W05wDvuVYo_!!6000000002725-2-tps-1604-478.png">

- `Originator`：创建、读取备忘录的发起者。
- `Memento`：备忘录，专门存储原始对象状态，并且防止 Originator 之外的对象读取。
- `Caretaker`：备忘录管理者，一般用数组或链表管理一堆备忘录，在撤销重做或者版本管理时会用到。

## 代码例子

下面例子使用 typescript 编写。

下面是备忘录模式三剑客的定义：

```typescript
// 备忘录
class Memento {
  public state: any

  constructor(state: any) {
    this.state = state
  }

  public getState() {
    return this.state
  }
}

// 备忘录管理者
class Caretaker {
  private stack: Memento[] = []

  public getMemento(){
    return this.stack.pop()
  }

  public addMemento(memoto: Memento){
    this.stack.push(memoto)
  }
}

// 发起者
class Originator {
  private state: any

  public getState() {
    return this.state
  }

  public setState(state: any) {
    this.state = state
  }

  public createMemoto() {
    return new Memoto(this.state)
  }

  public setMemoto(memoto: Memoto) {
    this.state = memoto.getState()
  }

  public void setMemento(Memento memento) {
      state = memento.getState();
  }
}
```

下面是一个简化版客户端使用的例子：

```typescript
// 实例化发起者，比如画布、文章管理器、游戏管理器
const originator = new Originator()

// 实例化备忘录管理者
const caretaker = new Caretaker()

// 设置状态，分别对应：
// 画布的组件操作。
// 文章的输入。
// 游戏的 .play()
originator.setState('hello world')

// 备忘录管理者记录一次状态，分别对应：
// 画布的保存。
// 文章的保存。
// 游戏的保存。
caretaker.setMemento(originator.createMento())

// 从备忘录管理者还原状态，分别对应：
// 画布的还原。
// 文章的读取。
// 游戏读取存档。
originator.setMemento(caretaker.getMemento())
```

在上面例子中，备忘录管理者存储状态是数组，所以可以实现撤销重做，如果要实现任意读档，可以将备忘录变为 `Map` 结构，按照 `key` 来读取，如果没有这些要求，存一个单一的 `Memoto` 也够用了。

## 弊端

备忘录模式存储的是完整状态而非 Diff，所以可能会在运行时消耗大量内存（当然在 Immutable 模式下，通过引用共享可以极大程度缓解这个问题）。

另外就是，备忘录模式已经很大程度上被融合到现代框架中，你在使用状态管理工具时就已经使用了备忘录模式了，所以很多情况下，不需要机械的按照上面的代码例子使用。设计模式重点在于利用它优化了程序的可维护性，而不用强求使用方式和官方描述一模一样。

## 总结

备忘录模式通过备忘录对象，将对象内部状态封装了起来，简化了程序复杂度，这符合设计模式一贯遵循的 “高内聚、低耦合” 原则。

其实践行备忘录模式最好的例子就是 Redux，当项目所有状态都使用 Redux 管理时，你会发现无论是撤销重做，还是保存读取，都可以非常轻松完成，这时候，不要质疑为什么备忘录模式还在解决这种 “遇不到的问题”，因为 Redux 本身就包含了备忘录设计模式的理念。

> 讨论地址是：[精读《设计模式 - Memento 备忘录模式》· Issue #301 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/301)

**如果你想参与讨论，请 [点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

> 版权声明：自由转载-非商用-非衍生-保持署名（[创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)）
