你必须要会的4种Web前端设计模式

在软件工程领域,设计模式是为了解决特定问题而产生的一些可复用的方法、模板。每一种设计模式都针对性解决某一类场景的问题。设计模式被认为是开发者解决通用问题的最佳实践。

通常我们学习的设计模式,大多数与面向对象的语言相关,比如Java。市面上大部分关于设计模式的书籍也是基于面向对象来进行讲解的。但对于大部分平常写JavaScript的开发者来说,面向对象的设计模式基本上是用不着的,如果不仔细思考,会经常错误或过度使用设计模式,不仅增加了软件开发成本,还加重了后期维护的负担。作为以Javascript为主语言的开发者,有必要认真审视设计模式与JavaScript语言之间的关系,以及如何将设计模式与JavaScript更紧密的结合起来,使得设计模式带来真正的收益。

本篇文章将会从以下三点来谈谈Web前端中的设计模式

  • 设计模式三问
  • 四种常用设计模式解析
  • 误区

设计模式三问

what:什么是设计模式

上半年由于全国乃至全世界届疫情的原因,口罩数量缺口巨大,因此口罩价格也疯涨了十几倍,这吸引了一大批老板加入口罩生产的行业。有的传统企业主将原本用于生产其他产品的车间改为生产口罩的,有的则是三五好友凑一笔钱临时建一个生产厂地。那么问题来了,为什么这些人都能在短时间内完成工厂的建设或改造呢?原因是,针对生产口罩这么一个场景,整个过程已经完全规范和自动化了,比如有的机器负责布料成型,有的负责多层压制,有的负责消毒。这就是模式,解决某一类问题的特定解决方法。

why:为什么会有设计模式?

“其实地上本没有路,走的人多了,也便成了路”。

在软件工程初期,需求相对较简单的时候,是没有设计模式这一说的。但随着时间推移,需求变得原来越复杂,遇到的问题也就越来越多。这些问题中很多都有相似性,慢慢的开发者们也根据不同的问题总结出了一套解决的方法论,并给它们取一个通俗易懂的名字,就是设计模式了,比如“工厂模式”、“观察者模式”等等。

why:为什么要用设计模式?

来打个比方,想必大家对下面三个小地图都比较熟悉吧?

想象一下,当地图上一个敌人的头像都看不见的时候,你方队伍一般情况下会怎么做?缩回高低还是抱团蹲人?无论选择哪种策略,都是在按照一种模式在进行,这些模式就是为了在地图上看不见敌方时能更好的应对而产生的解决方案。所以,用设计模式,是为了遇到相似的问题、场景时,能快速找到更优的方式解决。

how:怎么用设计模式

为了更方便、更快速的找到你想要的东西,往往你会把房间整理的井井有条。但整理是需要花时间和精力的,如果这个房间你根本就不常进去,里面的东西也不需要经常挪用,那其实并没有必要整理成这样。这些消耗的时间和精力,对应到软件工程中就是软件成本。在用设计模式时,必须要衡量其所带来的的收益,如果为了一段日后都大概率不会变动的代码采用过多的设计模式来编写,其实是得不偿失的。所以,在用设计模式前,我们需要慎重思考哪些功能是日后可能会变的,哪些不会。

 

四种常用设计模式解析

上图应该是大家在各类书籍中常见到的设计模式,但其中很大一部分在实际需求开发中使用频率不高,所以我根据个人经验从中摘取了四个比较常用的模式来进行讲解。

 

1.策略模式+组合模式

场景:

在一个设备管理系统中,当用户只有全部满足如下条件时,才能看到设备申请界面,有权限申请设备

  • A公司员工
  • B事业部
  • C技术栈
  • 部分经理

拿起键盘我们就开始写代码,会写成如下这样

这种写法有什么问题呢?

1.当条件越来越多时,checkAuth函数会越来越长,可读性以及可维护性相当的差。上面例子中,省略了具体判断中的处理代码,如果每个分支的处理代码都不少,那整个checkAuth函数最后会变得非常复杂。

2.每个判断其实都是独立的策略,假设有一天产品经理提了一个新需求,要做一个请假审核模块,也必须得是A公司+部门经理才能看到,那你就必须得从这个checkAuth函数中copy对应的处理分支过去了,这样项目中就又多了一块一模一样的代码。更进一步,后面产品经理要对判断公司这个分支的处理细节都要改进时,那你就不得不在多处进行修改,麻烦且不说,改漏了就会产生一个BUG。

对于模式较为单一的策略,其实只需要将策略的实现逻辑抽离即可,无需更复杂的写法

但是对于较为灵活的场景,需要策略之间的灵活组合来完成需求,就得使用策略模式+组合模式了。

比如,设备申请模块需要ABCD四个权限,而请假审核模块只需要BC权限,这时authStrategy就不再适用了,需要更灵活的写法。

通过这种动态添加策略的写法,只需要对不同场景配置不同的策略集,就能灵活的处理设备申请模块和请求审核模块的权限判断了。总结来看,当你负责的模块满足以下三个条件时,就建议用策略+组合模式

  • 各判断条件下的策略相互独立且可复用
  • 策略内部逻辑相对复杂
  • 策略需要灵活组合

 

代理模式

场景:

在系统中通过接口查询某日的统计数据,并期望能在前端对查询过的日期进行缓存,当下次请求时如果日期命中对应缓存,则直接返回缓存数据,不向后台发起请求。

直接写我们会写成下面这样

如果需求只是一处这样,那没什么问题,这样写最直接最简单。但我们得考虑到,系统中其他地方的请求,是否也是需要通过参数来缓存请求结果的?如果是,那么就需要改造上面的代码,用代理模式来让这种缓存逻辑更加通用。

上面代码通过通用的cacheRequest函数来代理真正的请求。cacheRequest函数利用闭包,将传入的请求函数与cacheDataMap锁定在一个作用域中,并返回一个增强后的请求函数。这样,其他需求如果也有类似的缓存需求,那就可以直接用cacheRequest函数去生成一个即可。

总结来看,当你负责的两个模块职责比较单一且可复用,并且两个模块间的存在整合、限制、过滤等关系时,可以尝试使用代理模式。

这里提一个思考问题,React中的HOC属于代理模式吗?

发布订阅模式

场景:

还是上面有提到过的设备申请模块,现在你要实现一个设备申请功能,需求要求申请成功后,会分别触发供应商订单生成,相关方消息通知,审核人审核三块的逻辑,调用逻辑如下图。

 

比较直接的代码写法是

这样写可能会有什么问题呢?当这三个模块不是你写的,而是分工到不同的开发者去完成时,你会发现麻烦有点大。

1.依赖模块的方法可能会改变,比如MessageCenter的开发者并没有遵照约定,把fetch方法改成了request,那执行的时候就报错了。

2.当你在写设备申请的onSubmitSuccess函数的时候,需求只依赖函数中这三个模块,但随着业务发展,你需要调用的模块在增加,这时你就不得不再次找到这个onSubmitSuccess函数往里面添加依赖项。

3.当你在写设备申请的onSubmitSuccess函数的时候,你并不知道要调用哪些模块,因为这些开发不是同时进行的。所以你无法再函数中预知要写哪些,必须得等到相关模块完成后再来填充调用代码。

基于这些问题的考虑,我们必须让程序更灵活一些。在写onSubmitSuccess函数的时候,我们只需要考虑结束时发通知出去告知申请成功,至于谁需要这个消息做什么并不需要关系。借助发布订阅模式,可以很轻松的实现这种效果。

总结来看,当你负责的模块,满足

  • 各模块相互独立
  • 存在一对多的依赖关系
  • 依赖模块不稳定、依赖关系不稳定
  • 各模块由不同的人员、团队开发

的条件时,建议使用发布订阅模式来进行开发。

思考一下,Redux是否使用了发布订阅模式呢?

 

责任链模式

场景:

假设在设备申请流程的需求中,需要经过三个逻辑模块

  1. 申请设备
  2. 选择收获地址
  3. 选择审核人

一般的写法是

当流程需求变得多样化时,这段代码就无法适应变化了,我们假设以下场景。

1.新流程申请软件license需要复用选择审核人逻辑。

在这种需求下,原有代码中审核人的代码无法复用,因为selectChecker函数依赖了处理具体提交业务的submitApply代码。要么copy修改,要么重新一份。

 

2.申请设备流程需要增加一个检查库存的环节。

在这种需求下,需要改动applyItDevice方法,增加检查库存逻辑才能满足。

如果我们使用责任链模式进行改造,那么这些场景都能更好的满足。

利用责任链模式,可以很轻松的复用每一个流程的逻辑,并且按需要组合成新的流程。

总结来看,当你负责的模块,满足

  • 你负责的是一个完整流程,或你只负责流程中的某个环节
  • 各环节可复用
  • 各环节有一定的执行顺序
  • 各环节可重组

时,建议使用责任链模式。

 

误区

“当我们有一把锤子,看什么都是钉子”

设计模式就好比一把锤子,当我们手握锤子的时候,不能看什么都是钉子,在写代码时,也不能处处都使用设计模式。使用设计模式本身是需要成本的,我们需要根据具体的场景来进行判断。正如开头所讲,识别变化与不变化的地方是核心关键点。

 

猜你喜欢

转载自blog.csdn.net/ForeverCjl/article/details/107516669