上一节,我们主要学习了几种提高复用性的设计模式。在本节中,学习方法的可扩展性以及如何更好地扩展方法。方法是组成程序的基本单元。基本单元的可扩展性是整个程序可扩展性的保证。顾名思义,可扩展性就是保证代码和程序能够更好的扩展。再好的程序员也会写出bug,再好的产品经理也会改需求。遇到需求变化,他总不能和产品硬拼。一个好的程序员需要随时准备好改变需求。问题杀产品经理,先想想是不是因为代码的可扩展性不够好,才这么难改需求。在改代码的时候,难免会发现之前的代码不够全面。在这个过程中写第一段代码的时候,稍微注意一下可扩展性,可以大大降低后面修改代码的难度。提高可扩展性的目的是什么?面对需求变更时,方便变更需求,降低代码修改难度。什么是好的可扩展性?当需求发生变化时,无需推翻重写之前的所有代码。由良好的可扩展性组成的程序就像一块橡皮泥。想改的时候只需要重新揉捏形状就可以了。代码修改不会引起大规模更改。很多人在改代码的时候改了一个bug导致了十个bug。按理说,修复一个问题,只需要添加解决问题的代码即可。让这个功能的代码加入,会造成大规模修改。易于添加新模块。从模块的角度来说,比如当一个产品加入一个新的模块时,我们整个模块的组合就像搭积木一样。很方便。1.提高可扩展性的设计模式1.1.更好的代码更改的设计模式它允许我们做出更好的更改。1.1.1.Adapter模式Adapter模式:顾名思义,适配器就是用来适配的。比如我的电脑只支持type-C耳机,而我只有圆孔耳机。我想用圆孔耳机插电脑。听音乐怎么样?第一个方案是买一个type-C耳机,但是买新的太贵了,所以第二个方案是买一个转接头,把圆孔耳机转成type-C耳机。这个适配器是一个适配器,一个适配器模式特定于被调用的接口的名称,这就产生了非通用性的问题。目的:通过编写适配器,而不是直接替换旧代码。应用场景:当接口不通用时,解决接口不通用的问题。比如我们要调用一个接口a,但实际上这个接口调用的是b,这两个接口不匹配。这个时候一定不能把a改成b。改了就相当于重写了原来的代码。通过编写适配器1.1.2。装饰者模式装饰者模式:装饰者模式针对的是方法本身的功能。当一个方法的功能不够时,就需要添加一个新的方法。函数,但是我们不能直接修改之前的方法,我们可以使用装饰器模式更优雅地扩展我们的方法。用途:无需重写方法的扩展方法适用场景:当需要扩展一个方法,但又不易修改该方法时。1.2.解耦方法和调用的设计模式1.2.1。Commandmode命令模式:命令模式是针对代码设计的,使用命令模式是在设计方法的时候,而不是在改变方法的时候。开始思考这种模式的目的:将实现和调用解耦,让双方互不干扰应用场景:当要调用的命令充满不确定性时,可以考虑使用命令模式。(功能比较简单的时候不要用,command层需要写很多代码,增加了复杂度)什么是implementation?实现是方法的功能。比如我们有一个方法a,方法a中的操作就是实现,调用就是直接调用a()。什么是解耦实现和调用?上面代码示例中定义方法和调用方法等价于:call>method,而command方式相当于在call和方法之间增加了一个命令层:call>commandlayer>method,也没有必要调用方法时直接调用,而是将命令输入命令层,命令层解析命令后调用具体方法:命令>命令层>方法。你不需要关心应该调用哪个方法,也不需要知道有哪些方法。你只需要关心你输入的命令。与之前直接调用>方法相比,之前的方式使用方法的人与方法本身直接相关,并且在中间添加命令层后,方法本身与需要使用的人解耦了方法。即使此时方法发生变化,不用说,命令也会随之发生变化。你只需要在命令层更改命令的解析即可。所以方法的改变不会影响命令。同样,当命令改变时,也不会影响方法。中间有一个命令层作为缓冲区。我们可以在命令层做一个部署和调度,这样两者的变化不会互相影响。我们的代码写法其实就相当于命令模式。我们不需要关心写好的代码最终是如何在计算机上以二进制执行的。我们只需要关心我们输入的代码。输入的代码就像一个命令,而解析器V8引擎相当于命令层,它负责将命令解析成计算机可以执行的二进制语言,而方法本身就是计算机执行的代码。为什么要把代码写成命令模式?因为编程方向多样化,可以编程任何效果,可以编程的效果很多,所以命令方式非常适合。二、基本结构2.1.adapter模式的基本结构要求:每次都要写console.log太麻烦,项目中要用log代替console.log。adapter模式使用起来很简单,就是在新接口中调用旧接口。适配器模式的应用场景是在接口不通用的情况下做一个适配器。解决这个需求,只需要在log函数中调用console.log即可。2.2.装饰者模式的基本结构要求:有一个别人写的模块a,里面有一个方法b。如果不能修改别人的模块,则扩展方法b。装饰器模式分为三个步骤:封装新方法、调用旧方法、添加扩展操作。于是我们新建一个自己的方法,调用里面的b方法,然后添加要扩展的函数,这样就可以在不修改原有对象的情况下进行扩展。b方法的功能,代码示例:2.3。命令模式的基本结构代码示例:上面的代码创建了一个匿名自执行函数,函数中有一个action,就是方法的实现,excute就是我们的命令层,通过这个匿名得到的命令自执行功能是命令层。调用action中的方法时,会在executecommand层解析命令,通过向command输入命令来调用action中的实现。用户无需关心具体要求。在动作中调用了哪个方法。命令模式有两个要素:一是行为action,二是命令执行层excute3.应用实例3.1.适配器模式示例3.1.1。框架变更需求:当前项目中使用了一个框架,A框架与jQuery非常相似。但是A框架对于刚进入公司的新人来说非常不友好。新人进入团队需要学习如何使用这个框架,所以现在需要换成jQuery。虽然这两个框架在时间上相似,但在方法上有一些差异。比如jQuery中的css调用是$.css(),而A框架中是A.c(),jQuery中的绑定事件是$.on(),A框架中是A.o(),这是问题,如果两个框架的方法名没有区别,直接把A赋值给jQuery就OK了。既然有不同的方法名,直接将jQuery赋值给A会导致老代码调用A.css()来解决这个问题。许多人一一处理这些方法。过程显然是重写旧代码。这是一个典型的适配器应用场景。我们只需要在不改动旧代码的情况下,写一个适配器,这样两个接口名称可以适配就好了。按照adapter模式的步骤,在新接口中调用旧接口即可,代码示例:3.1.2。参数适配要求:为了避免参数不匹配带来的问题,很多框架都有一个参数适配操作。在JavaScript中,健壮性非常重要。健壮性的基本保证是对参数类型进行判断,并赋予其默认值,比如通过typeof判断。这种判断对于简单的数据类型还好,但是如果参数是配置对象呢?比如Vue,newVue传入的时候,不是一个简单的参数,而是一个整体的配置对象,里面包含了很多内容,比如模板,数据,方法等等,而且里面肯定有一些内容这些内容是必需的。填写,比如模板,数据,这样一个对象如何保证别人使用它时传递的配置对象中的所有必填字段都是必填的?如果用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等,通过调用这些方法来触发数组的响应式样式。让数组方法触发响应怎么样?游与戏是这样做的:数组方法是native方法,从设计原则上是不能直接修改的,所以游与戏使用装饰器模式来扩展原生数组方法的功能,使其能够触发响应。代码示例:先把要修饰的方法名放到数组中,然后可以直接循环遍历数组生成方法,不用一个一个改,比如展开push、pop、shift等。获取之前的数组循环启动原型链,因为要修饰的方法都在原型链上,但是原型链上的方法不能直接修改,所以先复制一个循环数组,得到我们要修饰的方法名,然后放我们要装饰的方法在复制对象上重写,装饰器模式分三步,①重写新方法②调用旧方法③扩展新功能;这里新增的功能是调用dep.notify()来触发vue的响应,这个方法封装在vue的源码中,最后将重写的arrayMethods对象赋给了所有数组原型链上的数据vue,让data中的数组原型链上的push等方法具有触发响应的功能,并且不会影响到原生的数组和方法。前面两种设计模式引导我们更好的扩展方式,而命令模式引导我们更好的设计方式。3.3.命令模式示例3.3.1。绘图命令需求:很难封装一系列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.绘制随机数量的图片需求:做一个图库,图片的数量和排列顺序都是随机的。这是一个典型的不确定应用场景。很适合使用命令方式来封装。代码如下:先创建command模式的结构体,然后约定一个command,首先format是一个object,object中有一个imgArr数组,这个数组存放的是图片内容,还有一个排序方案field,假设使用type字段,normal是正序,reverse是反序,还有一个target属性,这个属性表示创建的图片应该插入到哪个元素中。命令约定好后,搭建实现层的架子。假设有一个create方法创建html结构,还有一个display方法显示。display方法接收到要创建的东西,通过create方法创建一个html结构插入到target中。create方法先不写,先看command层。命令层接收一个对象。对于对象格式的参数,我们最好先做。参数适配防止不必要的错误;然后调用action.display方法传入参数再补充create方法。该方法的作用是生成html字符串。可以使用类似于vue的模板引擎来完成字符串生成。这里简单介绍一下这种组织方式的好处是用户不需要知道调用哪个api,只需要输入命令就可以完成功能;对于代码的改动,假设命令发生了变化,他们只需要在命令解析层改变解析的方式就可以了,不会影响到实现层。同样,如果实现层发生变化,也不会影响命令层,只是改变了解析。3.4.总结当面临新旧模块接口api不匹配时,可以使用adapter方式进行api转换。当老方法不方便直接修改时,可以使用装饰器模式扩展功能,使用命令模式将实现与具体命令解耦,更容易扩展实现端和命令端
