Memento(备忘录模式)属于行为型模式,是针对如何捕获与恢复对象内部状态的设计模式。
意图:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
其实备忘录模式思想非常简单,其核心是定义了一个 Memoto(备忘录) 封装对象,由这个对象处理原始对象的状态捕获与还原,其他地方不需要感知其内部数据结构和实现原理,而且 Memoto 对象本身结构也非常简单,只有 getState
与 setState
一存一取两个方法,后面会详细讲解。
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
如果撤销重做涉及到大量复杂对象,每个对象内部状态的存储结构都不同,如果一个一个处理,很容易写出 case by case 的冗余代码,而且在拓展一种新对象结构时(如嵌入 ppt),还需要在撤销重做时对相应结构做处理。备忘录思维相当于一种统一封装思维,不管这个对象结构如何,都可以保存在一个 Memoto 对象中,通过 setState
设置对象状态与 getState
获取对象状态,这样对于任何类型的对象,画布都可以通过统一的 API 操作进行存取了。
玩过游戏的同学都知道,许多游戏支持设置与读取多种存档,如果转换为代码模式,我们可能希望有这样一种 API 进行多存档管理:
// 创建一盘游戏。
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 本身就包含了备忘录设计模式的理念。
如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)