2026-10-23 23:08:17
本文作者:不烧油的小火柴
原文链接:如何借助策略模式优化条件分支
前言在日常开发中笔者每天都要碰到条件分支判断的场景,而刚毕业没几年的我在业务里面写了大量的if/else判断,有时候自己看见了都觉得很不舒服。于是苦恼的我开始思考,该如何优化条件分支代码呢,近期也读了一些文章,它们的文章都不约而同地提到了策略模式,我对这个东西产生了好奇,于是我花了很多时间去学习它的思想,并且想着用更好地方式给大家分享,而不是一开头就讲述概念。
场景引入此时你皱着眉头正在修BUG,此时产品经理走了过来跟你说:小伙子,有个很简单的注册功能需要实现一下,该表单需要包括以下用户信息:
用户名
密码
手机号
在提交表单的时候需要校验格式:
用户名不能为空。
密码长度不能小于6位。
手机号格式正确。
你大概分析了一下,感觉也还算是简单,你的思路如下:
提交的时候先拿到表单实例。
对每个表单项进行校验
问题是这个校验怎么写呢,于是你开始实现你的代码。
实现if/else实现首先我们先把DOM结构给搭建好,先不考虑样式情况还是很简单的:
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"/><metahttp-equiv="X-UA-Compatible"content="IE=edge"/><metaname="viewport"content="width=device-width,initial-scale=1.0"/><title>design-pattern-strategy</title></head><body><formaction="/register"method="post"id="registerForm"><divclass="form-item">请输入用户名:<inputtype="text"name="userName"/></div><divclass="form-item">请输入密码:<inputtype="password"name="password"/></div><divclass="form-item">请输入手机号码:<inputtype="text"name="phoneNumber"/></div><button>提交</button></form></body></html>接着我们需要实现我们的JS逻辑,先拿到form实例,然后对每个字段进行校验:
constregisterForm=document.getElementById('registerForm')constvalidatorFn=(form)=>{if(form.userName.value===''){alert('用户名不能为空')returnfalse}if(form.password.value.length<6){alert('密码长度不能少于6位')returnfalse}if(!/(^1[3|5|8][0-9]{9}$)/.test(form.phoneNumber.value)){alert('手机号码格式不正确')returnfalse}returntrue}registerForm.onsubmit=function(e){constisValid=validatorFn(this)if(!isValid){returnfalse}alert('successsubmit')e.preventDefault()}实现效果如下:
你开开心心地提交了代码,然后开始“摸鱼”~
策略模式实现此时你的另外一个同事又接到需求:
用户名只能是字母和数字。
密码要改为不少于8位,最多100位。
增加邮箱验证。
它找到了validatorFn这个方法,看了你的if/else然后把规则加了进去,这时候这个验证函数越来越大...
而之后你发现这个规则不止注册模块用到,想复用这块代码时,你发现当初为啥要写这么多判断,无奈的你CV了一模一样的代码过去,最终这个代码成为了别人眼里的shit...
接下来的一个月时间里你开始修炼设计模式这门“内功”,发现策略模式可以很好地优化这部分逻辑,虽然之前的代码确实写得不好,但好在你对代码还有追求,于是你花了很长时间去优化这段代码,你明确了两个要求:
让规则可配置化,使用者不必关心具体内部实现,需要符合“开放-封闭”原则。
规则是可以复用的,方便其他模块使用。
首先需要一个规则对象,这个对象里面封装了所有规则方法处理,而且这些方法需要遵守一些参数规则:
第一个参数是要校验的值
最后一个参数接收校验失败时显示的文本
中间可以传递校验的参数
现在我们单独把这个文件放到一个地方进行维护:
rules.js
exportconstrules={//非空判断isNonEmpty(value,errorMsg){if(value===''){returnerrorMsg}},//最小长度限制minLength(value,errorMsg,length){if(value.length<length){returnerrorMsg}},//是否为手机号isMobile(value,errorMsg){if(!/(^1[3|5|8][0-9]{9}$)/.test(value)){returnerrorMsg}},}有了这个对象后,我们可以统一添加各种各样的规则,而且如果规则发生变化也很容易去更改,不用去考虑具体的业务逻辑。
接着我们需要抽象一个Validator校验器,你可以理解是一个控制器,可以利用它来添加和触发规则。它有以下属性:
cache数组,里面保存了所有的规则配置。
add方法,通过该方法来向某个表单项添加规则。
validator方法,通过该方法触发所有表单项的校验。
我们实现一下这个类:
validator.js
import{rules}from'./rules.js'classValidator{constructor(){this.cache=[]}/***添加规则*@param{HTMLElement}formItem被校验的表单项*@param{Array<{rule:string;errorMsg:string}>}ruleConfig校验规则*/add(formItem,ruleConfig){//遍历所有规则配置,存放到cacheruleConfig.forEach(({rule,errorMsg})=>{//获取参数minLength:6->['minLength','6']constparams=rule.split(':')//将校验函数存入缓存this.cache.push(()=>{//校验规则const_rule=params.shift()//校验的值params.unshift(formItem.value)//messageparams.push(errorMsg)returnrules[_rule].apply(formItem,params)})})}validate(){//调用所有校验函数for(constfnofthis.cache){consterrorMsg=fn()if(errorMsg){returnerrorMsg}}}}exportdefaultValidator接下来我们需要去写业务逻辑:
<scripttype="module">importValidatorfrom'./validator.js'constvalidatorFn=(registerForm)=>{constvalidator=newValidator()validator.add(registerForm.userName,[{rule:'isNonEmpty',errorMsg:'用户名不能为空',},{rule:'minLength:6',errorMsg:'用户名长度不能小于10位',},])validator.add(registerForm.password,[{rule:'minLength:6',errorMsg:'密码长度不能小于6位',},])validator.add(registerForm.phoneNumber,[{rule:'isMobile',errorMsg:'手机号码格式不正确',},])returnvalidator.validate()}constregisterForm=document.getElementById('registerForm')registerForm.onsubmit=function(e){e.preventDefault()consterrorMsg=validatorFn(this)if(errorMsg){alert(errorMsg)returnfalse}alert('success')}</script>此时代码维护性就更好了,之后即使新增了表单项也没有关系,我们只需要添加进去就完事了,而如果还是之前的做法话可能需要写大量的if/else,很显然代码是不好维护的,这种不用在业务逻辑里关心具体校验方法并以配置的形式添加规则的方式就是策略模式的一种体现,好像还是很模糊对吧...
so,到底什么是策略模式?
策略模式概念与使用场景概念用我个人理解的一句话总结就是:为达到目的,不择手段,不管用什么方式!
我们先来看第一个词“策略”。策略从字面上来看就是可以实现目标的所有方案,在上面的例子中rules对象中所有的规则就是策略,再想想我们是为了什么才有了这些策略呢,再回头看看上面的Validator类,它提供了校验功能,使用者利用它来管理校验规则,而为了达成校验这个目的,才有了各种各样的策略。引用《JavaScript设计模式与实践》的一句话:
策略模式指的是定义一系列的算法,把它们一个个封装起来。将不变的部分和变化的部分隔开是每个设计模式的主题,策略模式也不例外,策略模式的目的就是将算法的使用与算法的实现分离开来。
策略模式有两部分组成:
一组策略类:可以对应rules对象中所有方法,每一个方法都是一个策略。
一个Context类:接受使用者请求,把请求委托给某个策略,对应上面的Validator
现在你应该至少了解了它的奥秘之处了吧,那它的使用场景呢?
使用场景策略模式其实比较常用,一般当你发现你业务逻辑为了达到某个目标要进行大量判断的时候就可以考虑了,除了上面的表单校验案例,其实还有很多应用场景。
移动支付我们目前基本上所有消费都是扫码支付,但是支付的方式多种多样,有的人喜欢支付宝,有的人用微信,还有用银联的...
当你想到做一件事有很多种方式时就可以自然而然可以想到策略模式,“扫码支付”是一个目标,“支付方式”就是一组策略的集合。
出行方式我们日常出行可能有很多种方式,为达到这个目标,我们会选择以下几种:
走路
骑自行车
骑电动车
开车
坐飞机
这些不同的出行方式正是策略模式的用武之地。
策略模式在Vue源码中的应用上面的案例可能实战用得比较少,大家平时肯定都是用框架进行开发,下面我以Vue2.x为例,来讲讲它源码中使用策略模式的地方来加深大家的理解。
大家肯定都清楚,newVue的时候会传入一个option对象,或者使用mixin来给某个组件混入一些属性:
newVue({mixins:[],data:{//...},methods:{//...},})每个属性合并的规则其实都不一样,这些不同的规则其实就是策略模式中的策略,又称为“算法”,使用者根本不需要操心内部的实现,我们只需要安装业去写options就可以了,下面我们来看看具体的实现。
首先newVue的时候内部会调用_init方法进行一些初始化:
src/core/instance/index.js
functionVue(options){//Vue构造函数只能通过new调用if(process.env.NODE_ENV!=='production'&&!(thisinstanceofVue)){warn('Vueisaconstructorandshouldbecalledwiththe`new`keyword')}//调用init方法this._init(options)}该_init方法有一个逻辑是对传入的配置进行处理:
src/core/instance/init.js
exportfunctioninitMixin(Vue:Class<Component>){Vue.prototype._init=function(options?:Object){//记录vue实例constvm:Component=this//将Vue构造函数中options与用户传递的options进行合并vm.$options=mergeOptions(resolveConstructorOptions(vm.constructor),//获取vue实例内部的配置,在new之前已经声明在Vue.options里面,不必关心options||{},//传入的配置vm,)}}注意这个mergeOptions它是很关键的一个方法,它会对传入的配置与已有的配置进行合并,使用它内部自己的“算法”。该方法在src/core/util/options.js,下面是它的核心逻辑:
exportfunctionmergeOptions(parent:Object,child:Object,vm?:Component,):Object{//从这可以看出不管是在组件实例还是根实例都可以接受函数if(typeofchild==='function'){child=child.options}//将外部传入的props、inject、directives统一成内部的格式normalizeProps(child,vm)normalizeInject(child,vm)normalizeDirectives(child)//递归合并mixins和extends选项到parent上if(!child._base){if(child.extends){parent=mergeOptions(parent,child.extends,vm)}if(child.mixins){for(leti=0,l=child.mixins.length;i<l;i++){parent=mergeOptions(parent,child.mixins[i],vm)}}}//创建一个普通对象,这个是最终返回的配置constoptions={}letkey//遍历parent中所有属性,通过`mergeField`方法中的算法把配置合并到options对象for(keyinparent){mergeField(key)}//遍历child所有不存在parent中的属性,通过`mergeField`方法中的算法把配置合并到options对象for(keyinchild){if(!hasOwn(parent,key)){mergeField(key)}}//策略模式合并:为每一个配置选择一种不同的方式进行合并functionmergeField(key){conststrat=strats[key]||defaultStratoptions[key]=strat(parent[key],child[key],vm,key)}returnoptions}首先会对传入的配置进行一个标准化,主要是对props、inject、directives三个属性进行统一化操作,在内部统一转化成一种格式:
normalizeProps(child,vm)normalizeInject(child,vm)normalizeDirectives(child)接下来会把传入配置中的extends与mixins递归合并到parent中:
constregisterForm=document.getElementById('registerForm')constvalidatorFn=(form)=>{if(form.userName.value===''){alert('用户名不能为空')returnfalse}if(form.password.value.length<6){alert('密码长度不能少于6位')returnfalse}if(!/(^1[3|5|8][0-9]{9}$)/.test(form.phoneNumber.value)){alert('手机号码格式不正确')returnfalse}returntrue}registerForm.onsubmit=function(e){constisValid=validatorFn(this)if(!isValid){returnfalse}alert('successsubmit')e.preventDefault()}0然后进入最关键的环节,利用策略模式对parent和child进行配置合并:
constregisterForm=document.getElementById('registerForm')constvalidatorFn=(form)=>{if(form.userName.value===''){alert('用户名不能为空')returnfalse}if(form.password.value.length<6){alert('密码长度不能少于6位')returnfalse}if(!/(^1[3|5|8][0-9]{9}$)/.test(form.phoneNumber.value)){alert('手机号码格式不正确')returnfalse}returntrue}registerForm.onsubmit=function(e){constisValid=validatorFn(this)if(!isValid){returnfalse}alert('successsubmit')e.preventDefault()}1我们回顾一下策略模式一般有两大类组成:
一组策略类:这里对应的就是strats和defaultStrat(默认策略)。
一个Context类:对应上面的?mergeField。
strats则定义了每一个配置以什么方式来合并,即具体的算法,这些算法定义在当前文件(src/core/util/options.js),而因为篇幅问题,我不会把所有合并的策略列出来,这里挑选几个规则进行介绍。
生命周期函数的合并策略该策略首先会遍历所有的生命函数钩子,然后都使用mergeHook这个函数的算法进行合并:
constregisterForm=document.getElementById('registerForm')constvalidatorFn=(form)=>{if(form.userName.value===''){alert('用户名不能为空')returnfalse}if(form.password.value.length<6){alert('密码长度不能少于6位')returnfalse}if(!/(^1[3|5|8][0-9]{9}$)/.test(form.phoneNumber.value)){alert('手机号码格式不正确')returnfalse}returntrue}registerForm.onsubmit=function(e){constisValid=validatorFn(this)if(!isValid){returnfalse}alert('successsubmit')e.preventDefault()}2LIFECYCLE_HOOKS定义在src/shared/constants.js,它列举了Vue中所有的生命周期:
constregisterForm=document.getElementById('registerForm')constvalidatorFn=(form)=>{if(form.userName.value===''){alert('用户名不能为空')returnfalse}if(form.password.value.length<6){alert('密码长度不能少于6位')returnfalse}if(!/(^1[3|5|8][0-9]{9}$)/.test(form.phoneNumber.value)){alert('手机号码格式不正确')returnfalse}returntrue}registerForm.onsubmit=function(e){constisValid=validatorFn(this)if(!isValid){returnfalse}alert('successsubmit')e.preventDefault()}3接下来我们看一下mergeHook的实现,它的核心逻辑是一个很复杂的三元运算符组成,但是我们可以转换成下面这样去理解:
constregisterForm=document.getElementById('registerForm')constvalidatorFn=(form)=>{if(form.userName.value===''){alert('用户名不能为空')returnfalse}if(form.password.value.length<6){alert('密码长度不能少于6位')returnfalse}if(!/(^1[3|5|8][0-9]{9}$)/.test(form.phoneNumber.value)){alert('手机号码格式不正确')returnfalse}returntrue}registerForm.onsubmit=function(e){constisValid=validatorFn(this)if(!isValid){returnfalse}alert('succe