策略模式
定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
“并且使它们可以相互替换”,这句话在很大程度上是相对于静态类型语言而言的。因为静态类型语言中有类型检查机制,所以各个策略类需要实现同样的接口。当它们的真正类型被隐藏在接口后面时,它们才能被相互替换。而在JavaScript这种“类型模糊”的语言中没有这种困扰,任何对象都可以被替换使用。因此,JavaScript中的“可以相互替换使用”表现为它们具有相同的目标和意图。
计算奖金
员工的奖金与他的绩效有关
第一版
1const calculateBonus = function (performanceLevel, salary) {2 if (performanceLevel === 'S') {3 return salary * 44 }56 if (performanceLevel === 'A') {7 return salary * 38 }910 if (performanceLevel === 'B') {11 return salary * 212 }13}
函数体庞大,包含多个判断分支
缺乏弹性,如果修改 S 的倍数,需要在内部修改,违反开放封闭原则
可复用行差
策略模式重构
将算法的使用和算法的实现分开
一个基于策略模式的程序至少由两部分组成。第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。 第二个部分是环境类 Context,Context 接受客户的请求,随后把请求委托给某一个策略类。要做到这点,说明 Context 中要维持对某个策略对象的引用
1// 策略类2const PerformanceS = function () {}3PerformanceS.prototype.calculate = function (salary) {4 return salary * 45}67const PerformanceA = function () {}8PerformanceA.prototype.calculate = function (salary) {9 return salary * 310}1112const pPrformanceB = function () {}13PerformanceB.prototype.calculate = function (salary) {14 return salary * 215}1617// 环境类18const Bonus = function () {19 this.salary = null20 this.strategy = null21}2223Bonus.prototype.setSalary = function (salary) {24 this.salary = salary // 设置员工的原始工资25};2627Bonus.prototype.setStrategy = function (strategy) {28 this.strategy = strategy // 设置员工绩效等级对应的策略对象29}3031Bonus.prototype.getBonus = function () { // 取得奖金数额32 return this.strategy.calculate(this.salary) // 把计算奖金的操作委托给对应的策略对象33}
JavaScript 的策略模式
静态类型中需要通过组合,把策略(算法)封装到类中传到环境类里
JavaScript 函数也是对象,所以更简单和直接的做法是把strategy直接定义为函数
1// 策略2const strategies = {3 S(salary) {4 return salary * 45 },6 A(salary) {7 return salary * 38 },9 B(salary) {10 return salary * 211 },12}1314// 环境15const calculateBonus = function (level, salary) {16 return strategies[level](salary)17}
有点像用一个对象做映射(map)
多态在策略模式中的体现
Context 并没有计算奖金的能力,而是把这个职责委托给了某个策略对象。每个策略对象负责的算法已被各自封装在对象内部。当我们对这些策略对象发出“计算奖金”的请求时,它们会返回各自不同的计算结果,这正是对象多态性的体现,也是“它们可以相互替换”的目的。替换Context中当前保存的策略对象,便能执行不同的算法来得到我们想要的结果。
策略模式实现动画
动画开始时,小球所在的原始位置
小球移动的目标位置
动画开始时的准确时间点
小球运动持续的时间
1// 动画算法2var tween = {3 linear: function( t, b, c, d ){4 return c*t/d + b;5 },6 easeIn: function( t, b, c, d ){7 return c * ( t /= d ) * t + b;8 },9 strongEaseIn: function(t, b, c, d){10 return c * ( t /= d ) * t * t * t * t + b;11 },12 strongEaseOut: function(t, b, c, d){13 return c * ( ( t = t / d - 1) * t * t * t * t + 1 ) + b;14 },15 sineaseIn: function( t, b, c, d ){16 return c * ( t /= d) * t * t + b;17 },18 sineaseOut: function(t,b,c,d){19 return c * ( ( t = t / d - 1) * t * t + 1 ) + b;20 }21}2223class Animate {24 constructor(dom) {25 this.dom = dom // 进行运动的dom节点26 this.startTime = 0 // 动画开始时间27 this.startPos = 0 // 动画开始时,dom节点的位置,即dom的初始位置28 this.endPos = 0 // 动画结束时,dom节点的位置,即dom的目标位置29 this.direction = null // dom节点需要被改变的css属性名30 this.easing = null // 缓动算法31 this.duration = null // 动画持续时间32 }3334 start(direction, endPos, duration, easing) {35 this.startTime = +new Date; // 动画启动时间36 this.startPos = this.dom.getBoundingClientRect()[direction]; // dom节点初始位置37 this.direction = direction; // dom节点需要被改变的CSS属性名38 this.endPos = endPos; // dom节点目标位置39 this.duration = duration; // 动画持续事件40 this.easing = tween[easing]; // 缓动算法4142 const timer = setInterval(() => { // 启动定时器,开始执行动画43 if (this.step() === false){ // 如果动画已结束,则清除定时器44 clearInterval(timer);45 }46 }, 19)47 }4849 step() {50 const t = +new Date; // 取得当前时间51 if ( t >= this.startTime + this.duration ){ // (1)52 this.update( this.endPos ); // 更新小球的CSS属性值53 return false;54 }55 const pos = this.easing( t - this.startTime, this.startPos,56 this.endPos - this.startPos, this.duration );57 // pos为小球当前位置58 this.update( pos ); // 更新小球的CSS属性值59 }6061 update( pos ) {62 this.dom.style[ this.propertyName ] = pos + 'px'63 }64}656667// test68var div = document.getElementById( 'div' );69var animate = new Animate( div );7071animate.start( 'left', 500, 1000, 'strongEaseOut' );72// animate.start( 'top', 1500, 500, 'strongEaseIn' );
广义的“算法”
指一些业务规则
表单验证
1<form action="http:// xxx.com/register" id="registerForm" method="post">2 请输入用户名:<input type="text" name="userName" />3 请输入密码:<input type="text" name="password" />4 请输入手机号码:<input type="text" name="phoneNumber" />5 <button type="submit">提交</button>6</form>
用户名不为空
密码长度不小于六位
手机号码必须符合格式
第一版
1var registerForm = document.getElementById('registerForm')23registerForm.onsubmit = function(){4 if ( registerForm.userName.value === '' ){5 alert ( '用户名不能为空' )6 return false7 }8 if ( registerForm.password.value.length < 6 ){9 alert ( '密码长度不能少于6位' )10 return false11 }12 if ( !/(^1[3|5|8][0-9]{9}$)/.test( registerForm.phoneNumber.value ) ){13 alert ( '手机号码格式不正确' )14 return false15 }16}
策略模式重构
策略:
1const strategies = {2 isNonEmpty(spec) {3 const { value, errorMsg } = spec4 if (value) {5 return errorMsg6 }7 },89 minLength(spec) {10 const { value, errorMsg, length } = spec11 if (value.length < length){12 return errorMsg13 }14 },1516 maxLength(spec) {17 const { value, errorMsg, length } = spec18 if (value.length > length) {19 return errorMsg20 }21 }2223 isMobile(spec){24 const { value, erroeMsg } = spec25 if (!/(^1[3|5|8][0-9]{9}$)/.test(value)){26 return errorMsg27 }28 },29}
环境:
1class Validator {2 constructor() {3 this.caches = [] // 保存校验规则4 }56 add(dom, rules) {7 const value = dom.value8 rules.forEach(rule => {9 this.caches.push(() => {10 return rule[strategy](rule)11 })12 })13 }1415 start() {16 for (const fn of this.caches) {17 const errorMsg = fn()18 if (errorMsg) {19 return errorMsg20 }21 }22 }23}
调用:
1const validataFunc = function(){2 const validator = new Validator()34 // 添加一些校验规则5 validator.add(registerForm.userName, [6 { strategy: 'maxLength', errorMsg: '密码长度不大于 10 位', length: 10 },7 { strategy: 'isNonEmpty', errorMsg: '用户名不能为空' },8 ])9 validator.add(registerForm.password, [10 { strategy: 'minLength', errorMsg: '密码长度不小于 6 位', length: 6 },11 ])12 validator.add(registerForm.phoneNumber, [13 { strategy: 'isMobile', errorMsg: '手机号码格式不正确' },14 ])1516 const errorMsg = validator.start() // 获得校验结果17 return errorMsg // 返回校验结果18}1920const registerForm = document.querySelector('#registerForm')21registerForm.addEventListener('submit', e => {22 const errorMsg = validataFunc() // 如果errorMsg有确切的返回值,说明未通过校验23 if (errorMsg) {24 e.preventDefault()25 }26})
优缺点
优点:
避免多个分支
符合开放封闭原则
可复用
通过组合和委托代替继承,更轻便
缺点:
程序中会有许多策略对象,但实际上比堆砌在 Context 中好
strategy 要向客户暴露它的所有实现,违反最少知识原则
一等函数对象与策略模式
除了将策略封装成对象,还可以将分散在程序中的函数作为策略进行传入
1// 策略2const S = function (salary) {3 return salary * 44}5const A = function (salary) {6 return salary * 37}8const B = function (salary) {9 return salary * 210}1112// 环境13const calculateBonus = function (func, salary) {14 return func(salary)15}