在程序设计中,常常遇到要实现一种功能有多种方案可以选择,比如一个压缩文件的程序,既可以选择 zip 算法,也可以选择 gzip 算法。
策略模式的定义:定义一系列算法,把他们一个个封装起来,并且使他们可以相互转换。
使用策略模式计算奖金
策略模式用着广泛的应用,这里书上用了一个计算奖金的例子来举例。
- 最初的代码实现
编写一个 calculateBons 的函数来计算每个人的奖金数额,函数接受两个参数,员工的工资数额和它的绩效考核等级。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| var calculateBons = function(performanceLevelm, salary) { if (performanceLevelm === "S") { return salary * 4; } if (performanceLevelm === "A") { return salary * 3; } if (performanceLevelm === "B") { return salary * 2; } }; console.log(calculateBons("S", 20000)); console.log(calculateBons("A", 15000)); console.log(calculateBons("B", 10000));
|
这里的代码非常简单,就不写注释了。
这段代码缺陷也十分严重
- calculateBons 函数比较庞大,充斥了 if 判断语句,这些语句需要覆盖所有逻辑分支
- calculateBons 函数缺乏弹性,如果增加了新的员工,就需要更改函数内部的逻辑,这是违反开放-封闭的原则的。
- 算法复用性极差,不可复用。
- 使用组合函数重构代码
最容易想到的就是组合函数来重构代码,把各种算法封装到一个个小函数里,小函数有着良好的命名,可以一目了然知道对应那种算法,也可以被复用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| var performanceS = function(salary) { return salary * 4; }; var performanceA = function(salary) { return salary * 3; }; var performanceB = function(salary) { return salary * 2; };
var calculateBonus = function(performanceLevelm, salary) { if (performanceLevelm === "S") { return performanceS(salary); } if (performanceLevelm === "A") { return performanceA(salary); } if (performanceLevelm === "B") { return performanceB(salary); } }; console.log(calculateBonus("A", 20000));
|
这里的改善也十分有限,calculateBonus 函数依旧可能越来越庞大,而且在系统变化时候缺乏弹性。
- 使用策略模式重构代码
策略模式指的是定义一系列算法,把他们一个个封装起来,将不变的部分和变化的部分分隔开是每个设计模式的主题,策略模式也不例外。
在这个例子中,算法的使用方式是不变的,而每种绩效规则对应着不同的计算规则是变化的。
使用策略模式来重构上面的代码。第一个版本是模仿传统面向对象语言中的实现,先把每种绩效的计算规则都封装在对应的策略类里面
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
| class performanceS { calculate(salary) { return salary * 4; } } class performanceA { calculate(salary) { return salary * 3; } } class performanceB { calculate(salary) { return salary * 2; } }
class Bons { constructor(html) { this.salary = null; this.strategy = null; } setSalary(salary) { this.salary = salary; } setStrategy(strategy) { this.strategy = strategy; } getBonus() { return this.strategy.calculate(this.salary); } } var bonus = new Bons(); bonus.setSalary(10000); bonus.setStrategy(new performanceB()); console.log(bonus.getBonus());
|
重构之后的代码更加清晰,个各类职责鲜明(但是我感觉好麻烦)
JavaScript 版本中的策略模式
在上面的代码中,让 strategy 对象从各个策略类中创建而来,这是模拟一些传统面向对象语言的实现,再试及的 JavaScript 语言中,函数也是对象,所以更简单和直接的做法是吧 strategy 直接定义为函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| var strategies = { S: function(salary) { return salary * 4; }, A: function(salary) { return salary * 3; }, B: function(salary) { return salary * 2; } }; var calculateBonus = function(level, salary) { return strategies[level](salary); }; console.log(calculateBonus("A", 20000), calculateBonus("S", 30000));
|
多态在策略模式中的体现
通过使用策略模式重构代码,消除了源程序中大片的条件分支,所有跟计算奖金有关的逻辑不在放在 context 中,而是分布在各个策略中,context 并没有计算奖金的能力,而是把这个职责委托给某个策略对象,每个策略对象负责的算法被各自封装在对象内部,当我们对这些策略对象发出计算奖金的请求时,他们会返回各自不同的计算结果,这正事对象多态的体现,也是他们可以相互替换的目的,替换 context 中当前保存的策略对象,便能执行不同的算法来得到我们想要的结果。
使用策略模式实现缓动动画
如果让不熟悉前端开发的程序员投票,选出他们眼中的 JavaScript 语言在 web 开发中的两大用途,结果可能是这样的
- 编写一些让 div 飞来飞去的动画
- 验证表单
虽然只是玩笑,但是可以看出动画在 web 开发中的地位。一些别出心裁的动画效果可以让网站增色不少。
有一段时间网页游戏十分流行,(贪玩蓝月。。。)HTML5 版本的游戏可以达到不逊于 Flash 游戏的效果,我们首先让一个小球按照不同的算法进行运动。
实现动画效果的原理
用 JavaScript 实现动画效果的原理跟动画片的制作一样,动画片是吧一些差距不大的原画以较快的帧数播放,达到视觉上的动画效果,在 JavaScript 中,可以通过连续改变元素的某个 css 属性。
思路和一些准备
目标是编写一个动画类和一些缓动算法,让小球以各种各样的缓动效果在页面中运动。
分析一下思路
- 动画开始时,小球所在的原始位置
- 小球的目标位置
- 动画开始时的准确时间点
- 小球持续运动的时间
随后通过 setInterval 创建一个定时器,定时器每隔 19ms 循环一次,在定时器的每一帧里,我们会把动画已消耗的时间、小球的原始位置、小球目标位置和动画的持续的总时间等信息传入缓动算法,该算法会通过这个几个参数,计算出小球当前应该所在的位置,最后在更新该 div 对应的 css 属性,小球就能顺利的运行动起来。
让小球运动起来
在实现完整的功能之前,先了解一些常见的缓动算法,这些算法最初来自 Flash,但可以非常方便植入其他语言中。
这些算法都接受四个参数,这四个参数的含义分别是动画已消耗的时间、小球原始位置、小球目标位置、动画持续的总时间,返回的值则是动画元素应该处在的当前位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| var tween = { linear: function(t, b, c, d) { return (c * t) / d + b; }, easeIn: function(t, b, c, d) { return c * (t /= d) * t + b; }, strongEaseIn: function(t, b, c, d) { return c * (t /= d) * t * t * t * t + b; }, stringEaseOut: function(t, b, c, d) { return c * ((t = t / d - 1) * t * t * t * t + 1) + b; }, subeaseIn: function(t, b, c, d) { return c * (t /= d) * t * t + b; }, subeaseOut: function(t, b, c, d) { c * ((t = t / d - 1) * t * t + 1) + b; } };
|
下面编写完整代码,思想来自 JQuery 库,本节演示策略模式,并非编写一个完整的动画库,所以省去了动画的列队控制等更多完整功能。
1 2 3
| <body> <div style="position:absolute;background:blue">Im DIV</div> </body>
|
接下来定义 Animate 类,Animate 的构造函数接受一个参数,即运动起来的 dom 节点。
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
| class Animate{ constructor(dom){ this.dom = dom; this.startTime = 0; this.startPos = 0; this.endPos = 0; this.propertyName = null; this.easing = null; this.duration = null; }; start(propertyName, endPos, duration, easing) { this.startTime = +new Date; this.startPos = this.dom.getBoundingClientRect()[propertyName]; this.propertyName = propertyName; this.endPos = endPos; this.duration = duration; this.easing = twwen[easing]; var self = this; var timeId = setInterval(function(){ if(self.step() === false){ clearInterval(timeId) } },19) } }
step(){ vat t = +new Date; if(t >= this.startTime + this.duration){ this.update(this.endPos) return false } var pos = this.easing(t - this.startTime, this.startPos,this.endPos - this.startPos, this.duration) this.update(pos) } update(pos){ this.dom.style[this.propertyName] = pos + 'px' }
|
更广义的“算法”
策略模式指的是定义一系列的算法,并且把他们封装起来。
从定义开,策略模式就是用来封装算法,但如果把策略模式仅仅用来封装算法,未免有一些大材小用,在实际开发中,我们通常会被算法的含义扩散开,是侧罗模式可以用来封装一系列的业务规则。
表单验证
表单验证逻辑
- 用户名不能为空
- 密码长度不能少于 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 30 31 32 33 34 35 36 37 38 39 40
| var stertegies = { 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(value)){ return errorMsg; } }, }
var Validator = function(){ this.cache = [] } Validator.prototype.add = function(dom, rule, errorMsg){ var ary = rule.split('.') this.cache.push(function(){ var strategy = ary.shift() ary.unshift(dom.value) ary.push(errorMsg) return strategies[strategy].apply(dom, ary)
}) }
Validator.prototype.start = function(){ for(var i = 0, validatorFunc; validatorFunc = this.cache[i++]){ var msg = validatorFunc() if(msg){ return msg } } }
|
策略模式的优缺点
策略模式是一种常用有效的设计模式,总结一些优点
- 策略模式利用组合、委托和多态等技术和思想,可以有效避免多重条件选择语句
- 侧罗模式提供了对开放-封闭原则的完美支持,将算法独立封装,是的它们易于切换,易于理解,易于扩展。
- 策略模式中算法可以复用在系统的其他地方,从而避免许多赋值粘贴工作。
- 侧罗模式中利用组合和委托让 Content 拥有执行算法的能力,这也是一种继承的一种更轻便的替代方案。
策略模式有一些缺点,但这些并不严重
首先使用策略模式在程序中正价许多策略类或者策略对象,但实际上这比把他们负责的逻辑堆砌在 Content 中要好。
其次使用策略模式,必须了解所有的策略,必须了解各个策略之间的不同,才能选择一个合适的策略。