在程序设计中我们往往会遇到实现某一功能有多种方案可以选择。比如一个压缩算法,我们可以选择zip算法,也可以选择gzip算法。
这些算法灵活多样,而且可以随意互相替换。这种解决方案就是本章要讨论的策略模式。
定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
1. 最初的代码实现
我们可以编写一个名为calculateBonus的函数来计算每个人的奖金额。很显然,calculateBonus函数要正确工作,就需要接收两个参数:员工工资数额和绩效考核等级。如下:
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(‘C’,6000);
可以看出代码十分简单,但是也存在着显而易见的缺点。
l if-else分支多,这些分支要覆盖所有的逻辑
l calculateBonus函数缺乏弹性,如果增加了一种新的绩效等级C,或是把绩效S的奖金系数改为5,那么我们必须深入calculateBonus函数的内部实现,这违反开放—封闭原则
l 算法的复用性差,如果在程序的其他地方需要重用这些计算奖金的算法呢?我们只有复制和粘贴。
2. 使用组合函数重构代码
一般容易想到的办法就是使用组合函数来重构代码,我们把各种算法封闭到一个小函数里面,这些小函数有着良好的全名,可能一目了然地知道它对应着哪咱算法,它们也可以被利用在程序的其他地方:
var performanceS= function(salary){
return salary*4;
}
var performanceA= function(salary){
return salary*3;
}
var performanceB= function(salary){
return salary*2;
}
varcalculateBonus = function(performanceLevel,salary){
if(performanceLevel==”S”){
return performanceS(salary);
}
if(performanceLevel==”A”){
return performanceA(salary);
}
if(performanceLevel==”B”){
return performanceB(salary);
}
}
calculateBonus(‘A’,10000);
目前,我们的程序得到了一定的改善,但这种改善非常有限,我们依然没有解决最重要的问题:calculateBonus函数有可能越来越庞大,而且在系统变化的时候缺乏弹性。
3. 使用策略模式重构代码
策略模式是指定义一系列的算法,把它们一个个封装起来。将不变的部分和变化的部分分隔开是每个设计模式的主题:
策略模式的目的就是将算法的使用与算法的实现分离开来。
在上面的例子里,算法的使用方式是不变的,都是根据某个算法取得计算后资金数额。而算法的实现是各异和变化的,每种绩效对应着不同的计算规则。
因此一个策略模式的程序至少由两部分组成。
第一个部分是一组策略类,它封装了具体的算法,并负责具体的计算过程。
第二个部分是环境类Context,Context接受客户请求,随后把请求委托给一个策略类。要做到这一点,说明Context中要维持对某个策略对象的引用。
下面重构上面代码,传统OOP语言中的实现:
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:
//context
var Bonus =function(){
this.salary = null;//原始工资
this.strategy = null; //绩效等级对应的策略对象
}
Bonus.prototype.setSalary= function(salary){
}
Bonus.prototype.setStrategy= function(strategy){
this.strategy = strategy; //设置策略对象
}
Bonus.prototype.getBonus= function(){
return this.strategy.calculate(this.salary);//
}
再来回顾一下策略模式的思想:
定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
在对客户对Context发起请求的时候,把它们各自封装成策略类,算法被封装在策略类内部的方法里。在客户对Context发起请求的时候,Context总是把请求委托给这些策略对象中的某一个进行计算。如下:
var bonus = newBonus();
bonus.setSalary(10000);
bonus.setStrategy(newperformanceS()); //设置策略对象
console.log(bonus.getBonus());//输出:40000
bonus.setStrategy(newperformance()); //设置策略对象
console.log(bonus.getBonus());//输出:30000
实际上在javascript语言中,函数也是对象,所以更简单和直接的做法是把strategy直接定义为对象
var strategies = {
“S”:function(salary){
return salary*4;
},
“A”:function(salary){
return salary*4;
},
“B”:function(salary){
return salary*4;
}
};
同样,Context也没有必要必须用Bonus类来表示,我们依然用calculateBonus函数来充当Context来接受用户请求,如:
var calculateBonus =function(level,salary){
return strategies[level](salary);
}
console.log(calculateBonus(‘S’,20000)); //输出80000
console.log(calculateBonus(‘S’,10000)); //输出30000
通过使用策略模式重构代码,我们消除了原程序中大片的条件分支语句。所以跟计算奖金有关的逻辑不在放在Context中,而是分布在各个策略对象中。Context并没有计算奖金的能力,而是把这个职责委托给了某个策略对象。
缓动算法,最初是来自Flash,但可以非常方便的移植到其它语言中。
这些算法接受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;
}
};
以下代码思想来源于jQuery库,由于本节内容是策略模式,而非编写一个完整的动画库,因此我们省去了动画的队列控制等更多完整功能。
定义一个div
var Animate = function(dom){
this.dom = dom;
this.startTime = 0;
this.startPos = 0;
this.endPos = 0;
this.propertyName = null;
this.easing = null; //缓动算法
this.duration = null ; //动画持续时间
}
接下来Animate.prototype.start方法负责启动这个动画,在动画被启动的瞬间,要记录一些信息,供缓动算法在以后计算当前位置的时候使用(本例中是位置),此方法还负责启动定时器。
Animate.prototype.start =function(propertyName,endPos,duration,easing){
this.startTime = +new Date; //动画启动时间
this.startPos = this.dom.getBoundingClientRect()[propertyName];
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);
}
//调用step
},19);
}
propertyName:要改变的CSS属性名,如‘left’、‘top’分别表示左右移动和上下移动
endPos:小球运动的目标位置
duration:动画持续时间
easing:缓动算法
再接下来是Animate.prototype.step方法,该方法代表小球运动的每一帧要做的事情。Animate.prototype.update是用来负责计算当前位置和更新位置
Animate.prototype.step = function(){
var t = +new Date;
if(t>=this.startTime+this.duration){ //(1)
this.update(this.endPos);
return false;
}
var pos =this.easing(t-this.startTime,this.startPos,this.endPos-this.startPos,this.duration);
this.update(pos);
}
(1)注释的意思,如果当前时间大于开始时间加上动画持续时间之和,说明动画已经结束,此时要修正小球的位置。主要用于修正最终的目标位置。
负责更新CSS属性值的Animate.prototype.update方法:
Animate.prototype.update = function(pos){
this.dom.style[this.prototypeName]= pos+”px”;
}
可以验证结果:
var div = document.getElementById(‘div’);
var animate = new Animate(div);
animate.start(‘left’,500,1000,’strongEaseOut’);
//animate.start(‘top’,1500,500,’strongEaseIn’);
从定义上看,策略模式就是用来封装算法的。但如果把策略模式仅仅用来封闭算法,未免大材小用。在实际开发中,我们通常会把算法的含义扩散开来,使策略模式也可以用来封装一系列“业务规则”。只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以用策略模式来封装它。
提交表单数据,在数据交给后台之前,常常要做的一些客户端力所能及的校验工作,比如注册的时候需要校验是否填写了用户名,密码长度等等。这样可以避免因为提交不合法数据而带来的不必要网络开销。
如下:
这是一种常见的代码编写方式,它的缺点跟计算资金的最初版本一模一样。
registerForm.onsubmit函数比较庞大,包含了很多if-else语句,这些语句需要覆盖所有的校验规则
registerForm.onsubmit函数缺乏弹性,如果增加了一种新的校验规则,或者想把密码的长度从6改成8,我们必须深入registerForm.onsubmit函数的内部实现,这是违反开放—封闭原则的
算法的复用性差,如果在程序中增加另一个表单,这个表单也需要进行一些类似的校验,那我们很可能将这些校验逻辑复制得漫天野。
下我们将用策略模式来重构表单校验,第一步我们要把校验逻辑都封装成策略对象:
var strategies = {
isNonEmpty:function(value,errorMsg){//不为空
if(value=’’){
return errorMsg;
}
},
minLength:function(value,length,errorMsg){
if(value.length
接下来我们来准备一个Validator类。它用来做为Context,负责接收用户的请求并委托给strategy对象。在给出Validator类的代码之前,有必要提前了解用户是如何向Validateor类发送请求的,这有助于我们知道如何去编写Validator类的代码,如下:
var validataFunc = function(){
var validator = new 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(){
varerrorMsg = validataFunc(); //如果errorMsg有确切的返回值,说明未通过校验
if(errorMsg){
alert(errorMsg);
return false;
}
}
从这段代码中可以看到,我们先创建了一个validator对象,然后通过validator.add方法,往validator对象中添加一些校验规则。validator.add方法接受3个参数,元素、规则、提示信息。
具体实现如下:
var Validator = function(){
this.cache = [];
}
Validator.prototype.add =function(dom,rule,errorMsg){
var ary = rule.split(‘:’);
this.cache.push(function(){
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,validatorFunc;validatorFunc = this.cache[i++]){
var msg=validatorFunc(); //开始校验,并取得校验后的返回信息
if(msg){
return msg;
}
}
}
使用策略模式重构代码之后,我们仅仅通过“配置”的方式就可以完成一个表单的校验,这些校验规则也可以复用在程序的任何地方,还能作为插件的形式,方便地被移植到其它项目中。
在修改某个校验规则的时候,只需要编写或者改写少量的代码。比如我们想将用户名输入框的校验规则改成用户名不能少于4个字符,可以看到,这时候的修改是毫不费力的如下:
validator.add(registerForm.userName,’isNonEmpty’,’用户名不能为空’);
//改成:
validator.add(registerForm.userName,’minLength:10’,’用户名长度不能小于10位’);
为了让读者把注意力放在策略模式的使用上,目前我们的表单校验实现留有一点小遗憾:一个文本输入框只能对应一种校验规则,比如,用户名输入框只能校验输入是否为空:
validator.add(registerForm.userName,’isNonEmpty’,’用户名不能为空’);
如果我们既想校验它是否为空,又想校验它输入文本的长度不小于10怎么办,我们期望以如下的形式进行校验:
validator.add(
registerForm.userName,
[
{strategty:’isNonEmpty’,errorMsg:’用户名不能为空’}
,{strategy:’minLength:6’,errorMsg:’用户名长度不能小于10位’}
]
);
如下:
/*************策略对象************/
var strategies = {
isNonEmpty:function(value,errorMsg){
if(value==””){
return errorMsg;
}
},
minLength:function(value,length,errorMsg){
if(value.length
/*************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);
});
})(rule)
} //end for
};
Validator.prototype.start = function(){
for(var i=0,validatorFunc;validatorFunc = this.cache[i++];){
var errorMsg =validatorFunc();
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.phoneNumber,[
{
strategy:’isMobile’,
errorMsg:’手机号码格式不正确’
}
]);
var errorMsg =validator.start();
return errorMsg;
}
registerForm.onsubmit = function(){
var errorMsg =validataFunc();
if(errorMsg){
alert(errorMsg);
return false;
}
}
策略模式是一种常用且有效的设计模式,本章提供了计算奖金、缓动动画、表单校验这三个例子来加深对策略模式的理解。
优点:
l 有利于组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句
l 提供了对开放----封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,易于理解,易于扩展。
l 策略模式中的算法也可以利用在系统的其他地方,从而避免许多重复的复制粘贴工作
l 在策略模式中利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的替代方案。