上一节主要学习创建型的三种设计模式是怎么使用的。如何利用创建型设计模式来指导我们更好的封装代码更好的创建对象,本节主要学习怎样利用设计模式来提高代码复用性。
提高可复用性的目的?
为什么要提高可复用性?提高可复用性能带来什么好处?
- 遵循DRY原则:英文是dont repeat yourself,这个原则的意思是说要时刻记住去检查你代码之中是否有重复的代码,如果有,把它作为一个公共部分抽象出来。
- 减少代码量,节省开销:遵循了DRY原则之后我们的代码量就会减少,内存开销也会逐渐减少,因为没有重复的代码块了。
什么是好的复用?
- 在对象这个层面,一个可复用性高、设计的好的代码它的对象都是可以重复使用的,我们无须去频繁的修改对象来让它符合新的需求,它面向抽象而不面向具体。
- 对于进行同样操作的代码块不会重复写很多次。
- 高复用性的代码必然是建立在一个模块低耦合的基础之上的,尽量做到模块之间的功能单一,模块越单一,这个模块就能越容易被复用。
一、提高复用性的设计模式
1.1. 减少重复代码数量,高效复用代码的设计模式
桥接模式:它和DRY原则非常相似,都是把公共部分拿出来而不是耦合在现有的代码当中,通过把公共部分提取出来然后再桥接到使用的地方来减少代码耦合。
目的:通过桥接代替耦合
应用场景:减少模块之间的耦合
享元模式:通过观察相似的代码块和对象之间的共同点和不同点,区分出公有部分和私有部分,然后把公有部分作为一个享元抽取出来,从而减少对象或者代码块。
目的:减少对象/代码数量
应用场景:当代码中创建了大量类似对象和类似的代码块时就可以采用享元模式
1.2. 创建高可复用性代码的设计模式
模板方法模式:定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
目的:定义一系列操作的骨架,也就是说不要去具体实现操作,先把操作的骨架定下来,后面的类似操作就以这个骨架为基础,在骨架上进行扩展和具象化来实现后面具体的需求,简化后面类似操作的内容
应用场景:当你在项目中出现了很多类似的操作,它们可能基本的骨架是一样的,但是这些操作又具有自己的特点,这个时候我们可以使用模板方法模式
二、基本结构
2.1. 享元模式的基本结构
需求:有一百种不同文字的弹窗,每种弹窗行为相同,但是文字和样式不同
这种情况我们没必要去新建一百个弹窗,我们可以创建一个弹窗类,这个弹窗类保留相同的行为,然后把显示弹窗这个行为抽象为一个方法,这个方法再接收到底要显示什么样的弹窗;它们不同的地方是文字和样式,所以我们可以把它们不同的地方提取出来作为数组对象进行配置,然后我们只需要创建一个弹窗实例,通过循环数组调用弹窗实例的显示方法,传入每个弹窗的文字和样式即可。代码如下:
享元模式说白了就是当我们观察到对象或者代码块存在多个相似之处时,将它们相同的部分保留,不同的部分提取出来作为享元,这样我们就能用一个对象来代替多个对象。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。
2.2. 桥接模式的基本结构
需求:有3种形状,每种形状都要显示三种颜色
在这样的情况下如果我们不去注意可复用性,很有可能就会创建9种不同颜色的不同形状。我们可以观察一下:3个形状它们可能没有什么共同点,但是颜色有共同点,所以我们可以把它们的颜色提取出来作为公用方法,这些形状的类都依赖于这个方法,通过这个方法来展示颜色,这样我们只需要三个类即可完成需求。比如我们需要红色的圆形,只需要new一个圆形类告诉它需要红色即可。
桥接模式总结来说就是把这些重复的部分抽取出来作为一个公用的方法,然后再把它桥接回去,它有点类似建造者模式,先拆分然后再组合,但建造者模式的核心关注点是如何去构建对象(关注创建),而桥接模式关注的是如何通过一个桥接的方式来简化代码,提高代码可复用性(关注功能)。
2.3. 模板方法模式的基本结构
需求:编写导航组件,有的带消息提示,有的是竖的,有的是横的,后面可能还会新增其它类型
对于这种需求,我们没必要针对每一个导航组件都去新建一个类,可以写一个基础的组件类,具体的实现延迟到具体使用的时候,代码示例:
代码示例中,先新建基础类baseNav,在基础类中定下基本骨架,包括它的行为也是一样的处理,先写出它的基础行为,然后留出一个回调,等到具体决定要做消息提示型的还是其它类型的时候再通过回调来实现不同的地方,实现具体的操作和行为。
三、应用示例
3.1. 享元模式的示例
3.1.1. 文件上传
需求:项目中有一个文件上传的功能,该功能可以上传多个文件。
先看一个没有使用享元模式的代码示例:
上面代码中,在没有使用享元模式的情况下,我们要上传四个文件,就需要new四个uploader类分别指定文件,如果我们使用享元模式优化这段代码:
- 提取公有部分和私有部分:我们观察一下,对于这四个对象而言,它们的私有部分是要上传的文件和文件类型是不同的,而公有部分是每个对象都具有初始化、删除、上传的功能方法,享元模式就是把这四个对象的私有部分作为公共的享元提取出来,所以我们写一个数组,将这几个对象的私有部分储存起来
然后将公有部分都保留下来,所以说上面prototype上的init方法、delete方法、uploading方法都可以抄下来保留,而uploader类中的属性就可以删掉不需要了,我们把上传的文件、文件类型都作为参数传入uploading方法中
这么一改造之后,我们只需要实例化一次uploader,然后循环数组调用实例对象的uploading方法,传入文件和文件类型即可。
再次总结享元模式:享元模式到底怎么用?其实就是去观察代码中重复的部分,去看一下它们有哪些部分是私有的,哪些部分是公有的,然后把私有的部分提取出来作为公用的享元,享元的形式不一定非要是数组,它可以是对象也可以是方法等等。这种思想就像我们生活中一个很简单的道理,我们要试穿一百套衣服,没必要去请一百个模特,我们可以请一个模特分别试穿这一百套衣服。
3.1.2. jQuery中的extend
需求:extend方法,需要判断参数数量来进行不同的操作。
jQuery的extend方法如果给它一个参数,比如给个对象,就会把这个对象扩展到jQuery上面去,如果给它两个对象,它会将这两个对象合并之后返回出来,例如:
对于这样的操作,我们可能会想到先写一个extend方法,然后在方法内部通过argument对象去判断参数的数量,根据argument不同的长度去进行不同的操作,如:
这样的代码从原则上来说没有错误,但是可以发现这两段for in循环是极其相似的,DRY原则告诉我们要尽量减少重复的代码,所以我们使用享元模式来优化一下代码
- 提取公有部分和私有部分:这两段相似的代码它们的私有部分是接收拷贝的目标和拷贝的来源是不同的,我们先把它们的不同点提取为外部的公有享元,把接收拷贝的目标(extend第一个参数)作为外部的享元,通过target存起来,然后把拷贝数据的来源也就是被拷贝者变成source变量
它们公有的部分是for in循环,所以我们保留for in循环,for in循环应该循环我们的source,然后把sourse拷贝到target上面
这里依然要进行if else判断,判断如果参数长度等于1说明是给jQuery对象本身扩展属性,就改变一下我们的享元,将target变成this,然后将sourse变成argument第一项;如果参数长度不等于1的话就说明要将第二个参数对象拷贝到目标参数也就是第一个参数上,所以将target赋值为argument的第一项,将source变成argument第二项
(代码勘误:以上if判断应该是arguments.length == 1)
这样就成功的把两段for循环代码减少为一段,通过将不同的部分提取出来作为外部公用享元,然后只保留一段公用的操作,通过判断来改变外部的公用享元应该是哪个目标,有效的减少了重复代码,这就是享元模式的妙用。
3.2. 桥接模式的示例
3.2.1 创建不同的选中效果
需求:有一组菜单,上面每种选项都有不同的选中效果。
假设我们有三个菜单menu1、menu2、menu3,在常规情况下我们会先新建一个菜单类,它接收一个文字内容,把文字内容放到类属性上面,然后创建div,将div的内容设置为文字内容,如:
在使用的时候会创建menu1、menu2、menu3三个实例,将对应的内容传进去。
然后给它们绑定不同的事件实现不同的效果
在不使用桥接模式的情况下,我们要创建三个对象,分别对这三个对象绑定不同的事件去改变颜色,这样的代码重复率比较高,不符合高复用性原则。
使用桥接模式优化上面的代码:
- 提取公共部分:上面代码的公共部分是设置颜色和事件绑定这块,我们将这块提取出来然后桥接到源对象上:先在menuItem类中加入一个color属性,再接收一个color参数(它是我们要桥接的对象),然后将接收的color参数桥接到menuItem类的color属性上
然后创建menuColor类,它接收两个参数colorover(鼠标悬停的颜色)、colorout(鼠标移开的颜色)、分别把接收的两个参数放在menuColor类的属性上
因为dom已经放在menuItem类的属性上了,所以我们可以通过一个方法来把dom事件绑定上去,我们给menuItem类的prototype添加一个bind方法,它负责给dom绑定事件
绑定事件后我们不需要关心它到底要用哪个颜色,只需要直接拿出桥接过来的颜色对象(this.color)就可以了,所以我们在事件绑定的外层创建self变量赋值为this,因为事件中的this指向dom对象,所以只需要在onmouseover事件中将this.style.color赋值为菜单对象也就是self.color对象上的colorOver属性即可,onmouseout事件中同理。
这样再去实现需求时,我们只需要定义一个外部数据数组,这个外部数据定义了菜单的文字、悬停和移出的颜色,使用时循环这个数组创建menuItem实例,传入数据的内容,然后创建我们要桥接过去的颜色对象告诉它悬停和移除的颜色是什么,并将这个对象传入menuItem实例。最后调用bind方法将事件自动绑定上去。
通过对比可以发现我们的重复代码变得更少了,代码的复用性更好了,这就是桥接模式的作用。面临这样的问题时,我们通过把它们的变化部分和不变部分分离,再桥接到一起来应对后面多维度的变化,这就是桥接模式的目的和它带来的好处。
3.2.2. Express中创建get等方法
需求:express中有get、post等等方法,有七八个,如何方便快速的创建。
比如我们可以在express中通过app.get来设置一个get请求的中间件、通过app.post来设置一个post请求的中间件、可以通过app.delete来设置一个delete请求的中间件,我们要给这个实例app赋予这些方法
先看一个反面示例:
很多人会这样入手,创建一个express类,给它的prototype上添加get、post、delete方法
这是一个很典型的我们的系统可能会朝着多维度发展的例子,因为后期可能还要添加put等等请求方式,这些请求方式之间有非常相似的部分,这样就会重复的往原型链上添加方法,造成很多重复代码,如何去优化呢?
express源码中是这样做的,它首先会有一个methods数组,这个数组在源码中是通过一个第三方库然后require进来的,演示代码中我们就直接创建这个数组,将几个有代表性的请求方法名称写入数组,然后循环这个数组,在循环体内给express实例去注册这些方法
这样我们就不用重复写那么多方法了,关于具体的功能,它借助一个route对象将功能桥接过来,直接在注册的方法里面调用route[method]然后传入参数
route里面也是类似的一个循环,把源码拷过来
可以看到route里面的methods也是通过一个循环来注入的,循环内部根据我们是什么样的方法进行对应的操作,调用对应的中间件,这样就非常有效的减少了重复代码,提高了代码的可复用性
这种做法相当于直接调用get或者post,然后这个get和post利用桥接过来的route对象上的方法来完成功能,而route对象上的方法也是通过桥接来完成的,在route内部桥接的其实就是上面源码中一整段的function,在这个function中来判断方法是什么,调用对应方法的中间件
以上就是桥接模式在express中优化代码的应用。
3.3. 模板方法模式的示例
3.3.1. 编写一个弹窗组件
需求:项目有一系列弹窗,每个弹窗的行为、大小、文字都不相同。
假如我们写一个消息型弹窗、发送请求型的弹窗、删除操作型的弹窗
以常规的思维我们可能会新建一个消息型弹窗类、发送请求型的弹窗类、删除操作型的弹窗类,这种方式大可不必,虽然它们之间的行为、大小等不同,但是它们之间有非常多的共同点,比如说它们始终是一个弹窗、它们要弹出、点击确定或者取消它们都要消失等,这是它们的共同点,参照模板方法模式的思想,把他们的共同点先提取出一个基础模板类,基础模板类中定义word、size、dom属性,分别赋值为传入的word和size,dom初始化为null
然后定义基础的行为方法如显示弹窗的初始化,在方法体内创建div,把div的文字赋值为this.word,div的样式赋值为size的属性,这是每个弹窗都有的,它必须有一定的宽高和文字,再把this.dom赋值为这个div
然后定义基本操作的方法比如所有的弹窗点击取消之后都要隐藏,方法体内让div的display变为none
当然我们还有确定操作,但是我们没办法确定点击确定之后它要干什么,所以我们先定下确定操作的基础行为,点击确定首先也要隐藏弹窗
然后我们再定义一个特殊型弹窗,比如说定义一个ajax型的弹窗,点击确定之后要发送一个ajax请求。我们创建一个ajaxPop类,这个ajaxPop类继承基础类basePop(类的内部通过call调用其他类可以实现继承)
再扩展ajaxPop类的行为,将基础类的实例赋值给ajaxPop类的prototype即可继承基础类的行为
然后获取ajaxPop类之前的隐藏行为,再定义它新的隐藏行为方法,在新的方法里面先调用之前的隐藏行为方法,再去加入ajaxPop类特殊的隐藏行为,比如它点击取消之后要打印1(装饰者模式的体现)
确认型弹窗同理,先把之前的确认行为拿出来,然后再重写它的确认行为,在重写的方法体内调用之前的确认行为,再加入特殊的确认行为,比如要发送一个ajax请求。(装饰者模式的体现)
这样的方式下我们就可以非常好的以最少的代码量去创建和扩展不同类型的弹窗,这种方式和面向对象中的继承很类似,模板方法模式并不一定要通过继承来实现,有非常多的实现方式,它强调的并不是一定要有继承和模板,而是强调先定义后面需要进行的一系列不同维度的操作的基本行为,然后在这个基本行为的操作上给出一个扩展的空间,这就是模板方法模式的目的和作用。
3.3.2. 封装一个算法计算器
需求:现在我们有一系列自己的算法,但是这个算法常在不同的地方需要增加一些不同的操作。
比如它在a页面需要在计算之前让两个数相加,在b页面需要在计算之前让两个数相减,也就是说这个算法计算器有一系列的基本算法,这些基本算法又在不同的地方有不同的操作,这种需求就是非常典型的可以用模板方法模式来解决的例子。代码示例:
首先我们创建一个counter类
定义一下counter方法的基础计算也就是先把算法骨架搭出来,在prototype上添加count方法,这个count方法就是计算的时候调用的方法,它会接收计算的数据
我们在这个方法里面先去定义一个基本方法baseCount,假设baseCount先把num+4,再把num*4
这样我们定义好了基础计算,模板方法模式是先定义好基础,然后再去扩展,怎么去扩展基础计算?
所以我们需要留出两个扩展方法,对比上一个案例弹窗所演示的继承方式,这里我们可以用方法组合的方式来完成扩展。
先在counter类的prototype上定义两个方法,一个是基础算法计算之前要进行的计算,一个是基础算法计算之后要进行的计算
这两个方法类似axios的请求拦截器和响应拦截器,通过addBefore和addAfter添加一些方法,这些方法分别会在baseCount调用之前和之后来进行作用,所以我们需要在counter类中增加两个队列,用于存放基础算法计算之前添加的方法和之后添加的方法。
然后分别在addBefore和addAfter方法里面push对应的方法
有了扩展的方法之后我们需要改写一下计算方法,让它先执行beforeCounter队列,计算之后再执行afterCounter队列。
count方法内先定义变量_resultnum,初始等于传进来的num,然后定义一个_arr变量,先将_arr初始化为[baseCount],这个_arr就是我们要执行的方法队列
然后我们将beforeCounter拼接_arr再拼接上afterCounter,这样整个调用就形成了一个队列beforeCounter => baseCount => afterCounter
接下来我们只需要从头到尾依次执行这个队列,传入数据进行计算,再将结果给_resultnum,所有的方法都执行完后再将最终的计算结果_resultnum返回出去就可以了
代码写好之后,使用起来就非常简单了,假设在a页面基础计算之前先减减,基础计算之后再把数字乘以2,我们可以先新建一个实例化对象,调用它的addBefore方法,回调函数中进行减减操作再把计算结果返回;再调用addAfter方法,回调中将结果乘以2并返回;再去调用我们的基础计算方法count把需要计算的数字传入即可。
这样就保证了a页面既有我们的基础算法,又有它的特异性算法。
上面的弹窗例子中使用的是继承的方式,本例中使用组合的方式,把我们要扩展的东西变成方法组合到一起来达成扩展,无论使用哪种方式它们都是去找到相似的部分,然后定义基本的操作骨架,根据操作骨架再给出扩展接口,这就是模板方法模式的核心思想。再次强调无论什么设计模式,我们一定要先记住它的核心思想,而不是记住它的行为。
最后,我们来认识一下JavaScript的组合和继承
- 组合
① JavaScript最初没有专门的继承,所以最初的JavaScript推崇函数式编程,然后进行统一组合桥接到一起
② 桥接模式可以看成组合的一种体现,它把不同的东西组合到一起来产生一个新的对象或完整的功能,组合的好处是耦合低、复用方法方便、扩展方便,它的缺点就是要手动的去一个个组合,不能像继承一样自动完成
- 继承
① 在ES6出现class和extend之前,继承也可以实现,实现的方式多种多样,但都是各有弊端
② 模板方法模式可以看成继承的一种体现,继承的好处是可以自动获得父类的内容与接口,方便统一化,继承的缺点就是不方便扩展,如果用子类去继承父类,父类发生更改的时候子类也会发生更改,这样就非常不利于后续的扩展
通常来说组合大于继承,我们更推崇于使用组合来代替继承,能使用组合解决问题的话最好不要使用继承。
下一篇:设计模式—关于提高可扩展性(方法层面)的学习(更加从容的应对需求变更)
本文由博客一文多发平台 OpenWrite 发布!