策略模式
定义
定义一系列的算法,把它们一个个封装起来,并且使他们可以相互替换。
主要解决
在多种算法相似的情况下,使用 if…else 所带来的复杂和难以维护的问题。
奖金的例子
很多公司的年终奖的是根据员工的工资基数和年底绩效情况来发放的。例如,绩效为S的员工年终奖有4倍工资,绩效为A的员工年终奖有3倍工资,绩效为B的员工年终奖有2倍工资。假如需要一段代码来计算对应员工的年终奖。
最基础写法
我们直观想到的就是用if...else
或者switch...case
的方法来写。如下
1 2 3 4 5 6 7 8 9
| let calculateBonus = function(performanceLevel, salary){ if(performanceLevel === 'S'){ return salary * 4 }else if(performanceLevel === 'A'){ return salary * 3 }else if(performanceLevel === 'B'){ return salary * 2 } }
|
虽然代码简单,但是这段代码有很多问题:
- 过多的
if...else
语句,代码结构不好
calculateBonus
函数缺乏弹性,如果增加一种新的绩效C
,或者修改现有等级的奖金逻辑,就必须深入函数内部进行修改,这违背了开放-封闭原则
- 算法复用性差,如果需要在程序的其他地方重用这些算法就只能重新写(cpoy)一份相似的。
所以我们现在尝试一步步重构代码。
使用组合函数重构代码
这里我们将不同的绩效的计算代码抽出来,形成对应的计算函数,在计算奖金里调用这些函数进行计算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| let performanceS = function(salary){ return salary * 4 }
let performanceA = function(salary){ return salary * 3 }
let performanceB = function(salary){ return salary * 2 }
let calcalateBonus = function(perfomanceLevel, salary){ if(performanceLevel === 'S'){ return performanceS(salary) }else if(performanceLevel === 'A'){ return performanceA(salary) }else if(performanceLevel === 'B'){ return performanceB(salary) } }
|
但是这段代码对于最大的问题还没有解决:
- 过多的
if...else
语句,代码结构不好
calculateBonus
函数缺乏弹性,如果增加一种新的绩效C
,或者修改现有等级的奖金逻辑,就必须深入函数内部进行修改,这违背
使用策略模式重构代码
一个策略模式的程序至少由两部分组成。
- 第一部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。
- 第二部分是环境类Context,Context接受客户的请求,随后把请求委托给某一个策略类。要做到这一点,说明Context中要维持对某个策略对象的引用。
使用传统面向对象语言的算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| let performanceS = function(){}
performanceS.prototype.calculate = function(salary){ return salary * 4 }
let performanceA = function(){}
performanceS.prototype.calculate = function(salary){ return salary * 3 }
let performanceB = function(){}
performanceS.prototype.calculate = function(salary){ return salary * 2 }
let Bonus =function(){ this.salary = null this.strategy = null }
Bonus.prototype.setSalary = function(salary){ this.salary = salary }
Bonus.prototype.setStrategy = function(strategy){ this.strategy = strategy }
Bonus.prototype.getBonus = function(){ if(!this.strategy){ throw new Error('未设置strategy属性') } return this.strategy.calculate(this.salary) }
|
根据上面的模式,我们将计算具体奖金的方法封装。然后定义Bonus来调用封装的方法来计算具体的奖金。
但是对于JavaScript,我们的实现可以更简单一些。
JavaScript版本的策略模式
我们可以直接使用字面量对象来封装strategy。然后通过Context来计算奖金。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| let strategy = { S: function(salary){ return salary * 4 }, A: function(salary){ return salary * 3 }, B: function(salary){ return salary * 2 } }
let calculateBonus = function(level, salary){ return srtategy[level](salary) }
console.log(calculateBonus('S', 20000)) console.log(calculateBonus('A', 10000))
|
表单验证
表单验证与上面计算奖金的算法类似,都是由多个相似的规则组成。加入现在有以下规则:
- 用户名不能为空
- 密码长度不能少于6位
- 手机号码必须符合格式
基本写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <html> <body> <form id='registerForm'> 请输入用户名:<input type='text' name='userName'/> 请输入密码:<input type='text' name='password'/> 请输入手机号:<input type='text' name='phoneNumber'/> <button> 提交 </button> </form> <script> let registerForm = document.getElementBuId('registerForm') registerForm.onsubmit = function(){ if(registerForm.userName.value === ''){ alter('用户名不能为空') return false } if(registerForm.password.valu.length < 6){ alter('密码不能少于6位') return false } if(!/(!1[3|5|8][0-9]{9}$)/.test(registerForm.phoneNumber.value)){ alert('手机号码格式不正确') return false } } </script> </body> </html>
|
这是最常见的编写方式,其缺点也与奖金计算的最初版本一样。
registerForm.onsubmit
函数比较庞大,包含了很多if...else
语句。
registerForm.onsubmit
缺乏弹性,如果增加校验规则必须深入函数内部进行修改,违背了开放-封闭原则。
- 算法复用性差,如果在程序中增加另外一个表单,我们仍然需要写完全相同的算法。
用策略模式重构表单校验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| let stategies = { isNonEmpty: function(value, errorMsg){ if(value === ''){ return errorMsg } }, minLength: function(value, length, errorMsg){ if(value.length < length){ return errorMsg } }, isMobile: function(value, errorMsg){ if(!/(!1[3|5|8][0-9]{9}$)/.test(registerForm.phoneNumber.value)){ return errorMsg } } }
let Validator = function(){ this.cache = [] } Validator.prototype.add = function(dom, rule, errorMsg){ let ary = rule.split(':') this.cache.push(function(){ let strategy = ary.shift() ary.unshift(dom.value) ary.push(errorMsg) return strategies[strategy].aplly(dom, ary) }) } Validator.prototype.start = function(){ for(let i = 0; validatorFunc; validatorFunc = this.cache[i++]){ let msg = validatorFunc() if(msg){ return msg } } }
let validatorFunc = function(){ let validator = new Validator() validator.add(registerForm.userName, 'isNonEmpty', '用户名不能为空') validator.add(registerForm.password, 'minLength:6', '密码长度不能少于6位') validator.add(registerForm.phoneNumer, 'isNonEmpty', '用户名不能为空') let errorMsg = validator.start() return errotMsg }
let registerForm = document.getElementById('registerForm') registerForm.onsubmit = function(){ let errorMsg = validatorFunc() if(errorMsg){ alert(errorMsg) return false } }
|
策略模式的优缺点
优点:
- 策略模式利用了组合、委托和多态的技术和思想,可以有效的避免多重条件选择语句。
- 策略模式提供了对开放-封闭原则的完美支持,将算法封装在独立的
strategy
中,使得他们易于切换,易于理解,易于扩展。
- 在策略模式的算法也可以服用在系统中的其他地方,从而有效的避免代码的冗余。
- 在策略模式中利用组合和委托来让
Context
拥有执行算法的能力,这也是继承的一种更轻便的替代方案。
缺点:
策略模式的最大特点就是将同类操作封装在一个对象中,然后再其他类中调用该对象中对应的操作。
参考
《JavaScript设计模式与开发实践》