forked from ascoders/weekly
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
178 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
# 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)) |