策略模式定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
5.1使用策略模式计算奖金
策略模式有着广泛的应用,看一下年终奖的计算
某公司规定,绩效为S的员工,年终奖有4倍工资,绩效为A的人年终奖有3倍工资,而绩效为B的人年终奖是两倍工资。假设财务部要求我们提供一段代码,来方便他们计算员工的年终奖。
1.最初的代码
一般我们会这样做:
var calculateBonus = function(performanceLevel,salary){
if(performanceLevel == 'S'){
return salary*4
}
if(performanceLevel == 'A'){
return salary*3
}
if(performanceLevel == 'B'){
return salary*2
}
}
calculateBonus('B',20000);
calculateBonus('S',6000);
console.log(calculateBonus('B',20000));//40000
console.log(calculateBonus('S',6000));//24000
这段代码非常简单,但是缺点也是显而易见的。
- calculateBonus函数比较庞大,包含了很多if-else 语句,这些语句需要覆盖所有的逻辑分支。
- calculateBonus函数缺乏弹性,如果增加了一种新的绩效等级C,或者想把S绩效系数改为5,那我们必须深入calculateBonus函数内部实现,这是违反开放-封闭原则的。
- 算法复用性差,如果在程序的其他地方需要重用这些计算奖金的算法呢,我们就只能复制粘贴了。
2.使用组合函数重构代码
最容易想到的办法就是把各种算法封装到一个个的小函数里面,给它们良好的命名。它们可以被复用在程序的其他地方。
var performanceS = function(salary){
return salary*4;
}
var performanceA = function(salary){
return salary*3;
}
var performanceB = function(salary){
return salary*2;
}
var calculateBonus = function(performanceLevel,salary){
if(performanceLevel == 'S'){
return performanceS(salary);
}
if(performanceLevel == 'A'){
return performanceA(salary);
}
if(performanceLevel == 'B'){
return performanceB(salary);
}
}
console.log(calculateBonus('A',10000))//30000
上面的程序得到了一定的改善,但是我们依然没有解决最重要的问题,calculateBonus函数还是会可能越来月庞大,而且在系统变化的时候缺乏弹性。
3.使用策略模式重构代码
(黑色的两周终于过去了,每天加班都没空学习了···)
策略模式的目的就是将算法的使用与算法的实现分离开来,在这个例子,算法的使用方式是不变的,都是根据某个算法取得计算后的奖金数额。而算法的实现是各异和变化的,每种绩效对应着不同的计算规则。
一个基于策略模式的程序至少由两部分组成。第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。第二部分是环境类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;
}
var 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)//把计算奖金的操作委托给对应的策略对象
}
先来看一下上段代码用到的策略模式的思想:
定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换
具体的说就是:定义一系列的算法,把它们各自封装成策略类,算法被封装在策略类内部的方法里。在客户对Context发起请求的时候,Context总是把请求委托给这些策略对象中间的某一个进行计算。
接下来,我们来完成上面的代码,先创建一个bonus对象,并且给bonus对象设置原始数据,比如员工的原始工资。接下来把某个计算奖金的策略对象也传入bonus对象内部保存起来。当调用bonus.getBonus()
来计算奖金的时候,bonus对象本身并没有能力进行计算,继而把请求委托给了之前保存好的策略对象:
var bonus = new Bonus();
bonus.setSalary(10000);
bonus.setStrategy(new performanceS());
console.log(bonus.getBonus())//40000
bonus.setStrategy(new performanceA());
console.log(bonus.getBonus())//30000
可以看到,通过策略模式重构以后,代码更加清晰,各个类的职责更加鲜明。
5.2JavaScript版本的策略模式
上面的代码是模拟一些传统面向对象语言的实现,但实际上用JavaScript有更直接的做法:
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',20000))//80000
console.log(calculateBonus('A',10000))//30000
5.3使用策略模式实现缓动动画
我们的目标是编写一个动画类和一些缓动算法,让小球以各种各样的缓动效果在页面中运动。分析一下在运动开始之前,需要提前记录一些有用的信息:
- 动画开始时,小球所在的原始位置;
- 小球移动的目标位置;
- 动画开始时的准确时间点;
- 小球运动持续的时间。
先来了解一些常见的缓动算法,这些算法都接受4个参数,这4个参数的含义分别时动画已消耗的时间、小球原始位置、小球目标位置、动画持续的总时间,返回的值则是动画元素应该处在当前位置:
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
},
}
先在页面创建一个div
<div style="width:50px;height:50px;border-radius:50%;position: absolute;background: blue;" id="div"></div>
定义一个Animate类,Animate的构造函数接受一个参数:即将运动起来的dom节点。
var Animate = function(dom){
this.dom = dom;//进行运动的dom节点
this.startTime = 0;//动画启动时间
this.startPos = 0;//dom节点的初始位置
this.endPos = 0;//dom节点目标位置
this.propertyName = null;//dom节点需要被改变的css属性名
this.easing = null;//缓动算法
this.duration = null;//动画持续时间
}
接下来Animate.prototype.start
负责启动这个动画,在动画被启动的瞬间,要记录一些信息,供缓动算法在以后计算小球当前位置的时候使用。在记录完这些信息之后,此方法还要负责定时器。
// propertyName:要改变的css属性名,比如'left'、'top'分别代表左右移动或者上下移动
// endPos:移动的目标位置
// duration:动画持续时间
// easing:缓动算法
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
方法,该方法代表小球运动的每一帧要做的事情。在此处,这个方法负责计算小球的当前位置和调用更新css属性值的方法Animate.prototype.update
:
Animate.prototype.step = function(propertyName,endPos,duration,easing){
var t = +new Date;//取得当前时间
//如果当前时间大于动画开始时间加上动画持续时间之和,
//说明动画已经结束,此时要修正小球的位置。因为在这一帧开始之后,
//小球的位置已经接近了目标位置,但很可能不完全等于目标位置,
//此时我们要主动修正小球的当前位置为最终的目标位置。
//此外让Animate.prototype.step方法返回false,可以通知 Animate.prototype.start方法清除定时器。
if(t>=this.startTime+this.duration){
this.update(this.endPos);//更新小球的css属性
return false;
}
var pos = this.easing(t - this.startTime,this.startPos,this.endPos - this.startPos,this.duration);//小球当前的位置
this.update(pos);//更新小球的css属性
}
最后是负责更新小球css属性值的Animate.prototype.update方法:
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')
animate.start('left',500,1000,'strongEaseIn')
可以看到小球已经按照我们期望的缓动算法在页面中运动。
5.4表单验证
在web项目中,注册、登录、修改用户信息都离不开提交表单,假如我们正在编写一个注册页面,在点击注册按钮之前,有下面几条逻辑校验:
- 用户名不能为空
- 密码长度不能少于6位
- 手机号码必须符合格式
1.表单校验的第一个版本(未引入策略模式)
<form action="" id="registerForm" method="post">
请输入用户名:<input type="text" name="userName"/>
请输入密码:<input type="text" name="password"/>
请输入手机号码:<input type="text" name="phoneNumber"/>
<button>提交</button>
</form>
var registerForm = document.getElementById('registerForm');
registerForm.onsubmit = function(){
if(registerForm.userName.value === ''){
alert('用户名不能为空');
return false;
}
if(registerForm.password.value.length<6){
alert('密码长度不能少于6位');
return false;
}
if(!/(^1[3|5|8][0-9]{9}$)/.test(registerForm.phoneNumber.value)){
alert('手机号码格式不正确');
return false;
}
}
这种代码编写方式很常见,但是缺点和计算奖金最初的版本一模一样,接下来我们要用策略模式来重构表单校验的代码,第一步我们要把这些校验逻辑都封装成策略对象:
var strategies = {
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;
}
}
}
接下来准备实现Validator类。Validator在这里作为Context,负责接收用户的请求并委托给strategy对象。代码如下:
var validataFunc = function(){
var validator = new Validator();//创建一个validator对象
//添加一些校验规则
validator.add(registerForm.userName,'isNonEmpty','用户名不能为空');
validator.add(registerForm.password,'minLength:6','密码长度不能少于6位');
validator.add(registerForm.phoneNumber,'isMobile','手机号码格式不正确');
var errorMsg = validator.start();//获得校验结果
return errorMsg;//返回校验结果
}
var registerForm = document.getElementById('registerForm');
registerForm.onsubmit = function(){
var errorMsg = validataFunc();
//如果errorMsg有确切的返回值,说明未通过校验
if(errorMsg){
alert(errorMsg);
return false;//阻止表单提交
}
}
上面代码我们先创建了一个validator对象,然后通过validator.add方法,往validator对象中添加一些校验规则。添加规则后,调用validator.start()方法来启动校验。如果validator.start()返回了一个确切的errorMsg字符串当作返回值,说明该次校验没有通过,此时需让registerForm.onsubmit方法返回false来阻止表单的提交。
最后是Validator类的实现:
var Validator = function(){
this.cache = [];//保存校验规则
}
Validator.prototype.add = function(dom,rule,errorMsg){
var ary = rule.split(':');//把strategy和参数分开
this.cache.push(function(){//把校验的步骤用空函数包装起来,并且放入cache
var strategy = ary.shift();//用户挑选的strategy
ary.unshift(dom.value);//把input的value添加进参数列表
ary.push(errorMsg);//把errorMsg添加进参数列表
return strategies[strategy].apply(dom,ary);
})
}
Validator.prototype.start = function(){
for(var i = 0,validataFunc;validataFunc = this.cache[i++];){
var msg = validataFunc();//开始校验,并取得校验后的返回信息
if(msg){
return msg;//如果有确切的返回值,说明校验没有通过
}
}
}
使用策略模式重构代码之后,我们仅仅通过“配置”的方式就可以完成一个表单的校验,这些校验规则也可以复用在程序的任何地方,还能作为插件的形式,方便地移植到其他项目中。
2.给某个文本输入框添加多种校验规则
上面我们襟能校验一个文本框是否为空,如果我们既想校验它是否为空,又想校验它输入文本的长度不小于10呢?
// =====================策略对象=====================
var strategies = {
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;
}
}
}
// =====================Validator类=====================
var Validator = function(){
this.cache = [];//保存校验规则
}
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,validataFunc;validataFunc = this.cache[i++];){
var errorMsg = validataFunc();
if(errorMsg){
return errorMsg;//如果有确切的返回值,说明校验没有通过
}
}
}
// =====================客户调用代码=========================
var registerForm = document.getElementById('registerForm');
var validataFunc = function(){
var validator = new Validator();
validator.add(registerForm.userName,[{
strategy:'isNonEmpty',
errorMsg:'用户名不能为空'
},{
strategy:'minLength:10',
errorMsg:'用户名不能少于10位'
}]);
validator.add(registerForm.password,[{
strategy:'minLength:6',
errorMsg:'密码长度不能少于6位'
}]);
validator.add(registerForm.isMobile,[{
strategy:'isMobile',
errorMsg:'手机号码格式不正确'
}]);
var errorMsg = validator.start();//获得校验结果
return errorMsg;//返回校验结果
}
registerForm.onsubmit = function(){
var errorMsg = validataFunc();
//如果errorMsg有确切的返回值,说明未通过校验
if(errorMsg){
alert(errorMsg);
return false;//阻止表单提交
}
}
最后一起看一下策略模式的优缺点:
- 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
- 策略模式提供了对开放-封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,易于理解,易于扩展。
- 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
- 在策略模式中利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的替代方案。
当然,策略模式也有一些缺点,但这些缺点并不严重。
首先,使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比把它们负责的逻辑堆砌在Context中要好。
其次,要使用策略模式,必须了解所有的strategy,必须了解各个strategy之间的不同点,这样才能选择一个合适的strategy。比如,我们要选择一种合适的旅游出行路线,必须先了解选择飞机,火车,自行车等方案的细节。此时strategy要向客户暴露它的所有实现,这是违反最少知识原则的。