设计模式-策略模式
策略模式
策略模式:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
一个基于策略模式的程序至少由两个部分组成。
第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。
第二个部分是环境类 Context,Context 接受客户的请求,随后把请求委托给某一个策略类。
Context 中要维持对某个策略对象的引用。
实现
计算奖金
使用策略模式计算奖金
// 先把每种绩效的计算规则都封装在对应的策略类里面
var performanceS = function () {}
performanceS.prototype.calculate = function (salary) {
return salary * 4
}
var performanceA = function () {}
performanceA.prototype.calculate = function (salary) {
return salary * 3
}
var performanceB = function () {}
performanceB.prototype.calculate = function (salary) {
return salary * 2
}
// 奖金类Bonus
var Bonus = function () {
this.salary = null // 原始工资
this.strategy = null // 绩效等级对应的策略对象
}
Bonus.prototype.setSalary = function (salary) {
this.salary = salary // 设置员工的原始工资
}
Bonus.prototype.setSrategy = function (strategy) {
this.strategy = strategy // 设置员工绩效等级对应的策略对象
}
// 取得奖金数额
Bonus.prototype.getBonus = function () {
return this.strategy.calculate(this.salary) // 把计算奖金的操作委托给对应的策略对象
}
// 计算奖金
var bonus = new Bonus()
bonus.setSalary(10000)
bonus.setSrategy(new performanceS()) // 设置策略对象
console.log('performanceS', bonus.getBonus()) // 获取奖金
bonus.setSrategy(new performanceA()) // 替换策略对象
console.log('performanceA', bonus.getBonus())
策略模式的实现:定义一系列的算法,把它们各自封装成策略类,算法被封装在策略类内部的方法里。在客户对Context发起请求的时候,Context总是把请求委托给这些策略对象中间的某一个进行计算。
使用策略模式之后,代码变得更加清晰,各个类的职责更加鲜明。
使用对象的方式改写
// 对象改写
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 S', calculateBonus('S', 10000))
console.log('calculateBonus A', calculateBonus('A', 10000))
动画
缓动动画
// 缓动动画
// 参数的含义分别是动画已消耗的时间、小球原始位置、小球目标位置、动画持续的总时间
// 返回的值则是动画元素应该处在的当前位置
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
},
strongEaseOut: function (t, b, c, d) {
return c * ((t = t / d - 1) * t * t * t * t + 1) + b
},
sineaseIn: function (t, b, c, d) {
return c * (t /= d) * t * t + b
},
sineaseOut: function (t, b, c, d) {
return c * ((t = t / d - 1) * t * t + 1) + b
}
}
Animate 类
利用这个动画类和一些缓动算法就可以让小球运动起来。
使用策略模式把算法传入动画类中,来达到各种不同的缓动效果,这些算法都可以轻易地被替换为另外一个算法,这是策略模式的经典运用之一。
策略模式的实现并不复杂,关键是如何从策略模式的实现背后,找到封装变化、委托和多态性这些思想的价值。
// Animate类
var Animate = function (dom) {
this.dom = dom // 进行运动的dom节点
this.startTime = 0 // 动画开始时间
this.startPos = 0 // 动画开始时, dom节点的位置,即dom的初始位置
this.endPos = 0 // 动画结束时,dom节点的位置,即dom的目标位置
this.propertyName = null // dom节点需要被改变的css属性名
this.easing = null // 缓动算法
this.duration = null // 动画持续时间
}
// 动画启动,需要记录一些信息,供缓动算法在以后计算小球当前位置的时候使用
Animate.prototype.start = function (propertyName, endPos, duration, easing) {
this.startTime = +new Date // 动画启动时间
this.startPos = this.dom.getBoundingClientRect()[propertyName] // dom节点初始位置
this.propertyName = propertyName // dom节点需要被改变的CSS属性名
this.endPos = endPos // dom节点目标
this.duration = duration // 动画持续时间
this.easing = tween[easing] // 缓动算法
var self = this
// 启动定时器,开始执行动画
var timeId = setInterval(function () {
if (self.step() === false) { // 如果动画已结束,则清除定时器
clearInterval(timeId)
}
}, 19)
}
// 设置动画每一帧需要做的事情
Animate.prototype.step = function () {
var t = +new Date // 取得当前时间
// 如果当前事件大于动画开始时间加上动画持续时间之和,说明动画已经结束,此时需要修正小球的位置
if (t >= this.startTime + this.duration) {
this.update(this.endPos) // 更新小球的CSS属性值
return false // start函数清除定时器
}
var pos = this.easing(t - this.startTime, this.startPos, this.endPos - this.startPos, this.duration)
// pos为小球当前位置
this.update(pos) // 更新小球的CSS属性值
}
// 更新小球CSS属性值
Animate.prototype.update = function (pos) {
this.dom.style[this.propertyName] = pos + 'px'
}
// 测试
// var div = document.getElementById('div')
// var animate = new Animate(div)
// animate.start('left', 500, 1000, 'strongEaseOut')
从定义上看,策略模式就是用来封装算法的。
在业务规则中使用,只要这些业务规则指向的目标一致,并且可以被替换使用,就可以用策略模式来封装。
表单校验
使用策略模式可以仅仅通过配置的方式完成一个表单的校验,这些校验规则也可以复用在程序的任何地方,还能作为插件的形式,方便地移植到其他项目中。
// 表单验证
// 校验逻辑封装为策略对象
var strategies = {
// 不为空
isNotEmpty: 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
}
}
}
// validator类作为context
var Validator = function () {
this.cache = [] // 保存校验规则
}
// 仅验证一条规则
// Validator.prototype.add = function(dom, rule, errorMsg){
// 冒号前面的minLength代表客户挑选的strategy对象,冒号后面的数字6表示在校验过程中所必需的一些参数。
// 'minLength:6’的意思就是校验registerForm.password这个文本输入框的value最小长度为6。
// 如果这个字符串中不包含冒号,说明校验过程中不需要额外的参数信息。
// var ary = rule.split(':') // 把strategy和参数分开
// this.cache.push(function(){ // 把校验的步骤用控函数包装起来,并且放入cache
// var strategy = ary.shift() // 用户挑选的strategy
// ary.unshift(dom.value) // 把input的value添加进参数列表
// ary.push(errorMsg)
// return strategies[strategy].apply(dom, ary)
// })
// }
// 多条验证规则
Validator.prototype.add = function (dom, rules) {
var self = this
for (var i = 0, rule; rule = rules[i++];) {
(function (rule) {
var strategyAry = rule.strategy.split(':')
var errorMsg = rule.errorMsg
self.cache.push(function () {
var strategy = strategyAry.shift()
strategyAry.unshift(dom.value)
strategyAry.push(errorMsg)
return strategies[strategy].apply(dom, strategyAry)
})
})(rule)
}
}
Validator.prototype.start = function () {
for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
var msg = validatorFunc() // 开始校验,并取得校验后的返回信息
if (msg) { // 如果有确切的返回值,说明校验没有通过
return msg
}
}
}
// ================================== 使用 ======================================
var validatorFunc = function () {
var validator = new Validator() // 创建一个validator对象
// 添加一些校验规则, 单条验证规则的使用
// validator.add(registerForm.userName, 'isNotEmpty', '用户名不能为空')
// validator.add(registerForm.password, 'minLength:6', '密码长度不能少于6位')
// validator.add(registerForm.phoneNumber, 'isMobile', '手机号码格式不正确')
// 多条规则的验证
validator.add(registerForm.userName, [{
strategy: 'isNotEmpty',
errorMsg: '用户名长度不能为空'
}, {
strategy: 'minLength:10',
errorMsg: '用户名长度不能小于10位'
}])
var errorMsg = validator.start() // 获得校验结果
return errorMsg // 返回校验结果
}
var registerForm = document.getElementById('registerForm')
registerForm.onsubmit = function () {
var errorMsg = validatorFunc() // 如果errorMsg有确切发返回值,说明未通过校验
if (errorMsg) {
console.log(errorMsg)
return false // 阻止表单提交
}
}
优缺点
优点
- 使用策略模式,可以消除了原程序中大片的条件分支语句。每个策略对象负责的算法已被各自封装在对象内部。减少了维护成本。
- 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
- 策略模式提供了对开放——封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它们易于切换,易于理解,易于扩展。
- 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
- 在策略模式中利用组合和委托来让 Context 拥有执行算法的能力,这也是继承的一种更轻便的替代方案。
缺点
- 使用策略模式会在程序中增加许多策略类或者策略对象。
- 要使用策略模式,必须了解所有的 strategy,必须了解各个 strategy 之间的不同点,这样才能选择一个合适的 strategy。strategy 要向客户暴露它的所有实现,这是违反最少知识原则的。
应用与实践
在以类为中心的传统面向对象语言中,不同的算法或者行为被封装在各个策略类中,Context 将请求委托给这些策略对象,这些策略对象会根据请求返回不同的执行结果,这样便能表现出对象的多态性。
在函数作为一等对象的语言中,策略模式是隐形的。strategy 就是值为函数的变量。
在 JavaScript 语言的策略模式中,策略类往往被函数所代替,这时策略模式就成为一种“隐形”的模式。