手记

设计模式—关于如何提高代码质量(远离脏乱差,人人见了都要夸)

[上一节]我们学习了模块层面提高可扩展性的几种设计模式,本节主要学习如何使用设计模式来提高代码质量。

提高代码质量的目的?

  • 高质量代码是一切性能的基础,无论是可扩展性还是复用性,必须建立在一个高质量代码的基础上,这样方便后续的一切操作
  • 方便他人阅读,能理解代码的目的

什么是代码质量?

  • 代码整洁就意味着我们的代码无论是命名、缩进以及各个方面比较符合规范,没有一些多余、累赘的代码
  • 结构比较规整,没有漫长的结构,比如没有很长的if else分支、很长的赋值语句
  • 阅读起来好理解,也就是说我们的命名要符合语义化,代码逻辑比较清晰

一、提高代码质量的设计模式

1.1. 优化代码结构的设计模式

1.1.1. 策略模式/状态模式

之所以把这两个模式放在一起是因为这两个模式非常相似,状态模式相当于加了状态管理的策略模式。

目的:优化if/else分支

应用场景:if/else分支过长会显得代码十分丑陋,这个时候就可以使用策略模式或状态模式来优化代码

1.1.2. 外观模式

目的:通过为多个复杂的子系统提供一个一致的接口

应用场景:当完成一个操作需要操作多个复杂的子系统时,把这些复杂的子接口统一成一个更高级的接口进行调用完成功能

1.2. 优化代码操作的设计模式

1.2.1. 迭代器模式

目的:我们在编程的时候不可避免的要对很多的数组、对象进行操作,迭代器模式的目的就是让我们能够在不访问对象或数组内部的情况下,能方便的遍历数据

应用场景:当我们要对某个对象或数组进行操作时,但是又不能直接去暴露它的内部,就可以使用迭代器模式

1.2.2. 备忘录模式

目的:记住之前的状态,方便随时回滚。

应用场景:通常用于缓存、状态回滚等操作,当系统状态多种多样时,为了方便我们回滚状态,我们把状态记录下来,然后随时能进行回滚

二、基本结构

2.1. 策略模式的基本结构

需求:假设要编写一个计算器,有加减乘除四种运算

在没有使用策略模式的情况下我们会一层一层的用if else判断是什么运算然后进行操作,这样代码非常不优雅,使用策略模式改写代码,我们可以把加减乘除运算作为策略对象中的属性,这时候我们就不需要再去用if else判断了,只需要调用策略对象上的对应类型的属性方法就可以了,这样就省去很多的if else分支。代码示例:

2.2. 状态模式的基本结构

状态模式和策略模式都是用来优化if-else分支的,状态模式可以看成是一个加了状态管理的策略模式,相当于把这些if-else的结构变成对象内部的一个状态,然后通过对象内部状态的改变,让整个对象具有不同的行为。代码示例:

示例代码新建了一个状态工厂,里面有一个状态对象stateObject,这个状态对象里面有策略模式里面的策略对象、有一个_status状态属性和一个run方法,状态工厂里面改变状态对象的状态,将改变状态之后的状态对象返回,使用的时候调用状态工厂的run方法来执行对应状态的行为。通过改变状态让对象展现出不同的行为,来代替我们通过if-else分支来改变行为,这就是状态模式的基本结构和思想。

2.3. 外观模式的基本结构

我们平时在组织方法和模块的时候可能会细化模块,将模块细化成多个接口,但是我们在给别人去使用的时候,要合为一个接口,就像去餐厅点餐,为了让用户有更高的可选性,可能会有很多的菜,但是为了方便一些选择困难的用户可能会提供一些套餐,像外观模式一样,把接口细化成一个个的小接口,最终的功能会由一个统一的完整的接口来完成功能调用,这就是外观模式的核心思想。代码示例:

2.4. 迭代器模式的基本结构

迭代器的思想是将数组或者对象变成迭代器对象,然后通过一个方法在不去遍历数组或对象的情况下有序的访问数组或对象的内部,这样可以简化循环,简化数据操作。代码示例:

代码中新建了一个迭代器类,这个迭代器类接收传进来的数组或对象,它的prototype下有一个dealEach方法,这个方法类似forEach,通过这个方法,不需要我们手动去进行for循环的情况下,来对数组或对象里面的每一项进行一个方法操作。

2.5. 备忘录模式的基本结构

备忘录模式会记录对象内部的状态,当我们有需要的时候可以根据已经记录的状态回滚到之前的状态或者方便对象使用。代码示例:

代码中创建了一个备忘录函数(Memento),备忘录函数内有一个缓存对象(cache)来缓存一些状态,然后返回一个function,这个function可以访问到缓存对象,在这个function内部判断有没有这个缓存,如果有就进行有缓存的操作,没有就进行没缓存的操作。备忘录模式说白了就是利用缓存对象来记录某个状态,然后根据这个状态的有无来做对应的事情。

三、应用示例

3.1. 策略模式/状态模式的示例

3.1.1. 动态的内容

需求:项目中有一个动态的内容,根据用户权限的不同显示不同的内容。

假设我们有三块要展示的内容,在请求到当前用户权限之后,如果是boss就展现全部内容,如果是经理就展现第一块和第二块内容,如果是普通职员就展示第三块。

先看一个没有使用状态模式的代码示例

上面这段代码存在的问题是有很多if-else分支,目前虽然只有三个角色,但随着发展可能会出现很多种角色,if-else会越来越多,所以我们使用状态模式来优化一下这段代码:

首先创建一个状态类showControll,给类添加一个状态属性status和策略对象power,然后将不同的角色放入状态对象。

设置了状态对象跟状态之后,我们需要一个把状态展示出来的方法,所以我们在showControll的prototype上添加一个show方法,通过这个方法来改变状态

代码写好之后我们使用的时候只需要new showControll类然后调用它的show方法就可以根据当前用户的权限来显示不同内容了。

对比之前的代码,首先第一点好处是if-else分支少了,代码看起来更加优雅,第二点好处在于如果要扩展更多的角色,我们只需要去扩展策略对象power里面的策略就可以了,写法更加方便、好看,这就是状态模式给我们带来的优化。

3.1.2. 复合运动

需求:有一个小球,可以控制它左右移动或者上下等方式移动

先看一个反面代码示例:

上面示例代码先新建四个移动方法,然后调用mover方法进行移动,如果这个方法只接收一个方向移动还好,判断参数只有一个朝着指定的方向移动就可以,但如果它变成一个复合运动,这里接收两个参数比如左上左下或者右上右下,这时候就要判断很多种情况了,前后左右四种方式两两组合将会产生多种不同的情况,如果用if-else分支来做,就会特别累。我们用状态模式来改造一下:

状态模式首先要把if-else判断变成对象里面的状态,先新建一个类,考虑到这里可能有左上移动左下移动等等,所以我们把状态status变成数组而不是一个字符串,如果只有一个左移状态,这个数组中就只放一个左移,如果是左下移,数组里面就会有一个左移和下移

然后新建一个策略对象,分别定义不同移动方向调用不同的方法

再给mover类的prototype添加一个运行的方法,这个方法接收到底要怎么移动,所以我们将argument变成数组并赋值给status,这样status就储存了运动状态,我们只需要去循环这个状态数组,从策略工厂中去找对应的状态执行即可

使用的时候我们只需要new mover类,调用它的run方法传入移动的方向就可以了,比如让它左上移

对比前后两种代码,可以明显的发现无论是在编写上还是在代码的简洁程度上都有了一个质的提升,把判断变成对象上的状态,根据状态去执行行为,而不是依次的if-else判断去执行行为,这就是状态模式来解决复合运动这样一些复杂if-else分支做的一系列优化。

3.2. 外观模式的示例

3.2.1. 插件封装

需求:插件基本上都会给最终使用提供一个高级接口

也就是说它会有很多子模块子接口,但是在最终使用插件的时候只需要调用一个高级的接口就可以了。

假设我们有一个选项卡插件,它拥有很多子系统,比如有一个初始化HTML的系统、改变选项卡的系统、事件绑定系统等,代码示例:

这些子系统都有很多自己的子接口,别人要去使用这个选项卡插件直接生成选项卡并且让选项卡生效的时候,就需要给他提供一个统一的接口。假设这个接口名为init,我们在prototype上添加init方法,这个方法接收一个config也就是配置参数,我们在init方法中去调用子系统的接口

最后在使用的时候就无需关心它有哪些子系统,应该调用哪个子系统,只需要统一调用init方法来完成整体的功能就可以了。外观模式更像是一种指导思想,在项目中我们避免不了对一些底层的支持,比如开发插件和库,这个库和插件在编写的时候要注意划分它的子模块,根据接口隔离原则去把接口和模块划分的更细一点,但是当别人使用我们插件的时候我们要提供给他一个更高级、统一的接口,让他能够调用一个接口来完成整体的功能。

3.2.2. 封装方法

需求:在兼容性要求还是非常严格的时代,我们通常需要能力检测,比如dom事件绑定有dom一级、二级,在那个时代经常要检测浏览器支持哪一级然后再采用对应的方式去进行事件绑定,当时会采用封装成方法的思想,把这些检测封装成统一的接口方法来进行事件绑定操作。

比如会有dom二级dom.addEventListener()、还有dom.attchEvent()、还有dom.onclick这样一些绑定操作,代码示例:

面对这样一些绑定操作我们通常都要去检测浏览器支持哪个然后就用哪个,在当时就会把这种操作封装成方法,比如封装一个addEvent方法,它需要接收dom(绑定的元素)、type(事件的类型)、fn(事件的回调)三个参数,然后对浏览器进行能力检测,判断浏览器的支持状态去绑定事件

封装好之后再绑定事件时不需要再去自己写一遍能力检测了,只需要通过外观模式统一的包装一个接口,然后调用这个接口就可以完成功能了,这就是外观模式的思想。

3.3. 迭代器模式的示例

3.3.1. 构建一个自己的forEach

需求:forEach方法其实是一个典型的迭代器方法,构建一个forEach方法,让它能循环数组,也能循环对象

首先创建一个迭代器类,这个迭代器类接收一个数据,它可能是数组,也可能是对象,将接收的数据挂到data属性上

然后给Iterator类的prototype添加一个dealEach方法,它接收一个回调函数,我们先判断一下data是不是一个数组,如果是数组就进行for循环,循环体内调用fn,把每次循环的内容和下标给出去即可;如果是对象就进行for in循环,把value和key传给fn即可

这样就构建了一个简单的forEach循环,我们就可以在不用进行手动for循环数组或对象的情况下拿到每一项的内容,前面说了forEach就是一个典型的迭代器模式的代表,从这里可以看出迭代器模式说白了就是给我们提供一个方法,让我们能够不用自己去手动遍历就能对数组和对象进行统一的操作。

3.3.2. 给你的项目数据添加迭代器

需求:项目中会经常对后端数据进行遍历操作,封装一个迭代器,使遍历更加方便

假设我们有这么一个数据

项目里面肯定避免不了要找出数组里面有哪些对象它的num等于2或者等于1,检查一系列的对象中有哪个对象的某一个属性满足某个条件这种操作经常会有,我们可以把这种操作封装成迭代器,代码示例:

首先创建迭代器工厂,因为迭代器肯定要经常去创建迭代器对象来进行操作,所以用一个工厂模式来封装,这个工厂接收一个参数data,然后在工厂内部新建迭代器对象,把接收的data放在迭代器的属性上

然后给迭代器新增一个方法,让它能够检查出data里面符合要求的对象。假设我们添加一个getHasSomeNum方法,它接收两个参数,第一个是handler,第二个是num,handler可以是一个方法,也可以是一个属性名,如果是一个方法我们就以这个方法去检查data,如果是一个属性名就用这个属性名去检查对象的属性名。

函数体内新增一个数组变量_arr,用来储存符合条件的对象,然后新建一个handleFn,判断传入的handler是不是一个方法,如果是就把它赋值给handleFn,这是自定义的检查方式;如果不是就把handleFn赋值为一个function,让这个function根据属性名去检查对象,这是默认检查的方式。判断对象的handler是不是等于num,如果是就返回这个对象,这段代码其实就是一个享元模式。

然后循环data,调用handleFn传入data的每一项,并把返回值赋值给_result变量,判断_result如果不是undefined(item传进去不符合条件就会返回undefined),就把_result存入_arr中;最后将_arr返回,这样就可以调用getHasSomeNum方法直接来检查data了。

使用时调用迭代器工厂传入data,调用getHasSomeNum方法传入属性名和需要检查的值即可筛选出num为1的对象。

如果需要自定义检查,比如说要检查它的值减去1等于2的对象,调用getHasSomeNum时传入function然后再自己判断即可,代码示例:

通过把for循环封装成方法,让我们在处理繁杂的数据检查之中能够不用每次都去手动遍历它,这就是迭代器模式的思想。

3.4. 备忘录模式的示例

3.4.1. 文章页缓存

需求:项目中有一个文章页,现在要对它进行优化,如果上一篇已经读取过了,则不发起请求,否则请求文章数据

代码示例:

代码中创建一个pager函数,函数内部新建了一个cache对象用来缓存文章数据,return一个function让它能够读取到缓存数据,这个方法接收一个pageName,也就是当前文章页的名字,通过判断去检查缓存对象中有没有这个pageName,如果有就直接返回数据,如果没有再发起请求并将数据以键值对的形式存入cache对象。

这里的代码就是借助备忘录模式来完成文章缓存的思路,这段代码并不能完整的实现一个文章页缓存,在真实开发场景中还需要做一些别的事情,但这是一种去缓存页面、缓存内容的思路,通过一个缓存对象以键值对的形式缓存内容,然后只需要去检查键名,如果不存在就进行获取操作并将获取到的内容存入缓存对象;如果存在就直接返回内容,这就是备忘录模式的核心思想。

3.4.2. 前进后退功能

需求:开发一个可移动的div,拥有前进后退回滚到之前位置的功能

思路很简单,首先创建一个moveDiv函数,函数内部创建一个stateList用来缓存之前的状态,将之前的状态以一串数据的形式记录在数组当中,然后创建一个指针指向当前状态,初始为0

每次去移动div的时候需要一个方法,所以在prototype下新增一个move方法,这个方法接收两个参数,一个是type(移动的方向),一个是num(移动的数值),我们假设有一个changeDiv是用来移动div的,在move方法体内调用changeDiv传入type和num,每次div位置改变之后将状态push进stateList,所以我们push一个对象,这个对象记录type和num,还要将当前指针(nowState)的状态改变,让它指向stateList的最后一位。

上面代码写完以后再去进行前进后退就非常简单了,只需要根据状态指针去拿上一位或下一位在stateList里面的状态,然后再次调用changeDiv方法去移动div就可以了。

我们创建一个前进方法,方法体内新建一个变量_state,判断当前指针小于stateList的总长度,也就是说当前指针不在最后一位,代表它可以前进,然后把当前指针加加,取出当前状态也就是stateList里面对应的nowState项,并把取出来的状态赋值给_state变量,然后调用changeDiv把当前状态传入改变div的位置

对应的后退也是同理,只需要去改变指针的位置,然后取出指针对应的状态,再次调用changeDiv方法去改变div的位置就可以了。这是大多数前进后退功能所对应的思想,无非就是把之前的状态和之后的状态储存在数组里面缓存起来,然后根据指针看它现在指向哪个状态,然后取出对应的状态即可。以上代码实现是备忘录模式一个很好的体现。

0人推荐
随时随地看视频
慕课网APP