精读《设计模式 - Memoto 备忘录模式》

Memento(备忘录模式)

Memento(备忘录模式)属于行为型模式,是针对如何捕获与恢复对象内部状态的设计模式。前端

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

其实备忘录模式思想很是简单,其核心是定义了一个 Memoto(备忘录) 封装对象,由这个对象处理原始对象的状态捕获与还原,其余地方不须要感知其内部数据结构和实现原理,并且 Memoto 对象自己结构也很是简单,只有 getStatesetState 一存一取两个方法,后面会详细讲解。github

举例子

若是看不懂上面的意图介绍,没有关系,设计模式须要在平常工做里用起来,结合例子能够加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。typescript

撤销重作

若是撤销重作涉及到大量复杂对象,每一个对象内部状态的存储结构都不一样,若是一个一个处理,很容易写出 case by case 的冗余代码,并且在拓展一种新对象结构时(如嵌入 ppt),还须要在撤销重作时对相应结构作处理。备忘录思惟至关于一种统一封装思惟,无论这个对象结构如何,均可以保存在一个 Memoto 对象中,经过 setState 设置对象状态与 getState 获取对象状态,这样对于任何类型的对象,画布均可以经过统一的 API 操做进行存取了。编程

游戏保存

玩过游戏的同窗都知道,许多游戏支持设置与读取多种存档,若是转换为代码模式,咱们可能但愿有这样一种 API 进行多存档管理:redux

// 建立一盘游戏。
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 这个抽象的备忘录类。

结构图

  • Originator:建立、读取备忘录的发起者。
  • Memento:备忘录,专门存储原始对象状态,而且防止 Originator 以外的对象读取。
  • Caretaker:备忘录管理者,通常用数组或链表管理一堆备忘录,在撤销重作或者版本管理时会用到。

代码例子

下面例子使用 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();
  }
}

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

// 实例化发起者,好比画布、文章管理器、游戏管理器
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

若是你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

版权声明:自由转载-非商用-非衍生-保持署名( 创意共享 3.0 许可证
相关文章
相关标签/搜索