Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e888c4e22c | ||
|
|
8a3425b5e3 | ||
|
|
9081bde65e | ||
|
|
4f783678da | ||
|
|
299a60a881 | ||
|
|
7e651219b0 | ||
|
|
e1e7bc6f57 |
@@ -0,0 +1,108 @@
|
||||
# Mediator(中介者模式)
|
||||
|
||||
Mediator(中介者模式)属于行为型模式。
|
||||
|
||||
**意图:用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显示地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。**
|
||||
|
||||
前端开发中,最常用的 “数据驱动” 其实就最好的诠释了中介者模式。
|
||||
|
||||
想一个这样的场景:
|
||||
|
||||
1. 按钮点击后,表单提交。按钮需要调用所有表单项获取表单值。
|
||||
2. 表单关联,当勾选了城市后,才出现满意度 Input 框,此时城市勾选按钮需要引用满意度 Input 框。
|
||||
3. 甚至会出现循环引用,两个输入框是互斥的,输入了一个,另一个输入框就要 Disable。
|
||||
4. 当新增加一个表单项时,需要重新建立所有引用关系。
|
||||
|
||||
以上过程式编程方式,维护大型项目几乎是不可能的。然而数据驱动可以很好的解决这个问题,所有表单项都依赖数据,并修改数据,这样当 Input 框联动 Check 时,Input 并不需要感知 Checkbox 的存在,他只要关联数据、修改数据就行了,Checkbox 也只要关联数据和修改数据,这样不但逻辑可以独立完成,甚至可以解决循环引用的问题。
|
||||
|
||||
**在数据驱动的例子中,数据就是中介。** 所有 UI 之间都不会相互引用,而是通过数据这个中介来协同工作,这样做带来的明显好处是可以处理复杂项目,且易于维护。
|
||||
|
||||
## 举例子
|
||||
|
||||
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
|
||||
|
||||
### 数据驱动
|
||||
|
||||
正如开篇说的,数据驱动是中介者非常经典的例子,正是因为引入了 “数据中介者”,才让前端项目的复杂度可以呈几何倍数递增,而代码的逻辑复杂度仅线性递增。因为 UI 是杂乱的且动态的,UI 间依赖会导致关系网非常复杂,且关系网一旦形成,增加一个新元素或修改就变得异常困难。
|
||||
|
||||
中介者模式则避开了 UI 间依赖的关系网,通过数据层统一调度,UI 受控响应,可以大大减少逻辑复杂度。
|
||||
|
||||
### 解决循环依赖
|
||||
|
||||
循环依赖几乎只能利用中介者模式解决:
|
||||
|
||||
```typescript
|
||||
import { b } from './b'
|
||||
export const a = 'a'
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { a } from './a'
|
||||
export const b = 'b'
|
||||
```
|
||||
|
||||
当双方相互引用时,构成循环依赖,不仅对于模块化来说是有问题的,从逻辑上也是讲不通的,因为一定存在递归调用的问题。这是,引入第三方中介者就不仅仅是一种设计模式思维了,而是 a、b 模块中原本就有一些内容是两边公用的,一定需要提出来,而统一提出来的地方就是中介者模式的中介者部分。
|
||||
|
||||
### 企业组织架构
|
||||
|
||||
一个树状企业组织架构中,每个非叶子结点都是中介者,需要给他的子节点分配任务,并协调他们的工作,这样一来,叶子结点不需要有全局观即可工作,因为他们只需负责 “去做自己的事情”,而不需要关心 “是如何协同的”。
|
||||
|
||||
<img width=600 src="https://img.alicdn.com/imgextra/i1/O1CN01BPn79I1mzUt7yWRjB_!!6000000005025-2-tps-582-291.png">
|
||||
|
||||
如图所示,环境部不需要关心人事部做了什么,只要专注做好环境事物即可,他们之间的协调由总经理处理,这是一种分工协作的体现。
|
||||
|
||||
而只存在于理论中的网状企业管理模型,则是没有中介者的例子,所有节点都是非叶子结点,并相互引用,这样一来每个人既要做自己的工作,又要处理自己与公司里其他几万人的协同,几乎是一件不可能完成的事情,所以从设计模式角度来看,也更倾向于使用树状而不是网状模式管理企业。
|
||||
|
||||
## 意图解释
|
||||
|
||||
**意图:用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显示地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。**
|
||||
|
||||
中介者模式非常好理解,直接看字面意思即可。所谓的对象交互,指的是对象之间是如何协同的,中介者做的是处理对象间协同的工作,而不是 “替每个对象干活”。
|
||||
|
||||
最后一句 “可以独立地改变他们之间的交互”,指的是对象之间协同方式不是一成不变的,比如一个输入框组件,只要实现自己的输入功能就行了,而不需要关心是如何与外界交互的。外界可以通过将其嵌入到表单中,成为表单项的一部分,也可以将其包裹一层符号后缀,成为一个专门输入金额的金额输入框。
|
||||
|
||||
## 结构图
|
||||
|
||||
<img width=600 src="https://img.alicdn.com/imgextra/i4/O1CN01FLHDqJ1c1MjH3fM4k_!!6000000003540-2-tps-1602-440.png">
|
||||
|
||||
- Mediator:中介者接口,定义一些通信 API。
|
||||
- ConcreteMediator:具体的中介者,继承 Mediator,协调各个对象。
|
||||
- Colleague:同事类,比如之前提到的输入框、文本框,每个同事之间只要知道中介者即可,他们之间不需要知道对方的存在。
|
||||
|
||||
## 代码例子
|
||||
|
||||
下面例子使用 typescript 编写。
|
||||
|
||||
```typescript
|
||||
const memberA = new Member('美术')
|
||||
const memberB = new Member('程序')
|
||||
|
||||
const picture = memberA.draw() // 美术画出图
|
||||
const product = memberB.code(picture) // 程序按照美术画的图做产品
|
||||
```
|
||||
|
||||
这个例子中,完成了程序与美术的协同,他们各自不需要知道对方的存在。如果后续又引入了产品、测试工种,他们之间不需要做复杂的关联,只需要在中介者增加对应协同逻辑即可。
|
||||
|
||||
## 弊端
|
||||
|
||||
中介者模式虽然好,但过度使用可能使中介者逻辑非常复杂。
|
||||
|
||||
我们常说管理者直接管理人数最好不要超过二十人,原因是协调本身也非常耗费精力,一个中介者节点如果管理的对象过多,可能会导致中介者本身难以维护,甚至出现 BUG。
|
||||
|
||||
另外则是不要过度解耦,当两个对象本身可以构成依赖关系时,使用中介者模式使其强行解耦,带来的只会是更重的理解负担。
|
||||
|
||||
## 总结
|
||||
|
||||
当一个系统对象很多,且之间关联关系很复杂,交叉引用容易产生混乱时,就可能适用中介者模式。
|
||||
|
||||
中介者模式也符合迪米特法则,做到了每个对象了解最少的内容,这样做对于大型程序来说是非常有益的。
|
||||
|
||||
> 讨论地址是:[精读《设计模式 - Mediator 中介者模式》· Issue #299 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/299)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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))
|
||||
@@ -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))
|
||||
@@ -0,0 +1,140 @@
|
||||
# Observer(观察者模式)
|
||||
|
||||
Observer(观察者模式)属于行为型模式。
|
||||
|
||||
**意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。**
|
||||
|
||||
拿项目的 npm 依赖举例子:npm 包与项目是一对多的关系(一个 npm 包被多个项目使用),当 npm 包发布新版本时,如果所有依赖于它的项目都能得到通知,并自动更新这个包的版本号,那么就解决了包版本更新的问题,这就是观察者模式要解决的基本问题。
|
||||
|
||||
## 举例子
|
||||
|
||||
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
|
||||
|
||||
### 对象与视图双向绑定
|
||||
|
||||
在 [精读《设计模式 - Proxy 代理模式》](https://github.com/dt-fe/weekly/blob/v2/178.%E7%B2%BE%E8%AF%BB%E3%80%8A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20Proxy%20%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F%E3%80%8B.md) 中我们也提到了双向绑定概念,只不过代理是实现双向绑定的一个具体方案,而观察者模式才是在描述双向绑定这个概念。
|
||||
|
||||
观察者模式在最初提出的时候,就举了数据与 UI 相互绑定的例子。即同一份数据可以同时渲染为表格与柱状图,那么当操作表格更新数据时,如何让柱状图的数据也刷新?从这个场景引出了对观察者模式的定义,即 “数据” 与 “UI” 是一对多的关系,我们需要一种设计模式实现当 “数据” 变化时,所有依赖于它的 “UI” 都得到通知并自动更新。
|
||||
|
||||
### 拍卖
|
||||
|
||||
拍卖由一个拍卖员与多为拍卖者组成。拍卖时,由 A 同学喊出的竞价(我出 100)就是观察者向目标发出的 `setState` 同时,此时拍卖员喊出(有人出价 100,还有更高的吗?)就是一个 `notify` 通知行为,拍卖员通知了现场竞价全员,刷新了他们对当前最高价的信息。
|
||||
|
||||
### 聊天室
|
||||
|
||||
聊天室由一个中央服务器与多个客户端组成。客户端发送消息后,就是向中央服务器发送了 `setState` 更新请求,此时中央服务器通知所有处于同一聊天室的客户端,更新他们的信息,从而完成一次消息的发送。
|
||||
|
||||
## 意图解释
|
||||
|
||||
数据与 UI 的例子已经详细说明了其意图含义,这里就不赘述了。
|
||||
|
||||
## 结构图
|
||||
|
||||
<img width=600 src="https://img.alicdn.com/imgextra/i4/O1CN011HxE9E24luDnEQiqA_!!6000000007432-2-tps-1774-670.png">
|
||||
|
||||
- Subject: 目标,即例子中的 “数据”。
|
||||
- Observer: 观察者,即例子中的 “表格”、“柱状图”。
|
||||
|
||||
还是以数据与 UI 同步为例,当表格发生操作修改数据时,表格这个 TableObserver 会调用 Subject(数据)的 `setState`,此时数据被更新了。然后数据这个 `Subject` 维护了所有监听(包括表格 `TableObserver` 与柱状图 `ColumnChartObserver`),此时 `setState` 内会调用 `notify` 遍历所有监听,并依次调用 `Update` 方法,每个监听的 `Update` 方法都会调用 `getState` 获取最新数据,从而实现表格更新后 -> 更新数据 -> 表格、柱状图同时刷新。
|
||||
|
||||
为了更好的理解,以这张协作图为例:
|
||||
|
||||
<img width=600 src="https://img.alicdn.com/imgextra/i1/O1CN01QuF29i1RpKAEcCPrX_!!6000000002160-2-tps-1578-728.png">
|
||||
|
||||
- `aConcreteSubject`: 对应例子中的数据。
|
||||
- `aConcreteObserver`: 对应例子中的表格。
|
||||
- `anotherConcreteObserver`: 对应例子中的柱状图。
|
||||
|
||||
## 代码例子
|
||||
|
||||
下面例子使用 typescript 编写。
|
||||
|
||||
> PS: 为了简化处理,就不定义 Subject 接口与 ConcreteSubject 了,而是直接用 Subject 类代替。Observer 也同理。
|
||||
|
||||
```typescript
|
||||
// 目标,管理所有观察者
|
||||
class Subject {
|
||||
// 观察者数组
|
||||
private observers: Observer[] = []
|
||||
// 状态
|
||||
private state: State
|
||||
|
||||
// 通知所有观察者
|
||||
private notify() {
|
||||
this.observers.forEach(eachObserver => {
|
||||
eachObserver.update()
|
||||
})
|
||||
}
|
||||
|
||||
// 新增观察者
|
||||
public addObserver(observer: Observer) {
|
||||
this.observers.push(observer)
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
public setState(state: State) {
|
||||
this.state = state
|
||||
this.notify()
|
||||
}
|
||||
|
||||
// 读取状态
|
||||
public getState() {
|
||||
return this.state
|
||||
}
|
||||
}
|
||||
|
||||
// 观察者
|
||||
class Observer {
|
||||
// 维护目标
|
||||
private subject: Subject
|
||||
|
||||
constructor(subject: Subject) {
|
||||
this.subject = subject
|
||||
this.subject.addObserver(this)
|
||||
}
|
||||
|
||||
// 更新
|
||||
public update() {
|
||||
// 比如渲染表格 or 渲染柱状图
|
||||
console.log(this.subject.getState())
|
||||
}
|
||||
}
|
||||
|
||||
// 客户端调用
|
||||
const subject = new Subject()
|
||||
// 创建观察者
|
||||
const observer1 = new Observer(subject)
|
||||
const observer2 = new Observer(subject)
|
||||
// 更新状态
|
||||
subject.setState(10)
|
||||
```
|
||||
|
||||
## 弊端
|
||||
|
||||
不要拘泥于实现形式,比如上面代码中的例子,`subject` 与 `observer1`、`observer2` 是一对多的关系,但不一定非要用这种代码组织形式来实现观察者效果。我们也可以利用 Proxy 很轻松的实现:
|
||||
|
||||
```typescript
|
||||
const obj = new Proxy(obj, {
|
||||
get(target,key) {}
|
||||
set(target,key,value) {}
|
||||
})
|
||||
|
||||
renderTable(obj)
|
||||
renderChart(obj)
|
||||
```
|
||||
|
||||
我们可以在 `obj` 被任意一个组件访问时触发 `get`,进而对 UI 与视图进行绑定;被任意一个组件更新时触发 `set`,进而对所有使用到的视图进行刷新。使用设计模式切记不要死板,理解原理就行了,在不同平台有不同的更加优雅的实现方式。
|
||||
|
||||
## 总结
|
||||
|
||||
观察者模式是非常常用的设计模式,它描述了对象一对多依赖关系下,如何通知并更新的机制,这种机制可以用在前端的 UI 与数据映射、后端的请求与控制器映射,平台间的消息通知等大部分场景,无论现实还是程序中,存在依赖且需要通知的场景非常普遍。
|
||||
|
||||
> 讨论地址是:[精读《设计模式 - Observer 观察者模式》· Issue #302 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/302)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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))
|
||||
@@ -0,0 +1,151 @@
|
||||
# State(状态模式)
|
||||
|
||||
State(状态模式)属于行为型模式。
|
||||
|
||||
**意图:允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。**
|
||||
|
||||
简单来说,就是将 “一个大 class + 一堆 if else” 替换为 “一堆小 class”。一堆小 class 就是一堆状态,用一堆状态代替 if else 会更好拓展与维护。
|
||||
|
||||
## 举例子
|
||||
|
||||
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
|
||||
|
||||
### 团队接口人
|
||||
|
||||
团队是由很多同学组成的,但有一位接口人 TL,这位 TL 可能一会儿和产品经理谈需求,一会儿和其他 TL 谈规划,一会儿和 HR 谈人事,总之要做很多事情,很显然一个人是忙不过来的。TL 通过将任务分发给团队中每个同学,而不让他们直接和产品经理、其他 TL、HR 接触,那么这位 TL 的办事效率就会相当高,因为每个同学只负责一块具体的业务,而 TL 在不同时刻叫上不同的同学,让他们出面解决他们负责的专业领域问题,那么在外面看,这位 TL 团队能力很广,在内看,每个人负责的事情也比较单一。
|
||||
|
||||
### 台灯按钮
|
||||
|
||||
我们经常会看到只有一个按钮的台灯,但是可以通过按钮调节亮度,大概是如下一个循环 “关 -> 弱光 -> 亮 -> 强光 -> 关”,那么每次按按钮后,要跳转到什么状态,其实和当前状态有关。我们可以用 if else 解决这个问题,也可以用状态模式解决。
|
||||
|
||||
用状态模式解决,就是将这四个状态封装为四个类,每个类都执行按下按钮后要跳转到的状态,这样未来新增一种模式,只要改变部分类即可。
|
||||
|
||||
### 数据库连接器
|
||||
|
||||
在数据库连接前后,这个连接器的状态显然非常不同,我们如果仅用一个类描述数据库连接器,则内部免不了写大量分支语句进行状态判断。那么此时有更好的方案吗?状态模式告诉我们,可以创建多个不同状态类,比如连接前、连接中、连接后三种状态类,在不同时刻内部会替换为不同的子类,它们都继承同样的父类,所以外面看上去不需要感知内部的状态变化,内部又可以进行状态拆分,进行更好的维护。
|
||||
|
||||
## 意图解释
|
||||
|
||||
**意图:允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。**
|
||||
|
||||
重点在 “内部状态” 的理解,也就是状态改变是由对象内部触发的,而不是外部,所以 **外部根本无需关心对象是否用了状态模式**,拿数据库连接器的例子来说,不管这个类是用 if else 堆砌的,还是用状态模式做的,都完全不妨碍它对外提供的稳定 API(接口问题),所以状态模式实质上是一种内聚的设计模式。
|
||||
|
||||
## 结构图
|
||||
|
||||
<img width=600 src="https://img.alicdn.com/imgextra/i1/O1CN01tbZ0bQ1w8xcUgbWTJ_!!6000000006264-2-tps-1350-486.png">
|
||||
|
||||
- State: 状态接口,类比为台灯状态。
|
||||
- ConcreteState: 具体状态,都继承于 State,类比为台灯的强光、弱光状态。
|
||||
|
||||
## 代码例子
|
||||
|
||||
下面例子使用 typescript 编写。
|
||||
|
||||
```typescript
|
||||
// 定义状态接口
|
||||
interface State {
|
||||
// 模拟台灯点亮
|
||||
show: () => string
|
||||
}
|
||||
|
||||
class Light1 implements State {
|
||||
constructor(context: Context) {
|
||||
this.context = context
|
||||
}
|
||||
|
||||
show() {
|
||||
return '关灯'
|
||||
}
|
||||
|
||||
// 按下按钮
|
||||
public click() {
|
||||
this.context.setState(new Light2(this.context))
|
||||
}
|
||||
}
|
||||
|
||||
class Light2 implements State {
|
||||
constructor(context: Context) {
|
||||
this.context = context
|
||||
}
|
||||
|
||||
show() {
|
||||
return '弱光'
|
||||
}
|
||||
|
||||
// 按下按钮
|
||||
public click() {
|
||||
this.context.setState(new Light3(this.context))
|
||||
}
|
||||
}
|
||||
|
||||
class Light3 implements State {
|
||||
constructor(context: Context) {
|
||||
this.context = context
|
||||
}
|
||||
|
||||
show() {
|
||||
return '亮'
|
||||
}
|
||||
|
||||
// 按下按钮
|
||||
public click() {
|
||||
this.context.setState(new Light4(this.context))
|
||||
}
|
||||
}
|
||||
|
||||
class Light4 implements State {
|
||||
constructor(context: Context) {
|
||||
this.context = context
|
||||
}
|
||||
|
||||
show() {
|
||||
return '强光'
|
||||
}
|
||||
|
||||
// 按下按钮
|
||||
public click() {
|
||||
this.context.setState(new Light1(this.context))
|
||||
}
|
||||
}
|
||||
|
||||
// 台灯
|
||||
public class Lamp {
|
||||
// 当前状态
|
||||
private currentState = new Light1(this)
|
||||
|
||||
protected setState(state: State) {
|
||||
this.currentState = state
|
||||
}
|
||||
|
||||
// 按下按钮
|
||||
public click() {
|
||||
this.getState().click()
|
||||
}
|
||||
}
|
||||
|
||||
const lamp = new Lamp() // 关闭
|
||||
lamp.click() // 弱光
|
||||
lamp.click() // 亮
|
||||
lamp.click() // 强光
|
||||
lamp.click() // 关闭
|
||||
```
|
||||
|
||||
其实有很多种方式来实现,不必拘泥于形式,大体上只要保证由多个类实现不同状态,每个类实现到下一个状态切换就好了。
|
||||
|
||||
## 弊端
|
||||
|
||||
该用 if else 的时候还是要用,不要但凡遇到 if else 就使用状态模式,那样就是书读傻了。一定要判断,是否各状态间差异很大,且使用状态模式后维护性比 if else 更好,才应该用状态模式。
|
||||
|
||||
## 总结
|
||||
|
||||
在合适场景下,状态模式可以使代码更符合开闭原则,每个类独立维护时,逻辑也更精简、聚焦,更易维护。
|
||||
|
||||
> 讨论地址是:[精读《设计模式 - State 状态模式》· Issue #303 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/303)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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))
|
||||
@@ -0,0 +1,86 @@
|
||||
# Strategy(策略模式)
|
||||
|
||||
Strategy(策略模式)属于行为型模式。
|
||||
|
||||
**意图:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。本模式使得算法可以独立于使用它的客户而变化。**
|
||||
|
||||
策略是个形象的表述,所谓策略就是方案,我们都知道任何事情都有多种方案,而且不同方案都能解决问题,所以这些方案可以相互替换。我们将方案从问题中抽象出来,这样就可以抛开问题,单独优化方案了,这就是策略模式的核心思想。
|
||||
|
||||
## 举例子
|
||||
|
||||
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
|
||||
|
||||
### 地图导航
|
||||
|
||||
我们去任何地方都可以选择步行、骑车、开车、公交,不同的方案都可以帮助我们到达目的地,那么很明显应该将这些方案变成策略封装起来,接收的都是出发点和目的地,输出的都是路线。
|
||||
|
||||
### 布局方式
|
||||
|
||||
比如我们做一个报表系统,在 PC 使用珊格布局,在移动端使用流式布局,其实内容还是那些,只是布局方式会随着不同终端大小做不同的适配,那么布局的适配就是一种策略,它可以与报表内容无关。
|
||||
|
||||
我们可以将布局策略单独抽取出来,以后甚至可以适配电视机、投影仪等等不同尺寸的场景,而不需要对其他代码做任何改动,这就是将布局策略从代码中解耦出来的好处。
|
||||
|
||||
### 排序算法
|
||||
|
||||
当我们调用 `.sort` 时,使用的是什么排序算法?可能是冒泡、快速、插入排序?其实无论何种排序算法,本质上做的事情都是一样的,我们可以事先将排序算法封装起来,针对不同特性的数组调用不同的排序算法。
|
||||
|
||||
## 意图解释
|
||||
|
||||
**意图:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。本模式使得算法可以独立于使用它的客户而变化。**
|
||||
|
||||
算法可以理解为策略,我们制定许多解决某个场景的策略,这些策略都可以独立的解决这个场景的问题,这样下次遇到这个场景时,我们就可以选择任何策略来解决,而且我们还可以脱离场景,单独优化策略,只要接口不变即可。
|
||||
|
||||
这个意图本质上就是解耦,解耦之后才可以分工。想想一个复杂的系统,如果所有策略都耦合在业务逻辑里,那么只有懂业务的人才能小心翼翼的维护,但如果将策略与业务解耦,我们就可以独立维护这些策略,为业务带来更灵活的变化。
|
||||
|
||||
## 结构图
|
||||
|
||||
<img width=600 src="https://img.alicdn.com/imgextra/i1/O1CN01oQ1Vvc1kHPXNk8vzD_!!6000000004658-2-tps-1578-480.png">
|
||||
|
||||
- Strategy: 策略公共接口。
|
||||
- ConcreteStrategy: 具体策略,实现了上面这个接口。
|
||||
|
||||
只要你的策略符合接口,就满足策略模式的条件。
|
||||
|
||||
## 代码例子
|
||||
|
||||
下面例子使用 typescript 编写。
|
||||
|
||||
```typescript
|
||||
interface Strategy {
|
||||
doSomething: () => void
|
||||
}
|
||||
|
||||
class Strategy1 implements Strategy {
|
||||
doSomething: () => {
|
||||
console.log('实现方案1')
|
||||
}
|
||||
}
|
||||
|
||||
class Strategy2 implements Strategy {
|
||||
doSomething: () => {
|
||||
console.log('实现方案2')
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
new System(new Strategy1()) // 策略1实现的系统
|
||||
new System(new Strategy2()) // 策略2实现的系统
|
||||
```
|
||||
|
||||
## 弊端
|
||||
|
||||
不要走极端,不要每个分支走一个策略模式,这样会导致策略类过多。当分支逻辑简单清晰好维护时,不需要使用策略模式抽象。
|
||||
|
||||
## 总结
|
||||
|
||||
策略模式是很重要的抽象思维,我们首先要意识到问题有许多种解法,才能意识到策略模式的存在。当一个问题需要采取不同策略,且策略相对较复杂,且未来可能要拓展新策略时,可以考虑使用策略模式。
|
||||
|
||||
> 讨论地址是:[精读《设计模式 - Strategy 策略模式》· Issue #304 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/304)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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))
|
||||
@@ -0,0 +1,88 @@
|
||||
# Template Method(模版模式)
|
||||
|
||||
Template Method(模版模式)属于行为型模式。
|
||||
|
||||
**意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。TemplateMethod 使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。**
|
||||
|
||||
## 举例子
|
||||
|
||||
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
|
||||
|
||||
### 模版文件
|
||||
|
||||
我们办事打印的文件就是模版文件,只需要写上个人基本信息再签字就可以了,我们不需要做太多的重复劳动,因为某些场景下大部分内容是可以固化下来的。比如买卖房屋,那大部分甲方乙方的条款是固定的,最大的变化是甲方与乙方的不同,我们在模版上签字时,就是利用了模版模式减少了大量写条款的时间。
|
||||
|
||||
### 实例化
|
||||
|
||||
实例化也可以认为是模版模式的某种表现形式,因为对于工厂方法,我们传入不同的初始值可能给出不同结果,那么实际上就是用很少的代码撬动了很大一块功能,起到了抽象作用。
|
||||
|
||||
### Vue 模版
|
||||
|
||||
Vue 模版更符合我们对模版直觉的理解。这个场景中,模版指的是 HTML 模版,我们只需要在模版中以 `{}` 形式描述一些变量,就可以生成一块只有局部变量变化的模版 DOM,非常方便。
|
||||
|
||||
## 意图解释
|
||||
|
||||
**意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。TemplateMethod 使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。**
|
||||
|
||||
这个设计模式初衷是用于面向对象的,所以考虑的是如何在类中运用模版模式。首先定义一个父类,实现了一些算法,再将需要被子类重载的方法提出来,子类重载这些部分方法后,即可利用父类实现好的算法做一些功能。
|
||||
|
||||
比如说父类方法 `function a() { b() + c() }`,此时子类只需要重定义 b 与 c 方法,即可复用 a 的算法(b 与 c 相加)。当然这个例子比较简单,当算法较为复杂时,模版模式的好处将凸显出来。
|
||||
|
||||
## 结构图
|
||||
|
||||
<img width=600 src="https://img.alicdn.com/imgextra/i1/O1CN01DLdURm1t90ovmVI1g_!!6000000005858-2-tps-1150-652.png">
|
||||
|
||||
- ConcreteClass: 具体的父类。可以看到父类中实现了 TemplateMethod,其调用了 primitiveOperation1 与 primitiveOperation2, 所以子类只需要重载这两个方法,即可享用 TemplateMethod 提供的算法。
|
||||
|
||||
假设 TemplateMethod 是 `OpenDocument` 打开文档的作用,那么 primitiveOperation1 可能是 `CanOpen` 校验,`primitiveOperation2` 可能是 `ReadDocument` 读取文档方法。
|
||||
|
||||
我们只要专心实现具体的细节方法,而不需要关心他们之间是如何相互作用的,父级会帮我们实现它。之后我们就可以调用子类的 `OpenDocument` 实现打开文档了。
|
||||
|
||||
## 代码例子
|
||||
|
||||
下面例子使用 typescript 编写。
|
||||
|
||||
```typescript
|
||||
class View {
|
||||
doDisplay(){}
|
||||
|
||||
display() {
|
||||
this.setFocus()
|
||||
this.doDisplay()
|
||||
this.resetFocus()
|
||||
}
|
||||
}
|
||||
|
||||
class MyView extends View {
|
||||
doDisplay(){
|
||||
console.log('myDisplay')
|
||||
}
|
||||
}
|
||||
|
||||
const myView = new MyView()
|
||||
myView.display()
|
||||
```
|
||||
|
||||
这个例子中,`doDisplay` 表示父类希望子类重载的方法,一般以 `do` 约定打头。
|
||||
|
||||
## 弊端
|
||||
|
||||
模版模式用在类中,本质上是固定不可变的结构,进一步缩小重写方法的范围,重写的范围越小,代码可复用度就越高,所以一定要在具有通用算法可提取的情况下使用,而不要为了节省代码行数而过度使用。
|
||||
|
||||
另外前端开发中,HTML 本身就很契合模版模式,因为 HTML 中有大量标签描述千变万化的 UI 结构,可复用的地方实在太多太多,所以非常适合模版模式,所以不要认为模版模式仅能在类中使用,模版模式还能在脚手架使用呢,比如填入一些表单自动生成代码。
|
||||
|
||||
学习这个设计模式时,注意不要固化思维在其定义的类这个框子中,因为设计模式写于 1994 年,其中提到的模式已经被大量迁移运用,能否识别并做适当的知识迁移,是 20 多年后的今天学习设计模式的关键。
|
||||
|
||||
## 总结
|
||||
|
||||
模版模式与策略模式有一定相似处,模版模式是改变算法的一部分,而策略模式是将策略完全提取出来,所以可以改变算法的全部。
|
||||
|
||||
> 讨论地址是:[精读《设计模式 - Template Method 模版模式》· Issue #305 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/305)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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))
|
||||
@@ -0,0 +1,156 @@
|
||||
# Visitor(访问者模式)
|
||||
|
||||
Visitor(访问者模式)属于行为型模式。
|
||||
|
||||
**意图:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。**
|
||||
|
||||
访问者,顾名思义,就是对象访问的一种设计模式,我们可以在不改变要访问对象的前提下,对访问对象的操作做拓展。
|
||||
|
||||
## 举例子
|
||||
|
||||
由于能应用访问者模式的场景很少,所以这里只举一个例子。
|
||||
|
||||
### 建造游戏中的资源设计
|
||||
|
||||
假设你制作一款城市建造游戏,游戏的基础资源只有毛皮、木材、铜矿、铁矿。你需要用这些资源建造各种,比如造楼房、做衣服、制作家具、门、空调、甚至锅、健身房、游泳馆等。记住一个前提,就是你想把游戏设计的非常逼真,所以每种资源的不同使用方法都非常定制,不是简单的消耗 N 个数量就能完成,比如制作家具时,需要用到毛皮和木材,此时毛皮和木材对环境、制作人、资金都有不同的要求。
|
||||
|
||||
常见的想法是,我们将资源的所有使用方法都枚举在资源类中,这样资源就在用到不同场景时,调用不同方法即可。但问题是资源本身其实较为固定,我们每增加一种用途就修改一次木材、铁矿的类会显得非常麻烦。
|
||||
|
||||
能不能在增加新用途时,不修改原始资源类呢?答案是可以用访问者模式。
|
||||
|
||||
## 意图解释
|
||||
|
||||
**意图:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。**
|
||||
|
||||
第一句话指明了 Visitor 的作用,即 “作用于某对象结构中的各元素的操作”,也就是 Visitor 是用于操作对象元素的。“它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作” 也就是说,你可以只修改 Visitor 本身完成新操作的定义,而不需要修改原本对象。
|
||||
|
||||
这看上去比较奇怪,给对象定义新的操作,竟然不用修改对象本身,而通过改另外一个对象就可以?这就是 Visitor 设计的奇妙之处,它将对象的操作权移交给了 Visitor。
|
||||
|
||||
## 结构图
|
||||
|
||||
<img width=600 src="https://img.alicdn.com/imgextra/i3/O1CN01uUsrEF1LACvBPBs7j_!!6000000001258-2-tps-1738-1346.png">
|
||||
|
||||
- Visitor:访问者接口。
|
||||
- ConcreteVisitor:具体的访问者。
|
||||
- Element 可以被访问者使用的元素,它必须定义一个 Accept 属性,接收 visitor 对象。这是实现访问者模式的关键。
|
||||
- ObjectStructure:对象结构,存储了多个 Element,利用 Visitor 进行批量操作。
|
||||
|
||||
|
||||
可以看到,要实现操作权转让到 Visitor,核心是元素必须实现一个 Accept 函数,将这个对象抛给 Visitor:
|
||||
|
||||
```typescript
|
||||
class ConcreteElement implements Element {
|
||||
public accept(visitor: Visitor) {
|
||||
visitor.visit(this)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
从上面代码可以看出这样一条链路:Element 通过 `accept` 函数接收到 Visitor 对象,并将自己的实例抛给 Visitor 的 `visit` 函数,**这样我们就可以在 Visitor 的 `visit` 方法中拿到对象实例,完成对对象的操作。**
|
||||
|
||||
## 代码例子
|
||||
|
||||
下面例子使用 typescript 编写。
|
||||
|
||||
```typescript
|
||||
class ConcreteVisitorX implements Visitor{
|
||||
public visit(element: ELement) {
|
||||
element.accept(this);
|
||||
}
|
||||
|
||||
public visit(concreteElementA: ConcreteElementA) {
|
||||
console.log('X 操作 A')
|
||||
}
|
||||
|
||||
public visit(concreteElementB: ConcreteElementB) {
|
||||
console.log('X 操作 B')
|
||||
}
|
||||
}
|
||||
|
||||
class ConcreteVisitorY implements Visitor{
|
||||
public visit(element: ELement) {
|
||||
element.accept(this);
|
||||
}
|
||||
|
||||
public visit(concreteElementA: ConcreteElementA) {
|
||||
console.log('Y 操作 A')
|
||||
}
|
||||
|
||||
public visit(concreteElementB: ConcreteElementB) {
|
||||
console.log('Y 操作 B')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
配合上面已经写过的 `Element`,可以看到,经历了如下过程:
|
||||
|
||||
```typescript
|
||||
// 先创建元素
|
||||
const element = new ConcreteElement()
|
||||
|
||||
// 访问者 X
|
||||
const visitorX = new ConcreteVisitorX()
|
||||
|
||||
// 访问者 Y
|
||||
const visitorY = new ConcreteVisitorY()
|
||||
|
||||
// 然后让访问者 visit 观察一下元素
|
||||
visitorX.visit(element as Element)
|
||||
visitorY.visit(element as Element)
|
||||
```
|
||||
|
||||
要注意的是,访问者观察的 Element 一定要是通用类型 Element,而不是一个具体类型 ConcreteElement,否则访问者模式抽象性就无法体现了,因为 Visitor 可以访问任何类型的 Element,所以先把接口传进去。
|
||||
|
||||
到这里,我们看看下面经历了什么:首先 Visitor 定义的 `visit` 会被调用,由于符合了 Element 这个通用类型,所以会调用 Element 接口定义的 `accept` 函数,这是所有元素都有的方法。
|
||||
|
||||
接下来,每个具体元素都重写了 `accept` 方法:
|
||||
|
||||
```typescript
|
||||
public accept(visitor: Visitor) {
|
||||
visitor.visit(this)
|
||||
}
|
||||
```
|
||||
|
||||
所以又调用了 Visitor 的 `visit` 函数,不同的是,此时的参数是具体 Element 类型,所以可能调用到的是具体对某个元素处理的 `visit` 方法,比如:
|
||||
|
||||
```typescript
|
||||
public visit(concreteElementA: ConcreteElementA) {
|
||||
console.log('X 操作 A')
|
||||
}
|
||||
```
|
||||
|
||||
最终就输出了 “X 操作 A” 这段话。
|
||||
|
||||
我们可以看到这样的程序拓展性有这么些:
|
||||
|
||||
1. Element 元素的所有子类都不用频繁修改,只要修改 Visitor 即可。
|
||||
2. 一个 Visitor 可以选择性的操作任何类型的 Element 子类,只要申明了处理函数即可处理,不申明就不会命中,比较方便。在城市建造的例子中,可以提现为锅需要用铁制作,但不需要消耗木材,所以不需要定义木材的 `visit` 方法。
|
||||
3. 可以定义多种 Visitor,对同一种 Element 子类也可以有不同的操作,这在我们城市建造的例子中,可以体现为门和窗户,对铁矿的使用是不同的。
|
||||
|
||||
由此一来,我们就能在城市建造的例子中拓展出任意多种使用资源的场景,而无需让资源感知到这些场景的存在。
|
||||
|
||||
## 弊端
|
||||
|
||||
访问者模式使用场景非常有限,请确定你的场景满足以上情况再使用。如果资源并不需要频繁修改和拓展,那么就没必要使用访问者模式。
|
||||
|
||||
## 总结
|
||||
|
||||
访问者模式的精髓,就是在不断拓展的业务场景中,防止基础元素代码不断膨胀。
|
||||
|
||||
假设我们这款城市建造游戏有 20 人团队开发,每周发布 2 个版本,每个版本新增了几种资源的组合使用方式,由于资源一共就木材、铁矿、铜矿那么几种,如果你作为团队负责人,任大家随意修改这些资源基础类,过不了半年就会发现,木材类的成员方法突破了 100 种,而且以每天新增 2 种的速度不断增加,你会明显发现自己精心打造的程序即将变成一堆屎山。
|
||||
|
||||
更要命的是,你还搞不清楚哪些场景的用法是打包的,当一种使用场景下线时,已存在的成员方法还不敢删除。
|
||||
|
||||
假设你用了访问者模式,会发现,每天因为迭代而新增的那几个方法,都会放到一个新 Visitor 文件下,比如一种纳米材料的门板在游戏 V1.5 版本被引进,它对材料的使用会体现在新增一个 Visitor 文件,资源本身的类不会被修改,这既不会引发协同问题,也使功能代码按照场景聚合,不论维护还是删除的心智负担都非常小。
|
||||
|
||||
访问者模式背后的思考本质还是,基础的元素数量一般不会随着程序迭代产生太大变化,而对这些基础元素的使用方式或组合使用会随着程序迭代不断更新,我们将变化更快的通过 Visitor 打包提取出来,自然会更利于维护。
|
||||
|
||||
> 讨论地址是:[精读《设计模式 - Visitor 访问者模式》· Issue #306 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/306)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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))
|
||||
Reference in New Issue
Block a user