开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第8天,点击查看活动详情
设计模式补充
除了之前介绍的三种设计模式,这里对于其他设计模式做一个补充吧
- 策略模式
定义一系列算法,把它们一个个封装起来,并且它们可以互相替换。
从javaScript
的角度来看,好比如定义一个对象,对象里面提供各个功能不一的方法,需要使用其中某个或者某几个方法的时候,直接调用对应方法即可,直接来讲就是相当于项目里面的utils文件,需要什么功能函数,就调用对应的函数即可。
需要调用者对每个功能方法都要了解,才能选择出符合当前需求的方法函数,跟设计模式'最少知识原则'是冲突的。
- 外观模式
为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用,感觉有点类似代理模式,将子系统的访问收归到高层接口代理。<js中应用场景不多>
- 代理模式
为一个对象提供一个访问品或者占位符,以便控制对它的访问
按功能分类又可以分为以下几种方式:
- 保护代理,控制不同权限的对象对目标对象的访问。(js可通过
proxy
实现) - 虚拟代理,把一些开销很大的操作,延迟到真正需要它的时候再执行,比如对象的创建、图片的预加载;
- 缓存代理,为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的值跟之前一致,那么直接返回缓存的运算结果;
- 防火墙代理,控制网络资源的访问
- 适配(包装)器模式
解决两个软件实体间的接口不兼容问题
,好比插头转换器
- 装饰者模式
在不改变自身对象的基础上,动态的给该对象添加一些额外的属性或方法,而不会影响从这个类中派生的其他对象。
即AOP编程思想,如对Function函数原型添加新方法
Function.prototype.before = ...
Function.prototype.after = ...
由于js的动态语言,可以直接给对象增加新方法.
- 中介者模式
解除对象与对象间的紧耦合关系。
通过新增中介对象,将原有的对象之间的关系独立出去,以中介者和对象之间的一对多关系取代了对象之间的网状多对多关系。需要注意的是,对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的,最后中介者对象自身往往就是一个难以维护的对象
实际项目中,模块或对象之间有一些依赖关系是很正常的,没必要为了堆砌设计模式和过度设计。关键就在于如何去衡量对象之间的耦合程度。一般来说,如果对象之间的复杂耦合确实导致调用和维护出现了困难,而且这些耦合度随项目的变化呈指数增长曲线,那就可以考虑用中介者模式重构代码了
- 状态模式
先看下面这段代码:
function Light() {
this.state = 'off';
}
Light.prototype.triggerLight = function () {
if (this.state === 'off') {
console.log('关灯');
this.state = 'on';
} else if (this.state === 'on') {
console.log('开灯');
this.state = 'off';
}
};
const light = new Light();
light.triggerLight();
light.triggerLight();
Light
的内部状态state
在triggerLight
调用的时候被切换,每次切换之后会被同步变更为最新的状态,目前来看这个状态处理没什么问题。
如果灯光不只有开关两种状态呢?我们知道现在很多灯光都支持三挡或者更多档位的显示,假如现在灯光有三种强度,分别对应level1, level2, level3,思考一下,是继续添加else if呢,还是有更好的方式解决状态维护问题呢。
如果基于之前的代码改造,那么我们需要扩展triggerLight
函数逻辑,不过这也意味着,每次新增或者修改light状态,都需要修改该函数,违反开放-封闭
原则;
function Light() {
this.state = 'level1';
}
Light.prototype.triggerLight = function () {
if (this.state === 'level1') {
console.log('level1');
this.state = 'level2';
} else if (this.state === 'level2') {
console.log('level2');
this.state = 'level3';
} else if (this.state === 'level3') {
console.log('level3');
this.state = 'level1';
}
};
const light = new Light();
light.triggerLight();
light.triggerLight();
light.triggerLight();
于是我们考虑将状态的维护和变更解耦,单独局部维护:
function LightLevel1(light) {
this.light = light;
}
LightLevel1.prototype.triggerLight = function () {
console.log('level1');
this.light.setState(this.light.level2State);
};
function LightLevel2(light) {
this.light = light;
}
LightLevel2.prototype.triggerLight = function () {
console.log('level2');
this.light.setState(this.light.level3State);
};
function LightLevel3(light) {
this.light = light;
}
LightLevel3.prototype.triggerLight = function () {
console.log('level3');
this.light.setState(this.light.level1State);
};
function Light() {
this.level1State = new LightLevel1(this);
this.level2State = new LightLevel2(this);
this.level3State = new LightLevel3(this);
this.curLight = this.level1State; // 初始化当前调用对象
}
Light.prototype.setState = function (state) {
this.curLight = state;
};
Light.prototype.triggerLight = function () {
this.curLight.triggerLight();
};
const light = new Light();
light.triggerLight(); // level1
light.triggerLight(); // level2
light.triggerLight(); // level3
于是如果我们想新增一个状态,只需要新增一个对应的类的状态以及状态切换的方法,而不需要变动Light.prototype.triggerLight
的原始方法。状态越多,维护越解耦,越独立。
- 享元模式
一种用于性能优化的模式,核心是运用共享技术来有效支持大量细粒度的对象
当代码里面有大量相似的对象,为了避免占用过高内存,那么可以利用享元模式。
- 组合模式
将对象组合成树形结构,以表示<部分-整体>的层次结构
,既然是树形结构,那么我们就可以动态的添加节点,在组合模式下,调用根节点方法,所有子节点的命令依次执行。
举个例子:
开车有这么一系列动作,我们可以动态的往后面添加我们想要的操作,但是对调用者(乘客)而言,他只需要司机(软件)执行开车这个命令就行。
- 命令模式
一个执行某些特定事情的指令
比如上述开车的例子,也是命令模式的一种,你只需要执行开车,内部的其他操作不需要关注。
将过程化的请求调用,封装在一个命令对象内,该对象可以被四处传递,调用命令的时候,调用者不需要关心内部具体实现,其实是回调函数的一个面向对象的替代品。
js可以用高阶函数非常方便的实现命令模式,该模式在js语言中是一种隐形的模式。
- 职责链模式
通俗来讲,就是将一个大方法,拆分为多个方法函数,将这些方法连成一个调用链。
还是拿开车的例子,如果一开始将开车的一系列动作都写到一个函数内,不如提取成各个独立的方法,然后依次调用。
- 模板方法模式
将一组行为或动作定义的共同点抽象成公共类,该类则对应的是一套有规则的模板,通过该模板,衍生出各种满足该共同点的子类实现,其中可以通过钩子函数或子类重写父类方法的方式,扩展子类的自定义形态。
还是以前面开车的例子为例,启动的步骤是固定的,那么你开汽车,开卡车,只要分离出驾驶的共同点形成一套模板,那么你就可以基于该模板,衍生出开其他类型车辆的子类方法。
其实你会发现,也许在平时的开发中,你已经在使用设计模式的思想在写更加健壮的代码,只是不知道它对应的是何种设计模式,所以从高质量的代码中,难免都会有设计模式的影子,尝试了解它,也能更灵活的运用它。
谈谈设计原则
- 单一职责原则(SRP)
一个对象(方法)只做一件事,降低了单个类或者对象的复杂度,方便代码复用,也有利于单元测试;
会增加编写代码的复杂度,基于职责把对象的分解的粒度更小的同时也增大了这些对象之间相互联系的难度。
- 最少知识原则(LKP)
一个模块或者对象可以将内部的数据或者实现细节隐藏起来,只暴露必要的接口API供外界访问。让对象之间的联系限制在最小的范围之内
要求在设计程序时,应当尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么就不要发生直接的相互联系。如果需要,通过创建第三者对象来转发
- 开放-封闭原则(OCP)
当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,但是不允许改动程序的源码
把系统中稳定不变的部分和容易变化的部分隔离开来,利用回调函数、放置钩子(hook)开放使用。