[上一节]主要学习了方法的可扩展性以及怎么更好的扩展方法,本节主要学习模块的可扩展性以及怎么更好的扩展模块。
我们可以把任何一个程序看成是模块+组织模块沟通,模块是组成程序的一个单元,各种各样的模块加上它们的沟通就组成了我们的程序,这个过程很像我们生活中开一家餐馆,我们需要厨师模块、服务员模块、点餐模块等等,这些模块它们之间互相沟通,点餐模块告诉服务员模块,服务员模块再告诉厨师模块,厨师模块做好了之后再告诉服务员模块,这样沟通方式就组成了餐馆的工作方式,对于程序也是一样。
怎么做模块?
这里提供一个设计模块的思路,思路并不是固定的,如果需要,可以按自己的想法去设计一下,这个思路是在你还不熟悉模块设计的时候提供一个指导思想。
- 首先我们拿到需求
- 然后分析完成需求需要哪些步骤,比如把一个大象放进冰箱需要哪些步骤
- 分析完成这些步骤需要什么样的模块
- 完成模块
- 组织模块沟通
这个思维再借用开餐馆的例子来讲,需求是开餐馆,开餐馆需要有人服务客人、需要做菜、需要知道客人点什么餐,要服务客人我们就得有服务员模块,要做菜就得有厨师模块,要知道客人点什么就需要点餐模块,然后我们完成这些模块,再安排这些模块之间怎么工作。
提高整体项目可扩展性的核心?
- 低耦合
- 良好的组织沟通方式
低耦合的模块是整个程序层面具有可扩展性的基础,只有当你的模块划分低耦合的时候你的项目才会具有非常良好的可扩展性。
模块划分好了还要组织它们之间的沟通,这是第二个方面,要良好的组织它们之间的沟通,一个好的项目代码就是划分低耦合的模块以及组织它们之间的沟通。
我们平常所说的架构就是在设计如何把项目分成低耦合的模块,并组织沟通。
一、提高可扩展性的设计模式
1.1. 应对需求变更的设计模式(1)
1.1.1. 观察者模式
目的:减少模块之间的耦合,来提高扩展性。比如有a、b两个模块,如果a模块直接跟b模块沟通,a => b这两个模块的耦合性比较高,相当于我跟你面对面交流,要分开的时候招呼都不打,很不礼貌,加入观察者模式之后,a => 观察者 => b,a模块通过观察者把消息给到b模块,相当于我跟你之间用微信在交流,微信就是观察者,这个时候我们要分开的话可以不回对方消息,这样就降低了耦合度。
应用场景:当两个模块直接沟通会增加它们的耦合度,不方便直接沟通时可以使用观察者模式。
典型的不方便直接沟通的场景是异步,比如我有一个请求发给后端了,那么怎么确保它能跟同步模块沟通到呢?再比如绑定了一个点击事件,怎么知道什么时候触发了点击事件呢?就像你不知道异步请求结果什么时候能回来一样,你也不知道什么时候会触发点击事件。假如这个时候a模块是异步,b模块是同步,同步模块b要跟a模块沟通肯定不太方便,套回调就会增加它们之间的耦合度,所以通过观察者模式就很合适,b模块先在观察者身上注册一个观察方法,到时候a模块异步任务完成了就通知观察者,再由观察者把消息传递给b模块,这样就解决了它们不方便直接沟通的问题。
还有一种场景是,比如有两个完全没想过要沟通的模块,你在写代码的时候完全没有想过有一天这两个模块要产生交流,随着产品发展它们之间要进行交流了,如果强行改它们的代码让它们交流先不说增加了多少工作量和复杂度,增加耦合度是不可避免的。通过观察者模式来做对两边的变动就会比较少,因为一开始没想过要沟通,也就意味着没有给彼此留沟通接口,突然产品说要沟通的话你就要为彼此再新增一个沟通接口,这样改动成本是比较大的,通过观察者模式就可以把改动成本降低。
1.1.2. 职责链模式
职责链模式与观察者模式相对,它更多的是用于组织一系列的同步模块。
目的:为了避免请求发送者与多个请求处理者耦合在一起,把模块组织成一个链条。比如开一个水果加工厂,我们把水果加工的环节组织成一个链条,先经过清洗模块,清洗完以后再把水果交给切水果模块,切水果模块处理完之后交给加工水果模块,像一条流水线一样把我们要做的事情一个一个的组织下去,然后在这个链条上面依次的完成任务、传递消息。
应用场景:把操作分割成一系列的模块,每个模块只处理自己的事情。
这样组织的好处在于把你的操作分割成一系列的模块,每个模块只处理自己的事情,这时候你要加入新东西只需要新增一个环节,改起来比较舒服。
用上面的水果加工厂举例,我们现有的流程是 清洗 => 切水果 => 加工水果,若突然审查严格了,水果清洗好之后要加入一个消毒的环节,如果我们的流程是一条职责链的话,只需要在链条中加入一个消毒就可以了,这样流程就是 清洗 => 消毒 => 切水果 => 加工水果,加入一个环节对前后的处理模块都没有影响。
如果没有组织成职责链,模块就会很混乱,每个模块既负责切水果又负责洗水果,这时候如果要加入消毒功能,就要对水果加工厂的每一个模块加入消毒的程序,工厂量就变得很大。
1.2. 应对需求变更的设计模式(2)
1.2.1. 访问者模式
目的:解耦数据结构与数据的操作。比如我有一系列的数据,以前是直接操作这个数据,数据 => 操作,加入访问者模式之后,数据 => 访问者 <= 操作,数据给到访问者,操作也给到访问者,然后在访问者里面通过给入的操作来操作数据。
应用场景:不希望数据结构与操作有关联的时候可以使用访问者模式。
什么是数据结构不希望与操作有关联?
假设有10组数据结构完全不同的数据和10种完全不同的操作,数据是多变的,数据结构是不稳定的,操作也是不稳定的,这种情况下我们不好定义该怎么操作,所以我们可以通过访问者,把数据和操作都给访问者,希望哪种操作和操作什么数据的时候都能够把具体的命令传给访问者。
访问者模式很少用,因为我们在写程序的时候对于数据我们往往追求结构稳定,如果程序里面的数据结构不稳定也就意味着这个程序在设计上就是有问题的,所以说访问者模式很少用,因为在开发项目的时候基本上都是直接操作数据,正常的代码里面不会出现这种数据结构不稳定的情况。
二、基本结构
2.1. 观察者模式的基本结构
观察者模式的核心在于定义一个观察者,这个观察者并不会说具体的绑定在某一个模块与某一个模块,它是一个全局通用的东西,你到时候有哪个模块需要注册观察就来这里注册,哪个模块要触发观察就来这里触发。一般适用于不方便直接沟通或者异步操作。
观察者基本的两个要素是regist和fire,有这两个要素就满足了观察者的基本结构,如果你想也可以加入remove等等操作,regist就相当于把你的监听注册到observe的message对象上,fire相当于你要触发哪个监听,直接调用一下。
2.2. 职责链模式的基本结构
职责链模式是把我们要做的事情组织成一系列的模块,然后依次的调用这些模块去传递消息。适用于不涉及到复杂异步的操作。
假设把我们要做的事情组织成mode1、mode2、mode3,这三个模块依次的处理_result,这就是职责链模式的结构。
2.3. 访问者模式的基本结构
访问者模式是定义一个访问者来接收数据和操作。适用于数据和操作不稳定的情况。
data变量是我们的数据,handler是我们的操作,vistor是我们的访问者,把数据和操作给到访问者,在访问者里面让操作去访问数据。通过访问者来代替操作直接访问对象,来减少数据和操作之间的耦合。
三、应用示例
3.1. 观察者模式的示例
3.1.1. 多人协作的问题
需求:假设A工程师写了首页模块,B工程师写了评论模块,现在要把评论展示在首页。
假设A工程师写的模块叫index,B工程师写的模块叫comment,这两个模块在设计的时候完全没有想过后面要产生关联,突然产品说想把热门评论展示在首页上,此时响起了忽然之间天昏地暗的BGM,这个时候就会有点麻烦,评论模块根本没有留出一个接口让首页去调用拿到评论的数据,首页也没有写一段代码来调用评论模块的方法获取数据,如果强行沟通就要把两个模块的负责人叫过来坐下聊聊怎么定义接口怎么调用。
通过观察者模式就能轻松的扩展功能,代码示例:
首先定义观察者,然后定义它的两个基本要素,一个是触发(fire),还有一个注册(regist),注册函数会接收两个参数,第一个是要注册的监听叫什么名字,第二个参数是这个监听的操作方法,函数内部注册一下这个监听;触发函数接收我们要触发的这个监听叫什么名字,然后调用一下。
有了这个观察者之后,A、B两个工程师不需要坐在一起沟通了,首页模块只需要触发一下需要的监听,评论模块只需要注册一下监听就好了,不需要再为彼此新增一个接口。代码示例:
代码仅做示例,使用观察者时需要new这个类。通过观察者模式去做,两个模块的沟通成本和耦合度就会变得比较低。
3.1.2. 转盘应用
需求:有一个转盘应用,每转一圈速度减慢。
我们先分析步骤:
- 初始化转盘HTML
- 选定好奖品(抽奖其实在点下抽奖的时候就已经选定好结果了,动画只是为了好看,程序执行不需要那么久)
- 让转盘转动 => 减慢 => 转动(循环直到停下)
然后分析模块:
- 初始化模块(初始化HTML)
- 结果选定模块(选定抽奖结果)
- 动画效果模块(动画)
- 转动控制模块(控制转盘转动和停止)
组织沟通:
初始化模块工作完之后,抽奖按钮点击时选定抽奖结果,然后转动控制模块通知动画模块开始转动,动画完成后告诉转动控制模块完成了,转动控制模块再通知动画模块下一圈转多快,怎么转,动画模块和转动模块是个循环的过程。
动画模块和转动模块的沟通存在一个问题,动画一般会用setInterval来做,它是异步的,异步模块不能直接跟同步模块沟通,转动控制模块是同步的,它不知道动画模块什么时候才能转完一圈,这就是典型的不方便直接沟通的场景,所以这两个模块之间的沟通可以使用观察者模式。
代码示例:
先把观察者代码拿过来
然后按照分析步骤,先写好各个模块
我们先来规定消息的结构,moveControll模块告诉mover模块要怎么转,也就是moveControll会调用mover模块并给它一个配置,这个配置就是转动的命令,我们约定一下配置的格式为一个对象,speed属性表示转动的速度(比如speed是50,表示每50毫秒转动一格奖品),time属性表示本次转动几格奖品(比如time是10,表示这一次转动就转10格奖品)
模块架子搭好以后再来独立实现每个模块。
先实现初始化模块,示例不写详细代码了,我们先定义一个dom数组,整个初始化模块要做的事情就是创建10个奖品DOM全都push进数组里面,最终这些DOM都放在页面中展示出来。
结果选定模块一般是请求后端接口问它抽中哪个奖品,这里简单用一个随机数代替,例如我们随机一个0-10的随机数,随机数加40让它有四圈的基础圈,多转几圈意思意思,然后我们把结果取整并return出去。
动画效果模块的实现首先我们要定义好转动动画,默认第一个奖品是选中的,转动的时候把选中效果加到第二个奖品上并移除第一个奖品的选中效果,依次循环。
所以我们定义一个初始下标变量,默认值为0,表示当前在第一个奖品,然后创建一个定时器,执行间隔就取config中的speed,定时器中判断当前奖品如果是第一个,就移除最后一个的选中效果,其它的按照顺序即可。
看到这段if-else代码里重复的部分我们是不是可以想到前面学到的一种设计模式来优化一下呢?
采用享元模式来优化代码,先提取不同的部分作为公共享元,这里不同的部分是它们要移除的内容不同。我们先在定时器外面创建一个变量removeNum,默认为最后一个,也就是下标为9的那一项,然后判断一下当前奖品是不是第一个,如果不是第一个就把removeNum赋值为nowIn减1,然后设置选中样式即可,这样就能减少重复代码。
以上代码完成了动画效果模块的主要功能,我们还需要一个转动完成停止的判断,判断如果nowIn等于传进来的config中要求转动的总次数(time)了就清除定时器停止转动。比如config.time等于10,当nowIn等于10的时候就表示转完了。转完以后还需要通知转动控制模块已经转完了,所以清除定时器以后还需要通过观察者触发一个事件来通知转动控制模块。
这里如果不采用观察者模式,异步模块跟同步模块沟通就会比较困难,通过观察者这里沟通起来就会比较舒服,代码结构也很清晰。
代码勘误:以上动画效果模块的代码示例在定时器中设置选中效果后要给nowIn++,截图的时候忘记改正了。
然后我们补充转动控制模块的代码:
- 首先拿到抽奖结果赋值给final变量;
- 转动的总圈数赋值给_circle变量;
- 然后创建变量_runCicle记录已经跑了几圈了,默认值为0;
- 创建stopNum变量,表示最后要停在哪个奖品,抽奖结果对10取余得到最终停在哪个奖品;
- 定义初始速度变量_speed,默认值为50;
- 调用mover函数让它第一次转动起来,传入转动圈数和初始速度;
- 然后在观察者上面注册一个finish的监听,用来监听动画效果模块完成;
- 在监听的回调中去操作通知动画模块下一圈怎么转,首先创建_time变量,初始值为0;
- 转完一圈速度要减慢,所以_speed += 50让速度变慢;
- 已经跑过的圈数要加加,所以让_runCircle++;
- 然后判断跑过的圈数_runCircle是不是小于基础圈_circle,如果是就说明动画模块还是要转10格,如果跑完了,就让_time停在最终的奖品那。
这样我们就能很好的将动画效果模块和转动控制模块的沟通组织起来,异步模块mover不知道什么时候能完成,直接沟通是很不方便的,通过观察者来解决就很轻松。
3.2. 职责链模式的示例
3.2.1. axios的拦截器
需求:设计一个axios拦截器
axios的拦截器非常简单,就是利用职责链模式来实现的。代码示例:
先创建axios类,类里面会有一个interceptors属性,这个属性中有请求拦截对象request和响应拦截对象response,这两个对象分别都是interceptorManner类的实例化
然后我们创建interceptorManner类,它有一个use方法,调用这个方法就是用来添加拦截的,use方法接收两个参数,一个拦截成功回调和一个拦截失败回调
我们要把调用use方法加入的拦截成功回调以及拦截失败回调都存入interceptorManner类的handler属性里面,这个属性是一个数组。
整个拦截器执行的过程就是不断的把你的拦截操作加入到数组里面来形成一条职责链,因为拦截的数量是不确定的,你可以添加一个拦截或者多个拦截,所以通过职责链模式来组织代码扩展起来就会比较轻松。
我们发请求的时候会调用request方法,所以我们给axios类的prototype上添加request方法,request方法需要把请求拦截器的链条、初始链条、响应拦截器的链条拼接在一起,然后依次执行这个链条上的每一个方法,并且让我们的配置在这个链条上进行传递。
所以我们定义一个chain变量来存初始链条,然后分别遍历请求拦截链条和响应拦截链条,将这两个链条分别放在chain的开头和结尾。
然后依次执行链条,因为链条中可能有同步和异步操作,所以我们可以借助promise来完成。
promise.then执行完会返回新的Promise对象,在循环中把每次返回的Promise对象再赋值给promise变量就能完成自动调用,这样整个链条就自动执行起来了,然后我们把最终的结果return出去。
整个axios的源码实现就是一个典型的拦截器操作,通过职责链模式来实现代码是不是非常清晰呢?
3.2.2. 利用职责链模式组织一个表单验证
需求:有一个表单,需要先前端校验,再后端校验
假设我们要验证一个input,首先我们要绑定input失去焦点的事件,然后拿到要验证的值,在没有想过用职责链模式来做的情况下我们可能会写两个方法,然后依次调用来进行验证,例如:
这样扩展新功能就会比较麻烦,我们换成职责链模式来实现,代码示例:
首先将前端验证和后端验证用数组来组织成一个链条,然后我们要依次执行链条中的方法,promise的方式上面已经实现过了,我们换成async的方式来实现
async函数中拿到要验证的值,然后循环链条通过await来调用链条中的方法,最终把结果return出去
如果这时候需求改了,需要再添加一个中台验证,我们只需要在职责链数组对应的位置添加一个方法就好了。
就像我们前面学到的水果加工厂的例子,如果要加入一个消毒环节,我们只需要加一个消毒的模块。职责链的好处在于它把我们要做的事情组织成一系列的模块,扩展起来就会方便很多。
上面代码你甚至可以把验证操作封装成一个类,然后留出一个接口,别人可以通过这个接口来添加验证操作,这样代码的扩展性就会变得更高。代码示例:
这样组织代码之后,如果要加入新操作,只需要调用add方法把你要加入的操作放进去就可以了。
3.3. 访问者模式的示例
3.3.1. 不同角色访问数据
需求:公司有很多的财务报表,财务关心支出和收入,老板关心盈利。
一个公司的财务报表有很多,财务报表的访问者也有很多,比如有财务有老板等等,可能财务有多个,老板也有多个(合资经营),他们都想看到自己想看到的内容,这样的情况下无论是数据还是访问者都是不稳定的,访问者模式很适合处理这样的情况。
代码示例:
首先定义财务报表类,这个类里面有收入、支出、利润属性
然后定义老板类,给老板类添加一个get方法用来获取老板需要的数据,老板关心利润,所以接收一个参数
定义财务类,给财务类添加一个get方法用来获取财务需要的数据,财务关心收入和支出,所以接收两个参数
然后我们定义访问者,访问者接收到时候要访问哪张财务报表和是谁在访问这两个参数
因为老板和财务需要的数据是不同的,未来可能还会有多个角色加入,所以我们创建一个策略工厂,把对应的角色需要的数据返回出去。
我们还要知道传进来的这个人是老板还是财务,可以通过construcotr属性来获取调用的构造函数叫什么名字,然后调用对应的角色方法即可
在使用的时候,假如我们有三个财务报表、两个老板、一个财务,老板1想看财务报表2,老板2想看财务报表1,直接调用访问者把对应的报表和老板传进去就可以了。
这样的代码组织下我们可以随时更换访问者以及要访问的财务报表,实现了数据和操作之间的解耦。
共同学习,写下你的评论
评论加载中...
作者其他优质文章