Memento(备忘录模式)属于行为型模式,是针对如何捕获与恢复对象内部状态的设计模式。前端
意图:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象以外保存这个状态。这样之后就可将该对象恢复到原先保存的状态。git
其实备忘录模式思想很是简单,其核心是定义了一个 Memoto(备忘录) 封装对象,由这个对象处理原始对象的状态捕获与还原,其余地方不须要感知其内部数据结构和实现原理,并且 Memoto 对象自己结构也很是简单,只有 getState
与 setState
一存一取两个方法,后面会详细讲解。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 许可证)