[上一节]主要学习了提高复用性的几种设计模式,本节学习方法的可扩展性以及怎么更好的扩展方法。
方法是组成程序的基础单元,基础单元的可扩展性是整个程序的可扩展性保障,可扩展性顾名思义是保证代码、程序能够更好的进行扩展。再厉害的程序员都会写bug,再好的产品经理都会改需求,在遇到需求变更的时候不能总是跟产品硬怼,一个好的程序员需要随时做好改需求的准备,不要遇到问题就想着砍死产品经理,先思考是不是因为自己的代码可扩展性方面没做好,所以导致改需求才这么难。改代码的时候难免会发现之前的代码思考的不够全面,在这个过程中写第一份代码的时候稍微注意一下可扩展性,就可以极大的减少后面修改代码的难度。
提高可扩展性的目的?
- 面临需求更改时,方便更改需求
- 减少代码修改的难度
什么是好的可扩展?
- 当发生需求变更的时候,不需要把之前的所有代码推倒重写。好的可扩展性组成的程序就像一块橡皮泥,当你要改变的时候只需要重新捏一下形状即可。
- 代码修改不会引起大规模变动。很多人在改代码的时候改了一个bug引起十个bug,按理来说改一个问题只需要加一个解决问题的代码就行了,但因为前期代码设计的不好,导致要修改很多之前的代码才能让这一个功能的代码加上去,这样就会引起大规模修改。
- 方便加入新模块。从模块的角度讲,比如产品要加一个新模块的时候,我们整个模块之间的组合就像积木一样,加入一个新模块就像加入一个新积木,拼上去就完事了,这样加入整个模块就非常方便。
一、提高可扩展性的设计模式
1.1. 更好的更改代码的设计模式
这两个模式都是为了让我们更好的更改代码,它们更像是一种技巧,当我们的方法需要变更的时候能够让我们更好的进行变更。
1.1.1. 适配器模式
适配器模式:适配器顾名思义,就是用来做适配的,比如我的电脑只支持typeC的耳机,但是我现在只有圆孔的耳机,我要用圆孔的耳机插上电脑来听歌怎么办?第一种方案就是买个typeC的耳机,但是买新的太贵了不划算,所以第二种方案就是买个转接头,把圆孔的耳机转接成typeC的,这个转接头就是适配器,适配器模式针对的是调用的接口的名字产生了不通用的问题。
目的:通过写一个适配器,来代替直接替换老代码
应用场景:当接口不通用的时候解决接口不通用的问题。比如我们要调用一个a接口,但实际上这个接口名叫b,两个接口就不适配了,这时候肯定不能把a改成b,改了就相当于重写原代码了,通过写适配器,把a接口适配到b接口就不用去改写老代码了
1.1.2. 装饰者模式
装饰者模式:装饰者模式针对的是方法本身的作用,当一个方法的作用不够用了,需要添加新功能,但是又不能直接去修改之前的方法,使用装饰者模式就能更优雅的扩展我们的方法。
目的:不重写方法的扩展方法
应用场景:当一个方法需要扩展,但又不好去修改方法。
1.2. 解耦方法与调用的设计模式
1.2.1. 命令模式
命令模式:命令模式针对的是代码设计,命令模式的使用是在设计方法的时候,而不是更改方法的时候,设计之初就要开始考虑这个模式
目的:解耦实现和调用,让双方互不干扰
应用场景:调用的命令充满不确定性时,可以考虑使用命令模式。(功能比较单一的时候不要使用,命令层要写很多代码,增加复杂性)
什么是实现?实现就是方法的功能,比如我们有一个a方法,a方法里面的操作就是实现,而调用就是直接调用a()
什么是解耦实现和调用?像上面代码示例中定义a方法和调用a方法相当于:调用 > 方法,而命令模式相当于在调用和方法之间加上一层命令层:调用 > 命令层 > 方法,调用时不用直接去调用方法,而是通过输入命令输入到命令层,然后由命令层来解析命令再去调用具体方法:命令 > 命令层 > 方法
这样做的好处是:
- 对比于之前直接调用 > 方法,通过命令层来调用就不需要关心具体应该调用哪个方法,也不用去了解有哪些方法,只需要关心输入的命令就好
- 对比于之前直接调用 > 方法,之前的方式使用方法的人和方法本身是直接的关系,而在中间加入命令层之后,方法本身和要用方法的人就解耦了,这个时候方法就算发生变动,不用说命令也要跟着变动,只需要在命令层中改变一下对命令的解析即可,所以说方法的变动就不会影响到命令。同样的,当命令发生变化之后也不会影响到方法,中间都有一层命令层来作为缓冲,我们可以在命令层做一个调配和调度,这样它们双方发生变动都不会影响到彼此。
我们写代码其实就相当于命令模式,写好的代码最终在电脑上是怎么用二进制执行的不需要我们关心,只需要关心我们输入的代码就好,输入的代码就像命令,解析器V8引擎相当于命令层,它负责把命令解析成计算机能够执行的二进制语言,方法本身就是计算机去执行的代码。为什么要把写代码变成命令模式?因为编程的方向是多元化的,有可能编写成任何一个效果,可以编写很多效果,所以采用命令模式非常合适。
二、基本结构
2.1. 适配器模式的基本结构
需求:每次都写console.log太麻烦了,项目里要用log来代替console.log。
适配器模式使用非常简单,就是在新接口里面调用老接口,适配器模式的应用场景就是在接口不通用的时候做个适配器,解决这个需求只需要在log函数中调用console.log就可以了。
2.2. 装饰者模式的基本结构
需求:有一个他人写好的a模块,内部有一个方法b,不能修改他人模块的情况下,扩展b方法。
装饰者模式三步走:
- 封装新方法
- 调用老方法
- 加入扩展操作
所以我们新建一个自己的方法,在其内部调用b方法,然后加入要扩展的功能,这样就可以在不修改原对象的情况下扩展b方法的功能了,代码示例:
2.3. 命令模式的基本结构
代码示例:
上面代码创建一个匿名自执行函数,函数里面有一个action,它是方法的实现,excute就是我们的命令层,通过这个匿名自执行函数拿到的command就是命令层,要调用action里面的方法时,通过给command输入输入命令,这些命令就会在excute命令层进行解析来调用action里面的实现,使用者不需要关心具体要调用action里面哪个方法。
命令模式有两个要素:
- 一个是行为action
- 一个是命令执行层excute
三、应用示例
3.1. 适配器模式的示例
3.1.1. 框架的变更
需求:目前项目中使用A框架,A框架和jQuery非常类似,但是A框架对进公司的新人很不友好,新人进入团队还需要学一下这个框架的使用,所以现在要改成jQuery,这两个框架虽然时分类似,但存在少数几个方法不同。
比如在jQuery中css的调用是KaTeX parse error: Expected 'EOF', got ',' at position 7: .css(),̲而在A框架中是A.c(),在j….on(),而A框架中是A.o(),这就是问题所在,如果这两个框架中的方法名没有任何不一样的话,直接把A赋值为jQuery就OK了,现在存在方法名不一样,直接把jQuery赋值给A就会导致旧代码中调用A.css()报错
解决这个问题很多人的处理方式是一个个去找这些方法改掉,过程很明显就是重写老代码,这就是典型的适配器应用场景,我们只需要写一个适配器,不用改动老代码,让这两个接口名能够适配就好了。根据适配器模式的步骤,在新接口中调用老接口即可,代码示例:
3.1.2. 参数适配
需求:为了避免参数不适配产生问题,很多框架会有一个参数适配操作。
在JavaScript中,健壮性非常重要,健壮性的基础保障就是判断参数类型赋予默认值,比如通过typeof判断,这种判断对于简单的数据类型是好使的,但如果参数是一个配置对象怎么办?比如Vue,在new Vue时传入的不是一个简单的参数,而是一整个的配置对象,它里面包含了很多内容,如template、data、methods等等,这些内容里面肯定会有一些内容是必填的,比如template、data,像这样一个对象怎么去保障别人使用的时候传的配置对象里面该必填的都必填呢?
如果使用typeof判断只能判断这个配置参数是一个对象,但是配置参数里面的东西有没有必填判断不出来,对于这样的配置对象形式的参数,我们最好给它做一个参数适配,参数适配就可以理解为适配器模式的变更,当你遇到一些方法它接收的参数是一个配置对象的时候,一定要养成一个习惯,给这个配置对象做一个参数适配,怎么去保障它里面必传的参数都传了呢?很简单,在函数里面写一个默认的配置对象,在这个默认的配置对象中将必传的属性都写上,比如name属性必传、color属性必传,代码示例:
然后当参数传进来的时候先不急着操作,先做一下适配,循环这个参数,如果传入的参数自身有必传项就用自身的,否则就用默认的代替,这样就保障了必传项起码会有一个默认值,不会因此报错。
当你写的方法要接收的参数是一个配置对象时,就通过这种参数适配的方式去保障必传的参数都有值,这是一个好的习惯,在工作中一定要保持。
3.2. 装饰者模式的示例
3.2.1. 扩展已有的事件绑定
需求:项目要进行改造,需要给dom已有的事件上增加一些操作。
假设你进入一家新公司,接手了前同事的代码,他在dom上绑定了很多事件,比如删除按钮绑定了点击事件,点击就进行删除操作,你接手之后产品跟你说觉得之前这种点击就删除没有提示的方式不太友好,需要你在点击确定或者删除的同时给出一个提示,这时候你会怎么做?
很多人会这么做
- 不去找他之前的代码写在哪了,直接重写整个绑定事件
- 找到老代码,然后改一下
这两种方式都是错的,如果采用第一种方案,势必要把它之前的删除功能代码再写一遍,非常麻烦,如果采用第二种方案,去找老代码这个找的过程也很麻烦,最好的方式就是采用装饰者模式,装饰者模式是用来干嘛的?就是当你发现一个方法它的原功能不适用、要扩展,但你又不能直接去修改原方法的时候就可以派上用场了。
所以我们用装饰者模式来做这个事情,考虑到要做这个事情的按钮有很多,就不一个个装饰了,采用工厂模式的思维,直接封装一个装饰工厂,使用时告诉我你要装饰哪个dom,要扩展什么操作就可以了,代码示例:
上面代码中,首先出于健壮性考虑,先判断一下dom上面有没有绑定事件,有的话再装饰,没有就不管。根据装饰者模式三步走:
- 封装新方法,
- 调用老方法,
- 扩展新功能;
先给click事件赋值为新方法,提取出dom的老方法,然后在click事件的新方法中调用老方法,然后加入我们要扩展的操作。
在使用的时候比如说要装饰删除按钮,然后要扩展提示功能,删除之后打印删除成功
这样既不用去找老代码也不用去重新写整个事件绑定,只需要调用装饰工厂就好了,扩展起来的速度就快多了。
3.2.2. Vue的数组监听
需求:Vue中使用defineProperty可以监听对象,那么数组监听是怎么实现的?
vue响应式面临一个困境,整个vue2里面双向绑定是用defineProperty来实现的,而这个方法针对的是对象的某个属性,对于数组而言,实现双向绑定比较困难。你会发现,在vue2里面直接修改数组下标是没办法触发响应式的,所以vue重新封装了数组的方法,如push、replace、shift等等,通过调用这些方法来触发数组的响应式,怎么让数组的方法能够触发响应式呢?尤雨溪是这么做的:
数组的方法是原生方法,对于设计原则来讲,不能够直接进行修改,所以尤雨溪利用装饰者模式扩展的原生的数组方法功能,使其能够触发响应式。代码示例:
- 首先把要装饰的方法名放进数组,到时候直接循环数组生成方法就可以了,不用一个个的改动,例如将push、pop、shift等进行扩展
- 在循环开始前先获取一下数组的原型链,因为到时候要装饰的方法全都在原型链上,但是不能直接修改原型链上的方法,所以先拷贝一份
-
循环数组,拿到我们要装饰的方法名,然后把我们要装饰的方法重写于拷贝对象上,装饰者模式三步走,① 重写新方法 ②调用老方法 ③ 扩展新功能;这里的新功能就是调用dep.notify()来触发vue的响应式,这个方法封装在vue的源码里面
-
最后就是将重写的arrayMethods对象给到vue的data上面所有数组原型链上,这样data里面的数组原型链上的push等方法就有了触发响应式的功能,它也不会影响到原生的数组和方法。
前面两种设计模式都是指导我们更好的扩展方法,而命令模式是指导我们更好的设计方法。
3.3. 命令模式的示例
3.3.1. 绘图命令
需求:封装一系列的canvas绘图命令
canvas画图比较困难,用过canvas的人都知道,canvas提供了点、线等api,绘制图形需要一个个点连接,很麻烦,为了更方便的使用canvas画图,就有大牛对canvas进行封装,提供了一些画图形的api,这样比较方便。
假设我们自己封装一个canvas,提供画圆和画矩形两个api,如果不用命令模式,代码可能会这样写:
上面代码适用于固定的状态,比如只调用api画一个图形或者两个图形,但整个canvas的行为是不能够固定的,它可能会需要画n个图形,这就很符合命令模式调用命令充满不确定性的场景,使用命令模式改写如下:
代码中首先创建命令模式封装,定义好实现层,返回命令层,命令层接收具体的命令,然后跟使用者约定好传什么命令,比如传一个数组,数组中包含要画的图形,假如要画两个圆,数组格式为[{type: ‘drawCircle’, radius: 5, num: 2}]。命令层负责解析具体的命令,自动调用实现层里面的方法来完成功能。
而在使用者的角度,使用者只需要调用canvasCommand传入相应的命令即可完成功能,他不用去关心有哪些api,这样就实现的方法与调用的解耦。其实webpack就是一个封装的命令模式,要使用webpack的某个功能,不需要去了解webpack要调用哪个api,只需要在配置中配置一下要使用的功能就可以了。
3.3.2. 绘制随机数量图片
需求:做一个画廊,图片数量和排列顺序随机
这是一个典型的不确定性应用场景,使用命令模式来封装非常合适,代码如下:
首先创建命令模式的结构
然后约定一下命令,首先格式是一个对象,对象中有一个imgArr数组,这个数组存放图片内容,还有一个排序方案字段,假设用type字段,normal为正序,reverse为倒序,还有一个target属性,这个属性代表创建好的图片要插入到哪个元素里面
命令约定好之后搭建实现层的架子,假设有一个创建html结构的create方法,一个显示的方法display。display方法接收要创建的东西,通过create方法创建一个html结构并插入到target当中
create方法先不写,先看命令层,命令层接收的是一个对象,对于对象格式的参数我们最好先做一下参数适配防止不必要的错误;然后调用action.display方法传入参数
然后补充create方法,这个方法的作用是生成html字符串,字符串生成可以采用类似vue的模板引擎来完成,这里做个简单的示例
这种组织方式的好处在于:
- 对使用者而言不需要了解调用哪个api,只需要输入命令即可完成功能;
- 对于代码变更而言,假设命令变了只需要在命令解析层更改一下解析方式就好了,不会影响实现层,同理,如果实现层变了,也不会影响到命令层,只需要更改一下解析就好了。
3.4. 小结
- 当面临两个新老模块间接口api不匹配,可以用适配器模式来转化api
- 当老的方法不方便直接修改时,可以通过装饰者模式来扩展功能
- 使用命令模式解耦实现与具体命令,让实现端和命令端扩展的都更轻松