Compare commits

..
40 Commits
Author SHA1 Message Date
ascoders 7e651219b0 184 2021-02-22 10:11:00 +08:00
ascoders e1e7bc6f57 183 2021-02-01 09:05:50 +08:00
ascoders beed8434ec 182 2021-01-25 10:32:08 +08:00
ascoders ab13166c0a Merge branch 'v2' of https://github.com/dt-fe/weekly into v2 2021-01-20 15:12:50 +08:00
ascoders 71a48cde3b fix type 2021-01-20 15:12:30 +08:00
黄子毅 c603ac0d1d Merge pull request #297 from sosamuel/patch-1
Fix 校正笔误
2021-01-19 21:10:59 +08:00
Samuel Chia 316d3d98aa Fix 校正笔误 2021-01-18 10:43:42 +08:00
ascoders 0465cf43db 181 2021-01-18 10:14:59 +08:00
ascoders 65de19ddb6 181 2021-01-18 10:08:02 +08:00
ascoders 9c0f11cc55 181 2021-01-18 10:07:20 +08:00
ascoders 754ceb3d5a fix: typo 2021-01-10 11:28:15 +08:00
ascoders dcc5247ba0 fix typo 2021-01-10 11:23:26 +08:00
ascoders 02de198a8b 180 2021-01-10 11:21:15 +08:00
ascoders b7cfc4c6e3 179 2020-12-26 10:50:01 +08:00
ascoders 46a094e711 178 2020-12-20 23:22:19 +08:00
ascoders b5cd1b2379 Merge branch 'v2' of https://github.com/dt-fe/weekly into v2 2020-12-13 21:45:29 +08:00
ascoders cfe94e6aa7 177 2020-12-13 21:44:46 +08:00
黄子毅 3362f25775 Merge pull request #289 from wiolem/patch-1
docs: typo onDropOver
2020-12-09 17:59:49 +08:00
William 87f9704831 Update 140.精读《结合 React 使用原生 Drag Drop API》.md 2020-12-09 10:40:49 +08:00
ascoders 6c7084dfe1 Merge branch 'v2' of https://github.com/dt-fe/weekly into v2 2020-12-06 21:50:03 +08:00
ascoders 164590159d 176 2020-12-06 21:49:38 +08:00
黄子毅 b8ab46d26b Merge pull request #287 from spiritree/patch-3
Update 168.精读《设计模式 - Builder 生成器》.md
2020-12-03 20:19:48 +08:00
深樹 183b4fc41a Update 168.精读《设计模式 - Builder 生成器》.md
maybe misspell `Persion`->`Person`
2020-12-01 15:28:53 +08:00
ascoders 2b7ab34978 Merge branch 'v2' of https://github.com/dt-fe/weekly into v2 2020-11-29 18:04:36 +08:00
ascoders 2ca7132569 175 2020-11-29 18:04:27 +08:00
黄子毅 aac9aa34f8 Merge pull request #283 from JeromeLin/patch-3
Update 003.精读前后端渲染之争.md
2020-11-29 11:29:25 +08:00
黄子毅 ad8a645462 Merge pull request #285 from spiritree/patch-1
Update 171.精读《设计模式 - Singleton 单例模式》.md
2020-11-26 21:46:06 +08:00
深樹 2dbe4d6368 Update 171.精读《设计模式 - Singleton 单例模式》.md
class method: `instance` -> `getInstance`
2020-11-25 19:14:31 +08:00
ascoders d76253ef75 Merge branch 'v2' of https://github.com/dt-fe/weekly into v2 2020-11-22 11:10:11 +08:00
ascoders 46b68f1933 174 2020-11-22 11:09:40 +08:00
JeromeLin 8aab27bc3f Update 003.精读前后端渲染之争.md 2020-11-20 09:51:01 +08:00
黄子毅 22f118e1b7 Merge pull request #282 from JeromeLin/patch-2
Update 001.精读 js 模块化发展.md
2020-11-19 21:21:20 +08:00
JeromeLin 922eb6dc59 Update 001.精读 js 模块化发展.md 2020-11-19 17:37:21 +08:00
黄子毅 20526273d3 Merge pull request #281 from giscafer/patch-1
fix: typo
2020-11-18 10:35:33 +08:00
Nickbing Lao f3222c646b fix: typo
错别字
2020-11-16 16:09:23 +08:00
ascoders 9ae28c5e85 set img size 2020-11-15 16:45:15 +08:00
ascoders e72318adb4 173 2020-11-15 16:37:09 +08:00
ascoders 2682dc0504 update 2020-11-08 20:16:33 +08:00
ascoders 8b4611b47a 172 2020-11-08 20:12:56 +08:00
ascoders 25938c60a6 171 2020-11-01 21:41:08 +08:00
20 changed files with 1691 additions and 9 deletions
+1 -1
View File
@@ -103,7 +103,7 @@ YUI3 的 sandbox 像极了差不多同时出现的 AMD 规范,但早期 yahoo
> 看到大家基本都提到了 HTTP/2,对这项技术解决前端模块化及资源打包等工程问题抱有非常大的期待。很多人也认为 HTTP/2 普及后,基本就没有 Webpack 什么事情了。
不过 Webpack 作者 @sokra 在他的文章 [webpack & HTTP/2](https://medium.com/webpack/webpack-http-2-7083ec3f3ce6#.zdo4juvgo) 里提到了一个新的 Webpack 插件 `AggressiveSplittingPlugin`。简单的说,这款插件就是为了充分利用 HTTP/2 的文件缓存能力,将你的业务代码自动拆分成若干个数十 KB 的小文件。后续若其中任意一个文件发生变化,可以保证其他的小 chunck 不需要重新下载。
不过 Webpack 作者 @sokra 在他的文章 [webpack & HTTP/2](https://medium.com/webpack/webpack-http-2-7083ec3f3ce6#.zdo4juvgo) 里提到了一个新的 Webpack 插件 `AggressiveSplittingPlugin`。简单的说,这款插件就是为了充分利用 HTTP/2 的文件缓存能力,将你的业务代码自动拆分成若干个数十 KB 的小文件。后续若其中任意一个文件发生变化,可以保证其他的小 chunk 不需要重新下载。
可见,**即使不断的有新技术出现,也依然需要配套的工具来将前端工程问题解决方案推向极致。**
+2 -2
View File
@@ -27,7 +27,7 @@
- 服务端渲染不需要先下载一堆 js 和 css 后才能看到页面(首屏性能)
- SEO
- 服务端渲染不用关心浏览器兼容性问题(随浏览器发展,这个优点逐渐消失)
- 服务端渲染不用关心浏览器兼容性问题(随浏览器发展,这个优点逐渐消失)
- 对于电量不给力的手机或平板,减少在客户端的电量消耗很重要
以上服务端优势其实只有首屏性能和 SEO 两点比较突出。但现在这两点也慢慢变得微不足道了。React 这类支持同构的框架已经能解决这个问题,尤其是 Next.js 让同构开发变得非常容易。还有静态站点的渲染,但这类应用本身复杂度低,很多前端框架已经能完全囊括。
@@ -142,4 +142,4 @@ Next.js 是时下非常流行的基于 React 的同构开发框架。作者之
> 讨论地址是:[前后端渲染之争 · Issue #5 · dt-fe/weekly](http://link.zhihu.com/?target=https%3A//github.com/dt-fe/weekly/issues/5)
> 如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,每周五发布。
> 如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,每周五发布。
@@ -179,7 +179,7 @@ const dragProps = {
```jsx
const dropProps = {
onDropOver: ev => {
onDragOver: ev => {
// 做一些样式处理,提示用户此时松手会将元素防止在何处
},
onDrop: ev => {
+3 -3
View File
@@ -267,9 +267,9 @@ class Square {
针对以下三种短路语法提供了快捷赋值语法:
```typescript
a &&= b; // a = a && b
a ||= b; // a = a || b
a ??= b; // a = a ?? b
a &&= b; // a && (a = b)
a ||= b; // a || (a = b)
a ??= b; // a ?? (a = b)
```
### catch error unknown 类型
@@ -622,7 +622,7 @@ const App = () => (
import { Designer } from '@alife/bi-designer'
const App = () => (
<Designer
shouldAddComponents={({deletedComponentInstancesArray, pageSchema}) => {
shouldDeleteComponents={({deletedComponentInstancesArray, pageSchema}) => {
// 阻止删除
return false
}}
@@ -38,7 +38,7 @@ Builder(生成器)属于创建型模式,针对的是单个复杂对象的
**意图:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。**
我们再理解一次意图,所谓构建与表示分离,就是指一个对象 `Persion` 并不是简单的 `new Persion()` 就可以实例化出来的,如果可以,那就是构建与表示一体。**所谓构建与表示分离,就是指 `Persion` 只能描述,而不能通过 `new Persion()` 实例化,将实例化工作通过 Builder 实现,这样同样一个构建过程可以创建不同的 `Persion` 实例。**
我们再理解一次意图,所谓构建与表示分离,就是指一个对象 `Person` 并不是简单的 `new Person()` 就可以实例化出来的,如果可以,那就是构建与表示一体。**所谓构建与表示分离,就是指 `Person` 只能描述,而不能通过 `new Person()` 实例化,将实例化工作通过 Builder 实现,这样同样一个构建过程可以创建不同的 `Person` 实例。**
在乐高积木的例子中,通过乐高创建的房子并不是 `new House()` 出来,而是将构建与表示分离了,工厂流水线中我们创建一个黄桃罐头,不是通过 `new 黄桃罐头()`,而是通过流水线不同拼装方式来完成,在数据库例子中,我们没有通过 `new DB()` 的方式创建数据库,而是通过 Builder 来创建,这都体现了构建与表示的分离。
@@ -0,0 +1,115 @@
# Singleton(单例模式)
Singleton(单例模式)属于创建型模式,提供一种对象获取方式,保证在一定范围内是唯一的。
**意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。**
其实单例模式在前端体会的不明显,原因有:
1. 前端代码本身在单机运行,创建的任何变量都是天然分布式的,不需要担心影响另一个用户。
2. 后端代码是一对多的,分辨出哪些资源是请求间共享的,哪些是请求内独有的很重要。
另外我们说到单例,是隐含了一个范围的,指的是在某个范围内单例,比如在一个上下文中,还是一个房间中,还是一个进程,一个线程中单例,不同场景范围会不同。
## 举例子
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
### 多人游戏的共享物品
玩过游戏的同学都知道,我们在每局游戏中使用的公共物品在当前房间中是唯一的,但在游戏房间间却不是唯一的,所以这些公共物品肯定有不同的类去描述,那每局游戏中怎么拿公共物品,可以保证拿到的是当前局内唯一的?
### Redux 数据流
其实前端的 Redux 数据流本身就是单例模式,在一个应用中,数据是唯一的,但可以有不同的 UI 使用这份唯一的数据,甚至把一个表格组件展示在两个不同地方,比如全屏模式,但数据依然是一份,我们没有必要为了全屏展示表格,就让它再发一次取数请求,完全可以和原来的表格共享一份数据。
### 数据库连接池
每个 SQL 查询都依赖数据库连接池,如果每次查询都建立一次数据库连接池,则建立连接的速度会远远慢于 SQL 查询速度,因此你会怎么设计数据库连接池的获取方法?
## 意图解释
单例模式的意图很简单,几乎就是其字面含义:
**意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。**
对于多人游戏的共享物品,比如一口锅,要保证在一局游戏内唯一,就要提供一种方法访问到唯一实例。
Redux 数据流的 `connect` 装饰器就是全局访问点的一种设计。
数据库连接池可以提前初始化好,并通过固定 API 提供这个唯一实例。
## 结构图
<img width=600 src="https://img.alicdn.com/tfs/TB1qVf20QY2gK0jSZFgXXc5OFXa-1060-342.png">
`Singleton` 是单例模式的接口,客户只能通过其定义的 `instance()` 访问实例,以保证单例。
## 代码例子
下面例子使用 typescript 编写。
```typescript
class Ball {
private _instance = undefined
// 构造函数申明为 private,就可以阻止 new Ball() 行为
private constructor() {}
public static getInstance = () => {
if (this._instance === undefined) {
this._instance = new Ball()
}
return this._instance
}
}
// 使用
const ball = Ball.getInstance()
```
可以仔细想想,为什么这个例子把单例写成了静态方法,而不是一个全局变量?其实全局变量也能解决问题,但由于会污染全局,要尽可能通过模块化方式解决,上面的例子就是一个较好的封装方式。
当然这只是一个最简单的例子,实际上单例模式还有几种模式:
### 饿汉式
初始化时就生成一份实例,这样调用时直接就能获取。
### 懒汉式
就是代码例子中写的,按需实例化,即调用的时候再实例化。
> **要注意,按需不一定是什么好事,如果 New 的成本很高还按需实例化,可能把系统异常的风险留到随机的触发时机,导致难以排查 BUG,另外也会影响第一次实例化时的系统耗时。**
对 JAVA 来说,单例还需要考虑并发性,有 **双重检测、静态内部类、枚举** 等办法解决,这里不具体展开。
## 弊端
单例模式的问题有:
- 对面向对象不太友好。对封装、继承、多态支持不够友好。
- 不利于梳理类之间的依赖关系。毕竟单例是直接调用的,而不是在构造函数申明的,所以要梳理关系要看完每一行代码才能确定。
- 可拓展性不好。万一要支持多例就比较难拓展,比如全局数据流可能因为微前端方案改成多实例、数据库连接池为了分治 SQL 改成多实例,都是有可能的,在系统设计之初就要考虑到未来是否还会保持单例。
- 可测试性不好,因为单例是全局共享的,无法保证测试用例间的隔离。
- 无法使用构造函数传参。
另外单例模式还可以被工厂方法所替代,所以不用特别纠结一种设计模式,可以结合使用,工厂函数也可以内嵌单例模式。
## 总结
单例模式概念、用法都简单,是架构设计常用方案,但要充分理解到单例模式的弊端,防止不恰当的使用。
<img width=400 src="https://img.alicdn.com/tfs/TB15O3YmOpE_u4jSZKbXXbCUVXa-904-224.png">
> 讨论地址是:[精读《设计模式 - Singleton 单例模式》· Issue #278 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/278)
**如果你想参与讨论,请 [点击这里](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,149 @@
# Adapter(适配器模式)
Adapter(适配器模式)属于结构型模式,别名 `wrapper`,结构性模式关注的是如何组合类与对象,以获得更大的结构,我们平常工作大部分时间都在与这种设计模式打交道。
**意图:将一个类的接口转换成客户希望的另一个接口。Adapter 模式使得原本由于接口不兼容而不能在一起工作的那些类可以一起工作。**
这个设计模式的意图很好懂,就是把接口不兼容问题抹平。注意,也仅仅能解决接口不一致的问题,而不能解决功能不一致的问题。
## 举例子
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
### 接口转换器
插座的种类很多,我们都用过许多适配器,将不同的插头进行转换,可以在不替换插座的情况下正常使用。
USB 接口转换也同样精彩,有将 TypeC 接口转换为 TypeA 的,也有将 TypeA 接口转换为 TypeC 的,支持双向转换。
接口转换器就是我们在生活中使用到的适配器模式,因为厂商并没有生产一个新的插座,我们也没有因为接口不适配而换一个手机,一切只需要一个接口转换器即可,这就是运用设计模式的收益。
### 数据库 ORM
ORM 屏蔽了 SQL 这一层,带来的好处是不需要理解不同 SQL 语法之间的区别,对于通用功能,ORM 会根据不同的平台,比如 Postgresql、Mysql 进行 SQL 的转换。
对 ORM 来说,屏蔽不同平台的差异,就是利用适配器模式做到的。
### API Deprecated
当一个广泛使用的库进行了含有 break change 的升级时,往往要留给开发者足够的时间去升级,而不能升级后就直接挂掉,因此被废弃的 API 要标记为 `deprecated`,而这种被废弃标记的 API 的实际实现,往往是使用新的 API 替代,这种场景正是使用了适配器模式,将新的 API 适配到旧的 API,实现 API Deprecated。
## 意图解释
上面三个例子都满足下面两个条件:
1. API 不兼容:因为接口的不同;数据库 SQL 语法的不同;框架 API 的不同。
2. 但能力已支持:插座都拥有充电或读取能力;不同的 SQL 都拥有查询数据库能力;新 API 覆盖了旧 API 的能力。
这样就可以通过适配器满足 Adapter 的意图:
**意图:将一个类的接口转换成客户希望的另一个接口。Adapter 模式使得原本由于接口不兼容而不能在一起工作的那些类可以一起工作。**
## 结构图
适配器的实现分为继承与组合模式。
下面是名词解释:
- `Adapter` 适配器,把 `Adeptee` 适配成 `Target`
- `Adaptee` 被适配的内容,比如不兼容的接口。
- `Target` 适配为的内容,比如需要用的接口。
继承:
<img width=400 src="https://img.alicdn.com/tfs/TB1iy7Gk4vbeK8jSZPfXXariXXa-1590-518.png">
适配器继承 `Adaptee` 并实现 `Target`,适用场景是 `Adaptee``Target` 结构类似的情况,因为这样只需要实现部分差异化即可。
组合:
<img width=400 src="https://img.alicdn.com/tfs/TB1SrW21EY1gK0jSZFMXXaWcVXa-1524-500.png">
组合的拓展性更强,但工作量更大,如果 `Target``Adaptee` 结构差异较大,适合用组合模式。
## 代码例子
下面例子使用 typescript 编写。
继承:
```typescript
interface ITarget {
// 标准方式是 hello
hello: () => void
}
class Adaptee {
// 要被适配的类方法叫 sayHello
sayHello() {
console.log('hello')
}
}
// 适配器继承 Adaptee 并实现 ITarget
class Adapter extends Adaptee implements ITarget {
hello() {
// 用 sayHello 对接到 hello
super.sayHello()
}
}
```
组合:
```typescript
interface ITarget {
// 标准方式是 hello
hello: () => void
}
class Adaptee {
// 要被适配的类方法叫 sayHello
sayHello() {
console.log('hello')
}
}
// 适配器继承 Adaptee 并实现 ITarget
class Adapter implements ITarget {
private adaptee: Adaptee
constructor(adaptee: Adaptee) {
this.adaptee = adaptee
}
hello() {
// 用 adaptee.sayHello 对接到 hello
this.adaptee.sayHello()
}
}
```
## 弊端
**使用适配器模式本身就可能是个问题**,因为一个好的系统内部不应该做任何侨界,模型应该保持一致性。只有在如下情况才考虑使用适配器模式:
1. 新老系统接替,改造成本非常高。
2. 三方包适配。
3. 新旧 API 兼容。
4. 统一多个类的接口。一般可以结合工厂方法使用。
## 总结
适配器模式也符合开闭原则,在不对原有对象改造的前提下,构造一个适配器就能完成模块衔接。
适配器模式的实现分为类与对象模式,类模式用继承,对象模式用组合,分别适用于 `Adaptee``Target` 结构相似与结构差异较大的场景,在任何情况下,组合模式都是灵活性最高的。
最后用一张图概括一下适配器模式的思维:
<img width=400 src="https://img.alicdn.com/tfs/TB16L2n1AY2gK0jSZFgXXc5OFXa-1254-630.png">
> 讨论地址是:[精读《设计模式 - Adapter 适配器模式》· Issue #279 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/279)
**如果你想参与讨论,请 [点击这里](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,111 @@
# Bridge(桥接模式)
Bridge(桥接模式)属于结构型模式,是一种解决继承后灵活拓展的方案。
**意图:将抽象部分与它的实现部分分离,使它们可以独立地变化。**
桥接模式比较难理解,我会一步步还原该设计模式的思考,让你体会这个设计模式是如何一步一步被提炼出来的。
## 举例子
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
### 汽车生产线改造为新能源生产线
汽油车与新能源汽车的生产流程有很大相似之处,那么汽油车生产线能否快速改造为新能源汽车生产线呢?
如果汽油车生产线没有将内部实现解耦,只把生产汽油车的各部分独立了出来,对新能源车生产线是没什么用处的,但如果汽油车生产线提供了更底层的能力,比如加装轮胎,加装方向盘,那么这些步骤是可以同时被汽油车与新能源车所共享的。
在设计汽油车生产线时,就将生产过程与汽油车解耦,使其可以快速运用到新能源汽车的生产,这就是桥接模式的一种运用。
### 窗口(Window)类的派生
假设存在一个 Window 窗口类,其底层实现在不同操作系统是不一样的,假设对于操作系统 A 与 B,分别有 AWindow 与 BWindow 继承自 Window,现在要做一个新功能 ManageWindow(管理器窗口),就要针对操作系统 A 与 B 分别生成 AManageWindow 与 BManageWindow,这样显然不容易拓展。
无论我们新增支持 C 操作系统,还是新增支持一个 IconWindow,类的数量都会成倍提升,因为我们所做的 AMangeWindow 与 BMangeWindow 同时存在两个即以上的独立维度,这使得增加维度时,代码变得很冗余。
### 适配多个搭建平台的物料
做前端搭建平台时,经常出现一些物料(组件)因为固化了某个搭建平台的 API,因此无法迁移到另一个搭建平台,如果要迁移,就需要为不同的平台写不同的组件,而这些组件中大部分 UI 逻辑都是一样的,这使得产生大量代码冗余,如果再兼容一个新搭建平台,或者为已有的 10 个搭建平台再创建一个新组件,工作量都是写一个组件的好几倍。
## 意图解释
**意图:将抽象部分与它的实现部分分离,使它们可以独立地变化。**
“抽象” 部分与 “实现” 部分分离,这句话看起来很像接口与实现。确实,如果 “抽象” 指的是 接口(Interface),而 “实现” 指的是 类(Class) 的话,这就是简简单单的 `class MyWindow implements Window` 类实现过程而已。
但后半句话 “使它们可以独立地变化” 会让你难以和前半句联系起来,如果说 “抽象” 不变,“实现” 可以随意改变还好理解,但反过来就难以解释了。
**其实桥接模式中,抽象指的是一种接口(Abstraction),实现指的也是一种接口(Implementor),其中 Implementor 并不是直接实现了 Abstraction 定义的接口,而是提供更底层的方法,使 Abstraction 可以基于它们封装出自己的接口实现。**
这样一来,Abstraction 的接口可以随意变化,毕竟调用的是 Implementor 提供函数的组合,只要 Implementor 提供的功能全面,Implementor 可以不变;相应的,Implementor 的实现也可以随意变化,只要提供的底层函数不变,就不影响 Abstraction 对其的使用。
上面举的三个例子都是这样,我们应该把汽油车生产线的标准与通用汽车生产线标准分离、将具体功能窗口与适配不同操作系统的基础 GUI 能力隔离、将组件功能与平台功能隔离,只有做到了抽象部分与实现部分的隔离,才可以通过组合满足更多场景。
## 结构图
<img width=600 src="https://img.alicdn.com/tfs/TB1mZv52oH1gK0jSZSyXXXtlpXa-1726-696.png">
- Abstraction:定义抽象类的接口。
- RefinedAbstraction:扩充 Abstraction。
- Implementor:定义实现类的接口,该接口可以与 Abstraction 接口不一致。
- ConcreteImplementor:实现 Implementor 接口并定义它的具体实现。
抽象部分就是 Abstraction,实现部分就是 Implementor,在这个结构图中,它们是分离的,可以各自独立变化的,桥接模式,就是指 `imp` 这个桥,通过 Implementor 实现 Abstraction 接口,就算是桥接上了,这种组合的桥接相比普通的类实现更灵活,更具有拓展性。
## 代码例子
对于完全版桥接模式,Implementor 可以有多套实现,Abstraction 不需关心具体用的是哪一种实现,而是通过抽象工厂方式封装。下面举一个简单版的例子。
下面例子使用 typescript 编写。
```typescript
class Window {
private windowImp: WindowImp
public drawBox() {
// 通过画线生成 box
this.windowImp.drawLine(0, 1)
this.windowImp.drawLine(1, 1)
this.windowImp.drawLine(1, 0)
this.windowImp.drawLine(0, 0)
}
}
// 拓展 window 就非常容易
class SuperWindow extends Window {
public drawIcon {
// 通过自定义画线
this.windowImp.drawLine(0, 5)
this.windowImp.drawLine(3, 9)
}
}
```
桥接模式的精髓,通过上面的例子可以这么理解:
`Window` 的能力是 `drawBox`,那继承 `Window` 容易拓展 `drawIcon` 吗?默认是不行的,因为 `Window` 并没有提供这个能力。经分析可以看出,划线是一种基础能力,不应该与 `Window` 代码耦合,因此我们将基础能力放到 `windowImp` 中,这样 `drawIcon` 也可以利用其基础能力画线了。
## 弊端
不要过度抽象,桥接模式是为了让类的职责更单一,维护更便捷,但如果只是个小型项目,桥接模式会增加架构设计的复杂度,而且不正确的模块拆分,把本来关联的逻辑强制解耦,在未来会导致更大的问题。
另外桥接模式也有简单与复杂模式之分,只有一种实现的场景就不要用抽象工厂做过度封装了。
## 总结
桥接模式让我们重新审视类的设计是否合理,把类中不相关,或者说相互独立的维度抽出去,由桥接模式做桥接的方式使用,这样会使每个类功能更内聚,代码量更少更清晰,组合能力更强大,更容易做拓展。
下图做了一个简单的解释:
<img width=500 src="https://img.alicdn.com/tfs/TB1nossndTfau8jSZFwXXX1mVXa-1308-1078.png">
> 讨论地址是:[精读《设计模式 - Bridge 桥接模式》· Issue #280 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/280)
**如果你想参与讨论,请 [点击这里](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,111 @@
# Composite(组合模式)
Composite(组合模式)属于结构型模式,是一种统一管理树形结构的抽象方式。
**意图:将对象组合成树形结构以表示 “部分 - 整体” 的层次结构。Composite 使得用户对单个对象和组合对象的使用具有一致性。**
## 举例子
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
### 公司组织关系树
公司组织关系可能分为部门与人,其中人属于部门,有的人有下属,有的人没有下属。如果我们统一将部门、人抽象为组织节点,就可以方便的统计某个部门下有多少人、财务数据等等,而不用关心当前节点是部门还是人。
### 操作系统的文件夹与文件
操作系统的文件夹与文件也是典型的树状结构,为了方便递归出文件夹内文件数量或者文件总大小,我们最好设计的时候就将文件夹与文件抽象为文件,这样每个节点都拥有相同的方法添加、删除、查找子元素,而不需要关心当前节点是文件夹或是文件。
### 搭建平台的组件与容器
容器与组件的关系很小,用户常常认为容器也是一种组件,但搭建平台实现时,容器与组件稍有不同,不同之处在于容器可以嵌套子元素,而组件不可以。如果因此搭建平台就将组件分为容器与组件,会导致 API 割裂为两套,不利于组件开发者维护与用户理解,比较好的设计思路是将组件与容器统一看成组件,组件只是一种没有子元素的特殊容器,这样组件与容器就可以拥有相同的 API,统一理解与操作了。
## 意图解释
**意图:将对象组合成树形结构以表示 “部分 - 整体” 的层次结构。Composite 使得用户对单个对象和组合对象的使用具有一致性。**
比较好理解,组合是指多个对象虽然有一定差异,但共同组合成了一个树形结构,那么对象之间就一定存在 “部分 - 整体” 的关系,组合模式要求我们抽象一个对象 `Component` 作为统一操作模型,叶子结点与非叶子结点都实现了所有功能,即便是没有子元素的叶子结点,为了强调透明性,还是具备比如 `getChildren` 方法,只不过永远都返回 `null`
## 结构图
<img width=600 src="https://img.alicdn.com/tfs/TB19t0j27Y2gK0jSZFgXXc5OFXa-1504-678.png">
其中 `Component` 是组合中对象声明接口,一般会实现所有公共类的所有接口,还要提供一个接口管理其子组件。
`Leaf` 表示叶子结点,没有子结点,相应的 `Composite` 就是有子结点的节点。
可以看到,组合模式就是将树状结构中所有节点统一抽象了,**我们不需要关心叶子结点与非叶子结点的差异,而可以通过组合模式的抽象屏蔽掉这些差异,统一处理。**
## 代码例子
下面例子使用 typescript 编写。
```typescript
// 统一的抽象
class Component {
// 添加子元素
public add() {}
// 获取名称
public getName() {}
// 获取子元素
public getChildren() {}
}
// 非叶子结点
class Composite extends Component {
public add(component: Component) {
this.children.push(component)
}
public getName() {
return this.name
}
public getChildren() {
return this.children
}
}
// 叶子结点
class Leaf extends Component {
public add(component: Component) {
throw Error('叶子结点无法添加元素')
}
public getName() {
return this.name
}
public getChildren() {
return null
}
}
```
最后我们把对所有节点的操作都转为 `Component` 对象,而不用关心这个对象具体是 `Composite``Leaf`
## 弊端
组合模式进行了一层抽象,其实增加了复杂系统中业务复杂度。如果 `Composite``Leaf` 差异过大,那么统一抽象带来的理解成本是很高的。
同时,`Leaf` 不得不实现一些仅 `Composite` 存在的空函数,比如 `add` `delete`,即便这些方法对他们是无意义的,此时可能要进行统一的无效或错误处理,才能使业务层真正不用感知他们的区别,否则 `add` 可能会失败,其本质上还是将节点的区别暴露给了业务层。
## 总结
组合模式是针对树状结构这个特定场景的统一抽象方案,对降低系统复杂度有很重要的意义,同时也不要忘了过度抽象是有害的,我们要拿捏其中的度。
下图做了一个简单的解释:
<img width=500 src="https://img.alicdn.com/tfs/TB1_g24rvzO3e4jSZFxXXaP_FXa-1228-614.png">
程序中始终关注 `Component` 就行了,树状结构的差异已经被抹平。
> 讨论地址是:[精读《设计模式 - Composite 组合模式》· Issue #284 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/284)
**如果你想参与讨论,请 [点击这里](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,104 @@
# Decorator(装饰器模式)
Decorator(装饰器模式)属于结构型模式,是一种拓展对象额外功能的设计模式,别名 `wrapper`
**意图:动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator 模式相比生成子类更为灵活。**
## 举例子
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
### 相框
照片 + 相框 = 带相框的照片,这背后就是一种装饰器模式:照片具有看的功能,相框具有装饰功能,在你看照片的基础上,还能看到精心设计的相框,增加了美感,同时相框还可以增加照片的保存时间与安全性。
相框与照片是一种组合关系,任何照片都可以放到相框中,而不是每个照片生成一个特定的相框,显然,组合的方式更加灵活。
### 带有缓存的文件读写
假设我们有一个类 `FileIO` 用来读写文件,但是没有缓存能力,此时是新建一个 `CachedFileIO` 子类好,还是创建一个 `CachedIO`?
一眼看上去好像 `CachedFileIO` 用起来更方便,而 `CachedIO` 的用法是 `new CachedIO(new FileIO())` 稍微麻烦一些,但如果我们增加一个网络读写类 `NetworkIO`,一个数据库读写类 `DBIO` 呢?
显然,继承的方式会使子类数量极速膨胀,而组合的方式则非常灵活,生成一个支持缓存的网络读写器,只需要 `new CachedIO(new NetworkIO())` 即可,这就是组合灵活的地方。
当然,为了实现这个能力,`CachedIO` 需要与 `FileIO``CachedFileIO``CachedIO` 继承自同一个类,具备相同的接口。
### 搭建平台的组件 wrapper
装饰器模式别名也叫 `wrapper``wrapper` 也经常在前端搭建场景中遇到,当搭建平台加载一个组件时,希望拓展其基础能力,一般会使用 `wrapper` 层对组件进行嵌套,`wrapper` 层就是在不改变 API 的基础上,对第三方组件进行增强。
## 意图解释
**意图:动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator 模式相比生成子类更为灵活。**
不同于继承,组合可以在运行时进行,所以称之为 “动态添加”,这里的 “额外职责” 泛指一切功能,比如在按钮点击时进行一些 log 日志的打印,在绘制 text 文本框时,额外绘制一个滚动条和边框等等。
“就增加功能来说,Decorator 模式相比生成子类更为灵活” 这句话的含义是,组合比继承更灵活,当可拓展的功能很多时,继承方案会产生大量的子类,而组合可以提前写好处理函数,在需要时动态构造,显然是更灵活的。
## 结构图
<img width=600 src="https://img.alicdn.com/tfs/TB1cmhe3FY7gK0jSZKzXXaikpXa-1624-688.png">
`ConcreteComponent` 指的是需要被装饰的组件,可以看到,装饰器 `Decorator` 与他都继承同一个类,这样能保证 API 的一致,才保证无论装饰多少层,始终符合 `Component` 类型。
装饰器如果有多种,就要将 `Decorator` 申明为抽象类,`ConcreteDecoratorA``ConcreteDecoratorB` 分别实现它们,如果只有一种装饰器,可以退化到 `Decorator` 自身就是一种实现。
## 代码例子
下面例子使用 typescript 编写。
```typescript
class Component {
// 具有点击事件
public onClick = () => {}
}
class Decorator extends Component {
private _component
constructor(component) {
this._component = component
}
public onClick = () => {
log('打点')
this._component.onClick()
}
}
const component = new Component()
// 一个普通的点击
component.onClick()
const wrapperComponent = new Decorator(component)
// 一个具有打点功能的点击
wrapperComponent.onClick()
```
其实方法很简单,通过组合,我们得到了一个能力更强的组件,而实现的方式就是利用构造函数保存组件实例,并在复写函数时,增加一些增强实现。
## 弊端
装饰器的问题也是组合的问题,过多的组合会导致:
- 组合过程的复杂,要生成过多的对象。
- 包装器层次增多,会增加调试成本,我们比较难追溯到一个 bug 是在哪一层包装导致的。
## 总结
装饰器模式是非常常用的模式,Decorator 是一个透明的包装,只要保证包装的透明性,就可以最大限度发挥装饰器模式的优势。
最后总结一个装饰器应用图:
<img width=500 src="https://img.alicdn.com/tfs/TB1wlpgqPMZ7e4jSZFOXXX7epXa-1232-478.png">
> 讨论地址是:[精读《设计模式 - Decorator 装饰器模式》· Issue #286 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/286)
**如果你想参与讨论,请 [点击这里](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,99 @@
# Facade(外观模式)
Facade(外观模式)属于结构型模式,是一种日常开发中经常被使用到的设计模式。
**意图:为子系统中的一组接口提供一个一致的界面,Facade 模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。**
## 举例子
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
## 意图解释
### 图书管理员
图书馆是一个非常复杂的系统,虽然图书按照一定规则摆放,但也只有内部人员比较清楚,作为一位初次来的访客,想要快速找到一本书,最好的办法是直接问图书管理员,而不是先了解这个图书馆的设计,因为你可能要来回在各个楼宇间奔走,借书的流程可能也比较长。
图书管理员就起到了简化图书馆子系统复杂度的作用,我们只要凡事询问图书管理员即可,而不需要关心他是如何与图书馆内部系统打交道的。
### 最多跑一次便民服务
浙江省推出的最多跑一次服务非常方便,很多办事流程都简化了,无论是证件办理还是业务受理,几乎只要跑一次,而必须要持续几天的流程也会通过手机短信或者 App 操作完成后续流程。
这就相当于外观模式,因为政府系统内部的办事流程可能没有太大变化,但通过抽象出 Facade(外观),让普通市民可以直接与便民办事处连接,而不需要在车管所与驾校之间来回奔波,背后的事情没有少,只是便民办事处帮你做了。
### Iphone 快捷指令功能
手机的 App 非常多,而我们需要了解每个功能在哪个 App 上才能运用自如,而快捷指令功能可以将 App 的某些功能单独提取出来,形成一套新的功能组,我们可以只接触到 “拍照” “付款” “计算”,而不用管背后是调用了支付宝还是微信、系统内置摄像机还是其他摄像 App,也不用关心这个 App 内部功能的入口在哪里,这些对接都在快接指令中自动完成。
快捷指令也是一种外观模式。
## 意图解释
**意图:为子系统中的一组接口提供一个一致的界面,Facade 模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。**
为降低一个拥有多个接口的子系统内部复杂性,我们需要一个外观来屏蔽内部的复杂性,因此外观模式就是定义一个高层接口,这个接口直连子系统的内部实现,但调用这个高层接口的人不需要关心子系统内部的实现,这样,对于不想了解子系统内部实现的人来说,提高了易用度。
当然如果想要深度定制,就可以绕过外观模式,直接使用子系统提供的类,所以说并不是有了外观模式就必须通过外观调用,而是根据实际需要判断使用哪种调用方式。
## 结构图
<img width=600 src="https://img.alicdn.com/tfs/TB1j9gZ3.T1gK0jSZFrXXcNCXXa-1082-412.png">
可以看到,Facade 直接指向子系统中的类,**而子系统的类不会反向指向 Facade**。
## 代码例子
下面例子使用 typescript 编写。
```typescript
// 假设一个子系统是三个类结合使用的,为了抽象而解耦开了
class A {
constructor(b: B) {
this.b = b
}
}
class B {
constructor(c: C) {
this.c = c
}
}
class C {
}
// 它们组合成了一种常用功能,我们可以使用外观模式屏蔽子类的细节直接使用
class Compile {
public run() {
const parser = new A(new B(new C))
parser.run()
}
}
const compile = new Compile()
compile.run()
```
这样我们只要知道 `Compile` 类就可以了,而不需要了解背后的 `A` `B` `C` 以及其组合关系。
## 弊端
外观模式并不适合于所有场景,当子系统足够易用时,再使用外观模式就是画蛇添足。
另外,当系统难以抽象出通用功能时,外观模式的设计可能也无所适从,因为设计的高层接口可能适用范围很窄,此时外观模式的意义就比较小。
## 总结
其实抽象工厂模式也可以代替外观模式,来实现隐藏子类具体实现的效果,但外观模式描述更具有通用性。
> 讨论地址是:[精读《设计模式 - Facade 外观模式》· Issue #288 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/288)
**如果你想参与讨论,请 [点击这里](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,110 @@
# Flyweight(享元模式)
Flyweight(享元模式)属于结构型模式,是一种共享对象的设计模式。
**意图:运用共享技术有效地支持大量细粒度的对象。**
## 举例子
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
### 富文本编辑器的字母对象
富文本编辑器在英文环境下,其中的文本由大量字母组成,为了便于做统一的格式化、计算等处理,需要将每个字母都存储为对象,但这样存储的代价太大了。
已知英文字母一共 26 个,所以文档中存在大量重复使用的字母,而每个字母除了位置信息外,其它信息都是相同且只读的,那么有办法降低富文本场景巨大的字母对象数量吗?
### 网盘存储
当我们上传一部电影时,有时候几十 GB 的内容不到一秒就上传完了,这是网盘提示你,“已采用极速技术秒传”,你会不会心生疑惑,这么厉害的技术为什么不能每次都生效?
另外,网盘存储时,同一部电影可能都会存放在不同用户的不同文件夹中,而且电影文件又特别巨大,和富文本类似,电影文件也只有存放位置是不同的,而其余内容都特别巨大且只读,有什么办法能优化存储呢?
### 大型多人游戏
玩多人游戏时,为了防止外挂,一般对象的创建与计算是在服务器完成的,那如何保证一个玩家拾取物品后,另一个玩家看到的物品会消失?
其实道理已经不言而喻了,虽然在不同客户端之间,游戏对象是相互独立的,但在一局游戏中,所有玩家的对象在服务器是共享的。
## 意图解释
“共享” 就是享元模式的精髓,将那些大量的,具有很多内部状态而外部状态很少的对象进行共享,就是享元模式的使用方式。
**意图:运用共享技术有效地支持大量细粒度的对象。**
共享技术可以理解为缓存,当一个对象创建后,再次访问相同对象时,就不再创建新的对象了,而只有在访问没有被缓存过的对象时,才创建新对象,并立即缓存起来。
这样做可以有效支持大量细粒度的对象,在富文本例子中,**无数的字母就是大量细粒度对象**,在网盘存储中,**电影文件就是大量细粒度对象**,在大型多人游戏中,**每局游戏内存在大量细粒度对象**。
这些细粒度对象都拥有相同的特征:
- 量特别大,这个很容易理解。
- 具有大量内部状态,且不随着客户端的不同而改变。
- 富文本的字母,不因为展示到不同语句中而发生变化,变化的只有状态;电影文件,不因为放在不同用户的文件夹中而对电影内容产生变化,变化的只有属于哪些用户,放在哪些文件夹里;多人游戏中,同一把武器对象,不因为有多个人的电脑独立运行而拥有更多的弹药,变化的只有在哪些客户端被访问。
- 具有少量外部状态,甚至没有外部状态。在上面已经解释了,字母的位置、电影的位置、游戏对象的客户端都是外部状态,这些外部状态相比于其内部状态来说,大小微乎其微,且方便分离存储。
遇到这种情况,我们就可以将对象内部状态共享,外部状态独立存储,从而节省大量空间。
尤其是对于网盘的场景,承诺给用户 2 TB 的存储空间,这个用户看到其他人分享了 100 个电影,就点击 “下载到我的网盘”,**此时虽然占用了自己 1 TB 的网盘空间,但实际上网盘运营商并没有增加 1 TB 的存储空间,实际可能增加了 1kb 的存储空间,记录了存储位置**,这就是网盘鸡贼的地方,并不占用空间的内容,却占用了用户真金白银购买的存储空间。
当然,这就是享元模式的价值,对网盘公司来说,价值巨大,对用户来说,没有价值。所以享元模式的价值体现在全局,比如对整个富文本编辑器来说,减少了巨量字母对象数量,但对于每一个字母对象而言,并没有任何优化。
## 结构图
<img width=800 src="https://img.alicdn.com/tfs/TB1KMTY4UY1gK0jSZFMXXaWcVXa-1420-886.png">
对于 Client 而言,下图描述了如何共享 Flyweight:
<img width=800 src="https://img.alicdn.com/tfs/TB1JwLL4QL0gK0jSZFtXXXQCXXa-1460-542.png">
- Flyweight: 共享接口,通过这个接口可以操作对象的外部状态。
- ConcreteFlyweight: 实现 Flyweight 接口的对象,这个对象是可被共享的。
- UnsharedConcreteFlyweight: 不被共享的对象,因为在享元模式中,实际上并不是所有对象都可以被共享。
- FlyweightFactory: 创建并管理 Flyweight 对象,通过其返回的 Flyweight 对象,如果已创建,则会返回之前创建的那个,没有的话才会创建一个新的。
- Client: 使用 Flyweight 的客户端。
通过第二个图可以明显看到,两个不同的 Client 持有了相同 `aConcreteFlyweight` 引用。
## 代码例子
下面例子使用 typescript 编写。
```typescript
class FlyweightFactory {
public getFlyWeight(key) {
if (this.flyweight[key]) {
return this.flyweight[key]
}
const flyweight = new Flyweight()
this.flyweight[key] = flyweight
return flyweight
}
}
```
`FlyweightFactory` 提供的 `getFlyWeight` 方法,实际上是按照 `key``flyweight` 实例进行缓存,相同 `key` 下只存储一个 `flyweight` 实例。
## 弊端
如果细粒度对象不多,则没必要使用享元模式。
另外,就算细粒度对象很多,如果对象内部状态并不多,主要都是外部状态,那么享元模式就起不到什么作用了,**因为享元模式通过共享对象,只能节省内部状态,而不能节省外部状态。**
另外,如果享元模式映射到的共享对象数量并没有比原始对象少出数量级关系,使用的意义也不大。比如富文本编辑器的例子,对于英文来说,一共就 26 个字母,那么 1 万字的文章优化比例是 10000:26,但对于中文文章而言,文字实例本身就很多,可能 1 万字的文章中,汉字去重后依然有 3000 个,那么优化比例就是 10000:3000,此时享元模式的意义就没那么大了。
## 总结
享元模式的本质就是尽可能的共享对象,特别适用于存在大量细粒度对象,而这些对象内部状态特别多,外部状态较少的场景。
对于云存储来说,享元模式是必须使用的,因为云存储的场景决定了,存在大量细粒度文件对象,而存在大量只读的文件,就非常适合共享一个对象,每个用户存储的只是引用。
> 讨论地址是:[精读《设计模式 - Flyweight 享元模式》· Issue #290 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/290)
**如果你想参与讨论,请 [点击这里](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,100 @@
# Proxy(代理模式)
Proxy(代理模式)属于结构型模式,通过访问代理对象代替访问原始对象,以获得一些设计上的便捷。
**意图:为其他对象提供一种代理以控制这个对象的访问。**
## 举例子
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
### 获得文本对象长度
获得一个文本对象长度,必须要真正渲染出来,而渲染是比较耗时的,我们可能只在某些场景下需要访问文本对象长度,而更多时候只需要读取文本内容,这两种操作耗时是完全不同的,如何做到业务层调用无感知,来优化执行耗时呢?
代理模式可以解决这个问题,我们将业务层使用的文本对象替换为代理对象,这个代理对象初始化并不渲染文本,而是在调用文本长度时才渲染。
### 对象访问保护
某个大型系统开发完了,突然要求增加代码访问权限体系,不同模块对相同的底层对象拥有不同访问权限,此时这个权限控制逻辑如果写入底层对象,就违背了开闭原则,而对象本身的实现也不再纯粹,增加了维护成本,如何做到不修改对象本身,实现权限控制呢?
代理模式也能解决,将底层对象导出替换为代理对象,由代理对象控制访问权限即可。
### 对象与视图双向绑定
Angular 或 Vue 这类前端框架采用双向绑定视图更新技术,即对象修改后,使用到的视图会自动刷新,这就需要做到以下两点:
1. 在对象被访问时,记录调用的视图绑定。
2. 在对象被修改时,刷新调用它的视图。
问题是,在业务代码使用对象与修改对象的地方插入这段逻辑,显然会增加巨大的维护成本,如何做到业务层无感知呢?
代理模式可以很好的解决这个问题,其实业务层拿到的对象已经是代理对象了,它在被访问与被修改时,都会执行固定的钩子做视图绑定与视图刷新。
## 意图解释
**意图:为其他对象提供一种代理以控制这个对象的访问。**
代理模式的意图很容易理解,就是通过代理对象代替原始对象的访问。
这只是代理模式的实现方式,代理模式真正的难点不在于理解它是如何工作的,而是理解哪些场景适合用代理,或者说创建了代理对象,怎么用才能发挥它的价值。
在上面例子中,已经举出了几种常见代理使用场景:
1. 对开销大的对象使用代理,以按需使用。
2. 对需要保护的对象进行代理,在代理层做权限控制。
3. 在对象访问与修改时要执行一些其他逻辑,适合在代理层做。
## 结构图
<img width=600 src="https://img.alicdn.com/imgextra/i3/O1CN01eZHGHQ28t0oeHYzas_!!6000000007989-2-tps-1262-522.png">
使用时关系如下:
<img width=600 src="https://img.alicdn.com/imgextra/i4/O1CN01iwyMKQ1KbOnR0N2AP_!!6000000001182-2-tps-1270-206.png">
Subject 定义的是 RealSubject 与 Proxy 共用的接口,这样任何使用 RealSubject 的地方都可以使用 Proxy。
RealSubject 指的是原始对象,Proxy 是一个代理实体。
关系图中可以看出,当客户端要访问 subject 时,第一层访问的是 Proxy 代理,由这个代理将 realSubject 转发给客户端。
## 代码例子
下面例子使用 typescript 编写。
```typescript
// 对象 obj
const proxy = new Proxy(obj, {
get(target,key) {}
set(target,key,value) {}
})
```
JS 创建代理还是蛮简单的,代理可以控制对象的所有成员属性,包括成员变量与成员方法的访问(get)与修改(set)。
## 弊端
代理模式会增加微弱的开销,因此请不要将所有对象都变成代理,没有意义的代理只会徒增程序开销。
另外代理对象过多,也会导致调试困难,因为代理层的存在,我们往往可能忽略这一层带来的影响,导致忘记这个对象其实是一个代理。
## 总结
代理和继承有足够多的相似之处,继承中,子类几乎可以人为是对父类的代理,子类可以重写父类的方法。但代理和继承还是有区别的:
如果你没有采用 `new Proxy` 这种 API 创建代理,而是采用继承的方式实现,你会一下子继承这个类的所有方法,而做不到按需控制访问权限的灵活效果,所以代理比继承更加灵活。
JS 的 `new Proxy` 对应了 Java 动态代理模式,一般认为动态代理比静态代理更强大。
最后,还要重申那句话,代理模式理解与运用并不难,难就难在能否在恰当的场合想到它,双向绑定几乎是代理模式最好的例子。
> 讨论地址是:[精读《设计模式 - Proxy 代理模式》· Issue #291 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/291)
**如果你想参与讨论,请 [点击这里](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,97 @@
# Chain of Responsibility(职责链模式)
Chain of Responsibility(职责链模式)属于行为型模式。行为型模式不仅描述对象或类的模式,还描述它们之间的通信模式,比如对操作的处理应该如何传递等等。
**意图:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。**
> 几乎所有设计模式,在了解到它之前,笔者就已经在实战中遇到过了,因此设计模式的确是从实践中得出的真知。但另一方面,如果没有实战的理解,单看设计模式是枯燥的,而且难以理解的,因此大家学习设计模式时,要结合实际问题思考。
## 举例子
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
### 中间件机制
设想我们要为一个后端框架实现中间件(知道 Koa 的同学可以理解为 Koa 的洋葱模型),在代码中可以插入任意多个中间件,每个中间件都可以对请求与响应进行处理。
由于每个中间件只响应自己感兴趣的请求,因此只有运行时才知道这个中间件是否会处理请求,那么中间件机制应该如何设计,才能保证其功能和灵活性呢?
### 通用帮助文案
如果一个大型系统中,任何一个模块点击都会弹出帮助文案,但并不是每个模块都有帮助文案的,如果一个模块没有帮助文案,则显示其父级的帮助文案,如果再没有,就继续冒泡到整个应用,展示应用级别的兜底帮助文案。这种系统应该如何设计?
### JS 事件冒泡机制
其实 JS 事件冒泡机制就是个典型的职责链模式,因为任何 DOM 元素都可以监听比如 `onClick`,不仅可以自己响应事件,还可以使用 `event.stopPropagation()` 阻止继续冒泡。
## 意图解释
JS 事件冒泡机制对前端来说太常见了,但我们换个角度,站在点击事件的角度理解,就能重新发现其设计的精妙之处:
点击事件是叠加在每层 dom 上的,由于 dom 对事件的处理和绑定是动态的,浏览器本身不知道哪些地方会处理点击事件,但又要让每层 dom 拥有对点击事件的 “平等处理权”,所以就产生了冒泡机制,与事件阻止冒泡功能。
通用帮助文案和 JS 事件冒泡很类似,只是把点击事件换成了弹出帮助文案罢了,其场景机理是一样的。
说到这,我们可以再重新理解一下职责链模式的意图:
**意图:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。**
请求指的是某个触发机制产生的请求,是一个通用概念。“避免请求的发送者和接收者之间的耦合关系”,指的是如果我们只有一个对象有处理请求的机会,那接收者就与发送者之间耦合了,其他接收者必须通过这个接收者才能继续处理,这种模式不够灵活。
后半句描述的是如何设计,可以实现这个灵活的模式,即将对象连成一条链,沿着链条传递该请求,直到有一个对象处理它为止。还要理解到,任何一个对象都拥有阻断请求继续传递的能力。
在中间件机制的例子中,后端 Web 框架对 Http 请求的处理就是个运用职责链模式的典型案例,因为后端框架要处理的请求是平行关系,任何请求都可能要求被响应,但对请求的处理是通过插件机制拓展的,且对每个请求的处理都是一个链条,存在处理、加工、再处理的逻辑关系。
## 结构图
<img width=600 src="https://img.alicdn.com/imgextra/i4/O1CN01PUN6ed1FnhM2CeZDn_!!6000000000532-2-tps-1126-500.png">
Handler 就是对请求的处理,可以看到这里是一条环路,只要处理完之后就可以交给下一个 Handler 进行处理,可以在中途拦截后中断,也可以穿透整条链路。
`ConcreteHandler` 是具体 Handler 的实现,他们都需要继承 Handler 以具备相同的 `HandleRequest` 方法,这样每一个处理中间件就都拥有了处理能力,使得这些对象连成的链条可以对请求进行传递。
## 代码例子
职责链实现方式非常多,比如 Koa 的洋葱模型实现原理就值得再写一篇文章,感兴趣的同学可以阅读 [co 源码](https://github.com/tj/co)。这里仅介绍最简单场景的实现方案。
职责链的简单实现模式也分为两种,一种是每个对象本身维护到下一个对象的引用,另一种是由 Handler 维护后继者。
下面例子使用 typescript 编写。
```typescript
public class Handler {
private nextHandler: Handler
public handle() {
if(nextHandler) {
nextHandler.handle()
}
}
}
```
每个 Handler 的默认行为就是触发下一个链条的 `handle`,因此什么都不做的话,这个链条是完全打通的,因此我们可以在链条的任何一环进行处理。
处理的方式就是重写 `handle` 函数,我们在重写时,可以维持对 `nextHandler.handle()` 的调用,以使得链条继续向后传递,也可以不调用,从而终止链条向后传递。
## 弊端
职责链模式不保证每个中间件都有机会处理请求,因为中间件顺序的问题,后面中间件可能被前面的中间件阻断,因此当中间件之间存在不信任关系时,职责链模式并不能保证中间件调用的可靠性。
另外就是不要扩大设计模式的使用范围,对一堆对象的连续调用就没必要使用职责链模式,因为职责链适合处理对象数量不确定、是否处理请求由每个对象灵活决定的场景,而确定了对象数量以及是否调用的场景,就没必要使用职责链模式了。
## 总结
职责链模式是插件机制常用的设计模式,在事件机制、请求处理中有广泛的应用。
职责链模式还可以与组合模式组合使用,因为组合模式描述的是一种统一管理的树形结构,每个节点都可以把自己的父节点作为后继节点。实际上 dom 结构就是一种组合模式,事件冒泡就是在其基础上拓展的职责链模式。
> 讨论地址是:[精读《设计模式 - Chain of Responsibility(职责链模式)》· Issue #292 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/292)
**如果你想参与讨论,请 [点击这里](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,119 @@
# Command(命令模式)
Command(命令模式)属于行为型模式。
**意图:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。**
## 举例子
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
### 点菜是命令模式
为什么顾客会找服务员点菜,而不是直接冲到后厨盯着厨师做菜?因为做菜比较慢,肯定会出现排队的现象,而且有些菜可能是一起做效率更高,所以将点菜和做菜分离比较容易控制整体效率。
其实这个社会现象就对应编程领域的命令模式:点菜就是一个个请求,点菜员记录的菜单就是将请求生成的对象,点菜员不需要关心怎么做菜、谁来做,他只要把菜单传到后厨即可,由后厨统一调度。
### 大型软件系统的操作菜单
大型软件操作系统都有一个特点,即软件非常复杂,菜单按钮非常多。但由于菜单按钮本身并没有业务逻辑,所以通过菜单按钮点击后触发的业务行为不适合由菜单按钮完成,此时可利用命令模式生成一个或一系列指令,由软件系统的实现部分来真正执行。
### 浏览器请求排队
浏览器的请求不仅会排队,还会取消、重试,因此是个典型的命令模式场景。如果不能将 `window.fetch` 序列化为一个个指令放入到队列中,是无法实现请求排队、取消、重试的。
## 意图解释
**意图:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。**
一个请求指的是来自客户端的一个操作,比如菜单按钮点击。重点在点击后并不直接实现,而是将请求封装为一个对象,可以理解为从直接实现:
```typescript
function onClick() {
// ... balabala 实现逻辑
}
```
改为生成一个对象,序列化这个请求:
```typescript
function onClick() {
concreteCommand.push({
// ... 描述这个请求
})
// 执行所有命令队列
concreteCommand.executeAll()
}
```
看上去繁琐了一些,但得到了后面所说的好处:“从而使你可用不同的请求对客户进行参数化”,**也就是可以对任何请求进行参数化存储,我们可以在任意时刻调用。** 这相当于掌握了执行时机,可以在任意时刻调用,以实现排队或记录日志,如果再记录下反向操作信息,就可以实现撤销重做了。
## 结构图
<img width=600 src="https://img.alicdn.com/imgextra/i2/O1CN01preTih1iRMuH3oQYY_!!6000000004409-2-tps-1846-620.png">
Command 是命令的接口,一般固定有一个 `execute` 方法。
ConcreteCommand 是命令接口的实现,它会注入具体执行者 `Receiver`,它实现的 `execute` 方法会调用 `receiver.execute` 来具体执行。
`Invoker` 是执行请求的命令,其实上面都在推入命令,并没有真正执行,如果排队结束或点击撤销重做时,就触发了 Invoker 实际,就该调用对应的 Command 执行啦。
## 代码例子
下面例子使用 typescript 编写。
首先看最终执行态,最终执行需要先添加命令,再执行命令:
```typescript
const command1 = new Command('balabala1')
const command2 = new Command('balabala2')
const invoker = new Invoker()
invoker.push(command1)
invoker.push(command2)
invoker.execute()
```
`Invoker` 内部用一个队列维护,执行的时候其实是 `for` 循环执行了每个 `command.execute()`:
```typescript
class Invoker {
push(command) {
// 队列里推入命令
this.commands.push(command)
}
execute() {
this.commands.forEach(command => command.execute())
// 别忘了清空 this.commands
}
}
```
## 弊端
命令模式需要注意序列化大小,一般分为:
1. 仅记录操作。
2. 记录全量快照。
3. 全量快照共享内存。
记录操作是较为精细的管理方式,并且可以延伸出协同编辑功能。记录快照要注意尽量共享内存,防止快照过大,而且协同编辑场景因为快照无法做冲突处理,所以快照模式在协同编辑场景无法应用。
另外要识别没必要使用命令模式的场景,对于没有撤销重做的前端大部分场景来说,都无需改为命令模式。
## 总结
命令模式本质上就是将操作抽象为可序列化的命令,使操作可以在合适的时间执行,这种设计带来了许多额外好处。
利用命令模式可以达到高内聚低耦合的效果,提升代码可维护性,也可以实现撤销重做、协同编辑等功能性需求。
> 讨论地址是:[精读《设计模式 - Command(命令模式)》· Issue #295 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/295)
**如果你想参与讨论,请 [点击这里](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,125 @@
# Interpreter(解释器模式)
Interpreter(解释器模式)属于行为型模式。
**意图:给定一个语言,定义它的文法的一种表示,并定义一个解释器。这个解释器使用该表示来解释语言中的句子。**
任何一门语言,无论是日常语言还是编程语言都有明确的语法,只要有语法就可以用文法描述,并通过语法解释器将字符串的语言结构化。
## 举例子
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
### SQL 解释器
SQL 是一种描述语言,所以也适用于解释器模式。不同的 SQL 方言有不同的语法,我们可以根据某种特定的 SQL 方言定制一套适配它的文法表达式,再利用 antlr 解析为一颗语法书。在这个例子中,antlr 就是解释器。
### 代码编译器
程序语言也因为其天然是字符串的原因,和 SQL、日常语言都类似,需要一种模式解析后才能工作。不同的语言有不同的文法表示,我们只需要一个类似 antlr 的通用解释器,通过传入不同的文法表示,返回不同的对象结构。
### 自然语言处理
自然语言处理也是解释器的一种,首先自然语言处理一般只能处理日常语言的子集,因此先定义好支持的范围,再定义一套分词系统与文法表达式,并将分词后的结果传入灌入了此文法表达式的解释器,这样解释器可以返回结构化数据,根据结构化数据再进行分析与加工。
## 意图解释
**意图:给定一个语言,定义它的文法的一种表示,并定义一个解释器。这个解释器使用该表示来解释语言中的句子。**
对于给定的语言,可以是 SQL、代码或自然语言,“定义它的文法的一种表示” 即文法可以有多种表示,只需定义一种。要注意的是,不同文法执行效率会有差异。
“并定义一个解释器”,这个解释器就是类似 antlr 的东西,传给它一个文法表达式,就可以解析句子了。即:解释器(语言, 文法) = 抽象语法树。
我们可以直接把文法定义耦合到解释器里,但这样做会导致语法复杂时,解释器难以维护。比较好的方式是定义一套与解释器解耦的文法表达式,通过预处理器最终生成解释器。
## 结构图
<img width=600 src="https://img.alicdn.com/imgextra/i4/O1CN019y6R201yinq7xjJEK_!!6000000006613-2-tps-1530-776.png">
Context 是其他上下文变量,AbstractExpression 是抽象语法表达式。
可以看到,TerminalExpression(终结符)与 NonterminalExpression(非终结符) 都继承于 AbstractExpression,终结符指的是没有后续展开的符号,非终结符相反,所以非终结符又指向了 AbstractExpression,如此递归。
## 代码例子
下面例子使用 typescript 编写。
假设我们要实现以下文法:
```text
sum ::= number + number
number ::= 1 | 2
```
表达一个最简单的加法文法,其中加法表达式 sum 和 number 都是非终结符,而 +、1、2 是终结符。这个例子只能做到 1 与 2 的加法,通过这个简单例子,了解一下解释器模式的精髓吧:
```typescript
// 抽象表达式
class AbstractExpression {
interpret (text: string) {}
}
// 终结符表达式
class TerminalExpression extends AbstractExpression {
constructor(values: string[]) {
this.values = values
}
interpret(value: string) {
// 值必须是其中之一
return this.values.includes(value)
}
}
// 非终结符表达式
class NonterminalExpression extends AbstractExpression {
constructor(left: TerminalExpression, right: TerminalExpression) {
this.left = left
this.right = right
}
interpret(value: string) {
if (value.indexOf("+") === -1) {
// 必须包含 + 号
return false
}
const splitValue = value.split('+')
return this.left.interpret(splitValue[0])
&& this.right.interpret(splitValue[1])
}
}
// 调用
const context = new Context()
const terminal = new TerminalExpression(["1", "2"])
const add = new AddExpression(terminal, terminal)
add.interpreter("1 + 1") // true
add.interpreter("1 + 2") // true
add.interpreter("1 + 3") // false
add.interpreter("2 - 1") // false
```
遇到非终结符则继续调用,只有终结符才能直接判断,原理很简单。
## 弊端
上面的例子是比较低效场景,因为当语法复杂后,类的数目会明显增多,难以维护,此时需要用一个通用语法解析器,了解更多可以看笔者之前的文章:[精读《手写 SQL 编译器 - 语法分析》](https://github.com/dt-fe/weekly/blob/v2/066.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%89%8B%E5%86%99%20SQL%20%E7%BC%96%E8%AF%91%E5%99%A8%20-%20%E8%AF%AD%E6%B3%95%E5%88%86%E6%9E%90%E3%80%8B.md) 系列。
## 总结
解释器是一种思维,将复杂语法解析抽象为一个个独立的终结符与非终结符各自判断,只要每个文法自己的判断做好了,剩下的工作就是组装文法。
这种将单个逻辑判断与文法组装解耦的做法,可以使逻辑判断与文法组装独立变换,使复杂语法解析转化为一个个具体的简单问题。
> 讨论地址是:[精读《设计模式 - Interpreter 解释器模式》· Issue #296 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/296)
**如果你想参与讨论,请 [点击这里](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 @@
# Iterator(迭代器模式)
Iterator(迭代器模式)属于行为型模式。
**意图:提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。**
这种设计模式要解决的根本问题是,聚合的种类有很多,比如对象、链表、数组、甚至自定义结构,但遍历这些结构时,不同结构的遍历方式又不同,所以我们必须了解每种结构的内部定义才能遍历。
比如数组我们可以利用 length + for 循环,对象我们可以 Object.keys,链表比较麻烦,需要内部暴露出元素的 `next` 以操作指向下一个元素。
迭代器模式可以做到用同一种 API 遍历任意类型聚合对象,且不用关心聚合对象的内部结构。
这种模式和 `Array.from` 有点像,但其实真正的迭代器在 JS 里是 `obj[Symbol.iterator]()`,也就是一个对象实现了 `[Symbol.interator]`,就认为是可遍历的。
## 举例子
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
迭代器的例子非常简单,我们平时工作中有大量使用到。
### generator
`generator` 天生为迭代器的 API
```typescript
function* func () {
yield 'a';
yield 'b';
return 'c';
}
var run = func();
run.next() // {value: "a", done: false}
run.next() // {value: "b", done: false}
run.next() // {value: "c", done: true}
```
我们无需关心 generator 内部是何种存储结构,只需要调用 `.next()`,并根据返回的 `done` 来判断是否遍历完即可。在 generator 的场景中,迭代器不仅用来遍历聚合,还用于执行代码。
### 数组迭代器
我们可以用迭代器的方式遍历数组:
```typescript
const arr = [1, 2, 3]
const run = arr[Symbol.iterator]()
run.next() // {value: 1, done: false}
run.next() // {value: 2, done: false}
run.next() // {value: 2, done: false}
run.next() // {value: undefined, done: true}
```
可能有人觉得这是画蛇添足,因为毕竟遍历数组用 for 循环更方便,但这就是设计模式与非设计模式思维的区别,重要的不是用熟悉简单的 API 快速满足需求,**设计模式关注的是如何统一、抽象、低耦合的编码**。
### Map 迭代器
Map 对象也可以用迭代器方式遍历:
```typescript
const citys = new Map([['北京', 1], ['上海', 2], ['杭州', 3]])
const run = citys.entries()
run.next() // {value: ['北京', 1], done: false}
run.next() // {value: ['上海', 2], done: false}
run.next() // {value: ['杭州', 3], done: false}
run.next() // {value: undefined, done: true}
```
## 意图解释
从上面的例子可以看出,虽然用迭代器遍历数组看上去比 for 循环麻烦一点,但当我们把所有聚合类型放到一起看时,可以发现只有迭代器的 API 是最统一的,是唯一一个不需要关心聚合类型就可以完成遍历的方案。
**意图:提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。**
再来看意图,就非常好理解了,我们无需关心 数组、generator、Map 内部是如何存储的,就可以进行遍历。实际上,深究 generator 内部的存储结构也没有意义,如果我们不用迭代器进行遍历,那么对于复杂结构的遍历成本是非常高的。
## 结构图
<img width=600 src="https://img.alicdn.com/imgextra/i1/O1CN01lYSkni1DgFh6V3P85_!!6000000000245-2-tps-1658-754.png">
- `Aggregate`: 聚合,需要定义创建迭代器的接口。比如前端规范的 `[Symbol.iterator]()`,或者这里定义的 `CreateIterator()`
- `Iterator`: 迭代器,定义了访问与遍历的 API。
迭代器的定义很简单,实现时要考虑的因素可不少,包括:
- 健壮性。即迭代过程中增加、删除元素后,还能正常遍历。或者遍历空聚合时也要能正常工作。
- 外部控制迭代还是内部。即类似 KOA 由插件调用 `next()` 控制迭代,还是由外层统一控制迭代。
- 如何定义遍历算法。即便对于对象这种简单场景,也存在深度优先和广度优先、冒泡与捕获这几种遍历顺序,迭代器可以提供选择或者拓展的方式,自定义遍历算法。
## 代码例子
下面例子使用 typescript 编写。
```typescript
// 定义聚合接口
interface Aggregate{
getIterator: () => Iterator
}
// 定义迭代器接口
interface Iterator {
// 指向下一个
next: () => void
}
// 定义一个聚合
class List implements Aggregate {
// 存储元素
public values: string[]
// 游标
public index: number
getIterator() {
return new ConcreteIterator(this);
}
}
// List 的迭代器
class ConcreteIterator implements Iterator {
constructor(list: List) {
this.list = list
}
next() {
return this.list.values[this.list.index] // 注意边界情况,这里就不展开
this.list.index++
}
}
```
## 弊端
如果你只是遍历数组,直接用 for 循环会比迭代器方便很多,没必要为了用设计模式而用设计模式。迭代器仅在以下情况可以考虑用于数组:
1. 这个数组比较特殊,是 N 维数组,需要一次性遍历完,那么可以用迭代器。
2. 同时遍历数组和其他类型的聚合,则不论数组还是其他聚合,都用相同的迭代器模式遍历最好。
## 总结
迭代器模式比较好理解,这里补充几个相关设计模式:
- 迭代器可以和组合模式配合,在组合结构内进行递归,这样一个迭代器就可以遍历完所有组合。
- 可以用工厂模式 + 多态模式,实例化不同的迭代器的实例。
- 迭代器模式还可以与备忘录模式配合使用,当我们要还原迭代器状态时,适合在迭代器内部使用备忘录模式进行状态存储。
> 讨论地址是:[精读《设计模式 - Iterator 迭代器模式》· Issue #298 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/298)
**如果你想参与讨论,请 [点击这里](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,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)