Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b7ab34978 | ||
|
|
2ca7132569 | ||
|
|
aac9aa34f8 | ||
|
|
ad8a645462 | ||
|
|
2dbe4d6368 | ||
|
|
d76253ef75 | ||
|
|
46b68f1933 | ||
|
|
8aab27bc3f | ||
|
|
22f118e1b7 | ||
|
|
922eb6dc59 | ||
|
|
20526273d3 | ||
|
|
f3222c646b | ||
|
|
9ae28c5e85 | ||
|
|
e72318adb4 | ||
|
|
2682dc0504 | ||
|
|
8b4611b47a | ||
|
|
25938c60a6 | ||
|
|
9920f0dbb7 | ||
|
|
90f7d89db7 | ||
|
|
6ddbdbbbb2 | ||
|
|
3c8c9fd5ef | ||
|
|
91a9bf82fd | ||
|
|
4603ac77d8 |
+1
-1
@@ -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
@@ -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),每周都有新的主题,每周五发布。
|
||||
|
||||
@@ -276,7 +276,7 @@ Hook 函数必须以 "use" 命名开头,因为这样才方便 eslint 做检查
|
||||
|
||||
为什么不能用 condition 包裹 useHook 语句,详情可以见 [官方文档](https://reactjs.org/docs/hooks-rules.html#explanation),这里简单介绍一下。
|
||||
|
||||
React Hooks 并不是通过 Proxy 或者 getters 实现的(具体可以看这篇文章 [React hooks: not magic, just arrays](https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e)),而是通过数组实现的,每次 `useState` 都会改变下标,如果 `useState` 被包裹在 condition 中,那每次执行的下标就可能对不上,导致 `useState` 导出的 `setter` 更新错数据。
|
||||
React Hooks 并不是通过 Proxy 或者 getters 实现的(具体可以看这篇文章 [React hooks: not magic, just arrays](https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e)),而是通过链表实现的,每次 `useState` 都会改变下标,如果 `useState` 被包裹在 condition 中,那每次执行的下标就可能对不上,导致 `useState` 导出的 `setter` 更新错数据。
|
||||
|
||||
虽然有 [eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) 插件保驾护航,但这第一次将 “约定优先” 理念引入了 React 框架中,带来了前所未有的**代码命名和顺序限制**(函数命名遭到官方限制,JS 自由主义者也许会暴跳如雷),但带来的便利也是前所未有的(没有比 React Hooks 更好的状态共享方案了,约定带来提效,自由的代价就是回到 renderProps or HOC,各团队可以自行评估)。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## 1 引言
|
||||
|
||||
上周的 [精读《React Hooks》](https://github.com/dt-fe/weekly/blob/master/79.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Hooks%E3%80%8B.md) 已经实现了对 React Hooks 的基本认知,也许你也看了 React Hooks 基本实现剖析(就是数组),但理解实现原理就可以用好了吗?学的是知识,而用的是技能,看别人的用法就像刷抖音一样(哇,饭还可以这样吃?),你总会有新的收获。
|
||||
上周的 [精读《React Hooks》](https://github.com/dt-fe/weekly/blob/master/79.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20Hooks%E3%80%8B.md) 已经实现了对 React Hooks 的基本认知,也许你也看了 React Hooks 基本实现剖析(单向链表),但理解实现原理就可以用好了吗?学的是知识,而用的是技能,看别人的用法就像刷抖音一样(哇,饭还可以这样吃?),你总会有新的收获。
|
||||
|
||||
这篇文章将这些知识实践起来,看看广大程序劳动人民是如何发掘 React Hooks 的潜力的(造什么轮子)。
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# Factory Method(工厂方法)
|
||||
|
||||
Factory Method(工厂方法)属于创建型模式,利用工厂方法创建对象实例而不是直接用 New 关键字实例化。
|
||||
|
||||
理解如何写出工厂方法很简单,但理解为什么要用工厂方法就需要动动脑子了。工厂方法看似简单的将 New 替换为一个函数,其实是体现了面向接口编程的思路,它创建的对象其实是一个符合通用接口的通用对象,这个对象的具体实现可以随意替换,以达到通用性目的。
|
||||
|
||||
**意图:定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到其子类。**
|
||||
|
||||
## 举例子
|
||||
|
||||
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
|
||||
|
||||
### 换灯泡
|
||||
|
||||
我自己在家换过灯泡,以前我家里灯坏掉的时候,我看着这个奇形怪状的灯管,心里想,这种灯泡和这个灯座应该是一体的,市场上估计很难买到适配我这个灯座的灯泡了。结果等我把灯泡拧下来,跑到门口的五金店去换的时候,店员随便给了我一个灯泡,我回去随便拧了一下居然就能用了。
|
||||
|
||||
我买这个灯泡的过程就用到了工厂模式,而正是得益于这种模式,让我可以方便在家门口就买到可以用的灯泡。
|
||||
|
||||
### 卡牌对战游戏
|
||||
|
||||
卡牌对战中,卡牌有一些基本属性,比如攻防、生命值,也符合一些通用约定,比如一回合出击一起等等,那么对于战斗系统来说,应该怎样实例化卡牌呢?如何批量操作卡牌,而不是通用功能也要拿到每个卡牌的实例才能调用?另外每个卡牌有特殊能力,这些特殊能力又应该如何拓展呢?
|
||||
|
||||
### 实现任意图形拖拽系统
|
||||
|
||||
一个可以被交互操作的图形,它可以用鼠标进行拉伸、旋转或者移动,不同图形实现这些操作可能并不相同,要存储的数据也不一样,这些数据应该独立于图形存储,我们的系统如果要对接任意多的图形,具备强大拓展能力,对象关系应该如何设计呢?
|
||||
|
||||
## 意图解释
|
||||
|
||||
在使用工厂方法之前,我们就要创建一个 **用于创建对象的接口**,这个接口具备通用性,**所以我们可以忽略不同的实现来做一些通用的事情**。
|
||||
|
||||
换灯泡的例子来说,我去门口五金店买灯泡,而不是拿到灯泡材料自己 New 一个出来,就是因为五金店这个 “工厂” 提供给我的灯泡符合国家接口标准,而我家里的灯座也符合这个标准,所以灯座不需要知道对接的灯泡是具体哪个实例,什么颜色,什么形状,这些都无所谓,只要灯泡符合国家标准接口,就可以对接上。
|
||||
|
||||
对卡牌对战的系统来说,**所有卡牌都应该实现同一种接口**,所以卡牌对战系统拿到的卡牌应该就是简单的 Card 类型,这种类型具备基本的卡片操作交互能力,系统就调用这些能力完成基本流程就好了,如果系统直接实例化具体的卡片,那不同的卡片类型会导致系统难以维护,卡片间操作也无法抽象化。
|
||||
|
||||
正式这种模式,使得我们可以在卡牌的具体实现上做一些特殊功能,比如修改卡片攻击时效果,修改卡牌销毁时效果。
|
||||
|
||||
对图形拖拽系统来说,用到了 “连接平行的类层次” 这个特性,所谓连接平行的类层次,就是指一个图形,与其对应的操作类是一个平行抽象类,而一个具体的图形与具体的操作类则是另一个平行关系,系统只要关注最抽象的 “通用图形类” 与 “通用操作类” 即可,操作时,底层可能是某个具体的 “圆类” 与 “圆操作类” 结合使用,具体的类有不同的实现,但都符合同一种接口,因此操作系统才可以把它们一视同仁,统一操作。
|
||||
|
||||
**意图:定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到其子类。**
|
||||
|
||||
所以接口是非常重要的,工厂方法第一句话就是 “定义一个用于创建对象的接口”,这个接口就是 `Creator`,让子类,也就是具体的创建类(`ConcreteCreator`)决定要实例化哪个类(`ConcreteProduct`)。
|
||||
|
||||
所谓使一个类的实例化延迟到其子类,是因为抽象类不知道要实例化哪个具体类,所以实例化动作只能由具体的子类去做,这样绕一圈的好处是,我们可以将任意多对象看作是同一类事物,做统一的处理,比如 **无论何种灯泡实例都满足通用的灯座接口**,**所有工厂实例化的卡牌都具备玩一局卡牌游戏的基本功能**,**任何图形与交互类都满足特定功能关系**,这种思想让生活和设计得到了大幅简化。
|
||||
|
||||
## 结构图
|
||||
|
||||
<img width=800 src="https://img.alicdn.com/tfs/TB1VjyZmsVl614jSZKPXXaGjpXa-1434-476.png">
|
||||
|
||||
`Creator` 就是工厂方法,`ConcreteCreator` 是实现了 `Creator` 的具体工厂方法,每一个具体工厂方法生产一个具体的产品 `ConcreteProduct`,每个具体的产品都实现通用产品的特性 `Product`。
|
||||
|
||||
## 代码例子
|
||||
|
||||
下面例子使用 typescript 编写。
|
||||
|
||||
```typescript
|
||||
// 产品接口
|
||||
interface Product {
|
||||
save: () => void;
|
||||
}
|
||||
|
||||
// 工厂接口
|
||||
interface Creator {
|
||||
createProduct: () => Product;
|
||||
}
|
||||
|
||||
// 具体产品
|
||||
class ConcreteProduct implements Product {
|
||||
save = () => {};
|
||||
}
|
||||
|
||||
// 具体工厂
|
||||
class ConcreteCreator implements Creator {
|
||||
createProduct = () => {
|
||||
return new ConcreteProduct();
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
创建一个 `Product` 的子类 `ConcreteCreator`,并返回一个实现了 `Product` 的具体实例 `ConcreteProduct`,这样我们就可以方便使用这个工厂了。
|
||||
|
||||
工厂方法并不是直接调用 `new ConcreteCreator().createProduct` 那么简单,这样体现不出任何抽象性,真正的场景是,在一个创建产品的流程中,我们只知道拿到的工厂是 `Creator`:
|
||||
|
||||
```typescript
|
||||
function main(anyCreator: Creator) {
|
||||
const product = anyCreator.createProduct()
|
||||
}
|
||||
```
|
||||
|
||||
在外面调用 `main` 函数时,实际传进去的是一个具体工厂,比如 `myCreator`,但关键是 `main` 函数不用关心到底是哪一个具体工厂,只要知道是个工厂就行了,具体对象创建过程交给了其子类。
|
||||
|
||||
**你也许也发现了,这就是抽象工厂中其中的一步,所以抽象工厂使用了工厂方法。**
|
||||
|
||||
## 弊端
|
||||
|
||||
工厂方法中,每创建一种具体的子类,就要写一个对应的 `ConcreteCreate`,这相对比较笨重,但有意思的是,如果将创建多个对象放到一个 `ConcreteCreate` 中,就变成了 **简单工厂模式**,新增产品要修改已有类不符合开闭模式,反而推荐写成本文说的这种模式。
|
||||
|
||||
彼之毒药吾之蜜糖,要知道没有一种设计模式解决所有问题,没有一种设计模式没有弊端,**而这个弊端不代表这个设计模式不好,一个弊端的出现可能是为了解决另一个痛点。** 要接受不完美的存在,这么多种设计模式就是对应了不同的业务场景,**为合适的场景选择一种能将优势发扬光大,以至于能掩盖弊端,就算进行了合理的架构设计**。
|
||||
|
||||
## 总结
|
||||
|
||||
工厂方法并不是简单把 New 的过程换成了函数,而是抽象出一套面向接口的设计模式:
|
||||
|
||||
<img width=800 src="https://img.alicdn.com/tfs/TB1WKH.Zoz1gK0jSZLeXXb9kVXa-1480-786.png">
|
||||
|
||||
你看,我要做灯泡,可以直接做具体的灯泡,也可以定一个灯泡接口,通过灯泡工厂拿到具体灯泡,灯泡工厂对待所有灯泡的只做流程都是一样的,不管是中世纪风灯泡,还是复古灯泡,还是普通白织灯,都是一模一样的制作流程,具体怎么做由具体的子类去实现,这样我们可以统一管理 “灯泡” 这一个通用概念,而忽略不同灯泡之间不太重要的差别,程序的可维护性得到了大幅提升。
|
||||
|
||||
> 讨论地址是:[精读《设计模式 - Factory Method 工厂方法》· Issue #274 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/274)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,121 @@
|
||||
# Prototype(原型模式)
|
||||
|
||||
Prototype(原型模式)属于创建型模式,既不是工厂也不是直接 New,而是以拷贝的方式创建对象。
|
||||
|
||||
**意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。**
|
||||
|
||||
## 举例子
|
||||
|
||||
如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。
|
||||
|
||||
### 做钥匙
|
||||
|
||||
很显然,为了房屋安全,要尽量做到一把钥匙只能开一扇门,每把钥匙结构都多多少少不一样,却又很相似,做钥匙的人按照你给的钥匙一模一样做一个新的,这属于什么模式呢?
|
||||
|
||||
### 两种状态表
|
||||
|
||||
当网站做不停机维护时,假设维护内容是给每个高级会员账户多打 100 元现金,现在需要改数据库表。已知:
|
||||
|
||||
1. 数据库表有几千万条数据,其中高级会员有几千位,为了方便调用已经缓存在中间层了,且数据库对应 ID 更新后对应缓存也会更新。
|
||||
2. 几千条数据修改语句执行完需要几分钟,这几分钟内无法接受用户数据不同步的问题。
|
||||
|
||||
一种常见的做法是,我们生成一份高级会员列表的拷贝,代替数据库缓存的结果,数据库只要读到对应会员 ID 就从拷贝列表中获取,数据表新增一列状态标志,操作完后这个拷贝移除,更新高级会员缓存。
|
||||
|
||||
但是如何生成高级会员列表拷贝呢?如果直接从几千万条用户数据中重新查询,会有较高的数据库查询成本。
|
||||
|
||||
### 模版组件
|
||||
|
||||
通用搭建系统中,我们可以将某个拖拽到页面的区块设置为 “模版”,这个模版可以作为一个新组件被重新拖拽到任意为止,实例化任意次。实际上,这是一种分段式复制粘贴,你会如何实现这个功能呢?
|
||||
|
||||
## 意图解释
|
||||
|
||||
解决上面问题的办法都很简单,就是基于已有对象进行复制即可,效率比 New 一个,或者工厂模式都要高。
|
||||
|
||||
**意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。**
|
||||
|
||||
所谓原型实例,就是被选为拷贝模版的那个对象,比如做钥匙例子中,你给老板的样板钥匙;两种状态表中的已有缓存高级会员列表;模版组件中选中的那个组件。然后,通过拷贝这些原型创建你想要的对象即可。
|
||||
|
||||
我们抽象思考一下,如果每把钥匙都遵循 `Prototype` 接口,提供了 `clone()` 方法以复制自己,那就可以快速复制任意一把钥匙。钥匙工厂可无法解决每把钥匙不一样的问题,我们要的就是和某个钥匙一模一样的副本,复制一份钥匙最简单。
|
||||
|
||||
高级会员状态表例子中,查询数据库的成本是高昂的,但如果仅仅复制已经查询好的列表,时间可以忽略不计,因此最经济的方案是直接复制,而不是通过工厂模式重新连接数据库并执行查询。
|
||||
|
||||
模版组件更是如此,我们根本没有定义那么多组件实例的基类,只要每个组件提供一个 `clone()` 函数,就可以立即复制任意组件实例,这无疑是最经济实惠的方案。
|
||||
|
||||
看到这里,你应该知道了,原型模式的精髓是对象要提供 `clone()` 方法,而这个 `clone()` 方法实现难度有高有低。
|
||||
|
||||
一般来说,原型模式的拷贝建议用深拷贝,毕竟新对象最好不要影响到旧对象,**但是在深拷贝性能问题较大的情况下,可以考虑深浅拷贝结合,也就是将在新对象中,不会修改的数据使用浅拷贝,可能被修改的数据使用深拷贝。**
|
||||
|
||||
## 结构图
|
||||
|
||||
<img width=800 src="https://img.alicdn.com/tfs/TB1roQlZWL7gK0jSZFBXXXZZpXa-1328-596.png">
|
||||
|
||||
`Client` 是发出指令的客户端,`Prototype` 是一个接口,描述了一个对象如何克隆自身,比如必须拥有 `clone()` 方法,而 `ConcretePrototype` 就是克隆具体的实现,不同对象有不同的实现来拷贝自身。
|
||||
|
||||
## 代码例子
|
||||
|
||||
下面例子使用 typescript 编写。
|
||||
|
||||
```typescript
|
||||
class Component implements Prototype {
|
||||
/**
|
||||
* 组件名
|
||||
*/
|
||||
private name: string
|
||||
/**
|
||||
* 组件版本
|
||||
*/
|
||||
private version: string
|
||||
|
||||
/**
|
||||
* 拷贝自身
|
||||
*/
|
||||
public clone = () => {
|
||||
// 构造函数省略了,大概就是传递 name 和 version
|
||||
return new Component(this.name, this.version)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我们可以看到,实现了 `Prototype` 接口的 `Component` 必须实现 `clone` 方法,这样任意组件在执行复制时,就可以直接调用 `clone` 函数,而不用关心每个组件不同的实现方式了。
|
||||
|
||||
从这就能看出,原型模式与 Factory 与 Builder 模式还是有类似之处的,在隐藏创建对象细节这一点上。
|
||||
|
||||
使用的时候,我们就可以这样创建一个新对象:
|
||||
|
||||
```typescript
|
||||
const newComponent = oldComponent.clone()
|
||||
```
|
||||
|
||||
这里有两个注意点:一般来说,**如果要二次修改生成的对象,不建议给 `clone` 函数加参数,因为这样会导致接口的不一致。** 我们可以为对象实例提供一些 `set` 函数进行二次修改。另外,`clone` 函数要考虑性能,就像前面说过的,可以考虑深浅拷贝结合的方式,同时要注意当对象存在引用关系甚至循环引用时,甚至不一定能实现拷贝函数。
|
||||
|
||||
## 弊端
|
||||
|
||||
每个设计模式必有弊端,但就像每一期都说的,有弊端不代表设计模式不好用,而是指在某种场景喜爱存在问题,我们只要规避这些场景,在合理的场景使用对应设计模式即可。
|
||||
|
||||
原型模式的弊端:
|
||||
|
||||
1. 每个类都要实现 `clone` 方法,对类的实现是有一定入侵的,要修改已有类时,违背了开闭原则。
|
||||
2. 当类又调用了其他对象时,如果要实现深拷贝,需要对应对象也实现 `clone` 方法,整体链路可能会特别长,实现起来比较麻烦。
|
||||
|
||||
## 总结
|
||||
|
||||
**原型模式一般与工厂模式搭配使用,一般工厂方法接收一个符合原型模式的实例,就可以调用它的 `clone` 函数创建返回新对象啦。** 代码大概是这样:
|
||||
|
||||
```typescript
|
||||
// buildComponentFactory 内部通过 targetComponent.clone() 创建对象,而不是 New 或者调用其他工厂函数。
|
||||
const newComponent = buildComponentFactory(new Component())
|
||||
```
|
||||
|
||||
最后来一张图快速理解原型模式:
|
||||
|
||||
<img width=600 src="https://img.alicdn.com/tfs/TB1hBIdm6MZ7e4jSZFOXXX7epXa-982-486.png">
|
||||
|
||||
> 讨论地址是:[精读《设计模式 - Prototype 原型模式》· Issue #277 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/277)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](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,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))
|
||||
Reference in New Issue
Block a user