【学习笔记javascript设计模式与开发实践(策略模式)----5】

来源:互联网 发布:大数据公司盈利模式 编辑:程序博客网 时间:2024/05/16 15:11

第5章策略模式

 在程序设计中我们往往会遇到实现某一功能有多种方案可以选择。比如一个压缩算法,我们可以选择zip算法,也可以选择gzip算法。

这些算法灵活多样,而且可以随意互相替换。这种解决方案就是本章要讨论的策略模式。

定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

 

5.1 使用策略模式计算奖金

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.    使用策略模式重构代码

策略模式是指定义一系列的算法,把它们一个个封装起来。将不变的部分和变化的部分分隔开是每个设计模式的主题:

 策略模式的目的就是将算法的使用与算法的实现分离开来

在上面的例子里,算法的使用方式是不变的,都是根据某个算法取得计算后资金数额。而算法的实现是各异和变化的,每种绩效对应着不同的计算规则。

因此一个策略模式的程序至少由两部分组成

第一个部分是一组策略类,它封装了具体的算法,并负责具体的计算过程。

第二个部分是环境类ContextContext接受客户请求,随后把请求委托给一个策略类。要做到这一点,说明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  

5.2 javascript版的策略模式

 实际上在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 

5.3 多态在策略模式中的体现

通过使用策略模式重构代码,我们消除了原程序中大片的条件分支语句。所以跟计算奖金有关的逻辑不在放在Context中,而是分布在各个策略对象中。Context并没有计算奖金的能力,而是把这个职责委托给了某个策略对象。

5.4 使用策略模式实现缓动动画

缓动算法,最初是来自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

[html] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. <body>  
  2.   <div style=’position:absolute;background:blue’ id=”div”></div>  
  3. </body>  

[javascript] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. var Animate = function(dom){  
  2.   this.dom = dom;  
  3.   this.startTime = 0;  
  4.   this.startPos = 0;  
  5.   this.endPos = 0;  
  6.   this.propertyName = null;  
  7.   this.easing = null//缓动算法  
  8.   this.duration = null ; //动画持续时间  
  9. }  

接下来Animate.prototype.start方法负责启动这个动画,在动画被启动的瞬间,要记录一些信息,供缓动算法在以后计算当前位置的时候使用(本例中是位置),此方法还负责启动定时器。

[javascript] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. Animate.prototype.start =function(propertyName,endPos,duration,easing){  
  2.   this.startTime = +new Date; //动画启动时间  
  3.   this.startPos = this.dom.getBoundingClientRect()[propertyName];  
  4.   this.propertyName = propertyName; //dom节点需要被改变的CSS属性名  
  5.   this.endPos = endPos; //dom节点目标位置  
  6.   this.duration = duration; //动画持续事件  
  7.   this.easing = tween[easing]; //缓动算法  
  8.   var self = this;  
  9.   var timeId = setInterval(function(){  
  10.   if(self.step()===false){  
  11.    clearInterval(timeId);  
  12.   }  
  13. //调用step  
  14.   },19);  
  15. }  

propertyName:要改变的CSS属性名,如‘left’、‘top’分别表示左右移动和上下移动

endPos:小球运动的目标位置

duration:动画持续时间

easing:缓动算法

 

再接下来是Animate.prototype.step方法,该方法代表小球运动的每一帧要做的事情。Animate.prototype.update是用来负责计算当前位置和更新位置

[javascript] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. Animate.prototype.step = function(){  
  2.   var t = +new Date;  
  3.   if(t>=this.startTime+this.duration){ //(1)  
  4.   this.update(this.endPos);  
  5.      return false;  
  6.    }  
  7.   var pos =this.easing(t-this.startTime,this.startPos,this.endPos-this.startPos,this.duration);  
  8.   this.update(pos);  
  9. }  
 

(1)注释的意思,如果当前时间大于开始时间加上动画持续时间之和,说明动画已经结束,此时要修正小球的位置。主要用于修正最终的目标位置。 

负责更新CSS属性值的Animate.prototype.update方法:

[javascript] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. Animate.prototype.update = function(pos){  
  2.    this.dom.style[this.prototypeName]= pos+”px”;  
  3. }  

可以验证结果:

[javascript] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. var div = document.getElementById(‘div’);  
  2. var animate = new Animate(div);  
  3. animate.start(‘left’,500,1000,’strongEaseOut’);  
  4. //animate.start(‘top’,1500,500,’strongEaseIn’);  

.5 更广义的“算法”

从定义上看,策略模式就是用来封装算法的。但如果把策略模式仅仅用来封闭算法,未免大材小用。在实际开发中,我们通常会把算法的含义扩散开来,使策略模式也可以用来封装一系列“业务规则”。只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以用策略模式来封装它。

5.6 表单校验的第一个版本

提交表单数据,在数据交给后台之前,常常要做的一些客户端力所能及的校验工作,比如注册的时候需要校验是否填写了用户名,密码长度等等。这样可以避免因为提交不合法数据而带来的不必要网络开销。

如下: 

[javascript] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. <script>  
  2. var registerForm= document.getElementById(‘registerForm’);  
  3. registerForm.onsubmit= function(){  
  4.   if(registerForm.userName.value===’’){  
  5.      alert(‘用户名不能为空’);  
  6.   }  
  7.   if(registerForm.password.value.length<6){  
  8.     alert(‘密码长度不能少于6位’);  
  9.   }  
  10.  if(!/^1[3|5|8][0-9]{9}$/.test(registerForm.phoneNumber.value)){  
  11.     alert(‘手机号码格式不正确’);  
  12.     return false;  
  13.   }  
  14. }  
  15. </script>  

这是一种常见的代码编写方式,它的缺点跟计算资金的最初版本一模一样。

registerForm.onsubmit函数比较庞大,包含了很多if-else语句,这些语句需要覆盖所有的校验规则

registerForm.onsubmit函数缺乏弹性,如果增加了一种新的校验规则,或者想把密码的长度从6改成8,我们必须深入registerForm.onsubmit函数的内部实现,这是违反开放—封闭原则的

算法的复用性差,如果在程序中增加另一个表单,这个表单也需要进行一些类似的校验,那我们很可能将这些校验逻辑复制得漫天野。

5.6.2 用策略模式重构表单校验

下我们将用策略模式来重构表单校验,第一步我们要把校验逻辑都封装成策略对象:

[javascript] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. var strategies = {  
  2.  isNonEmpty:function(value,errorMsg){//不为空  
  3.    if(value=’’){  
  4.     return errorMsg;  
  5.   }  
  6.  },  
  7.  minLength:function(value,length,errorMsg){  
  8.    if(value.length<length){  
  9.      return errorMsg  
  10.    }  
  11.  },  
  12.  isMobile:function(value,errorMsg){  
  13.   if(!/^1[3|5|8][0-9]$/.test(value)){  
  14.      return errorMsg;  
  15.   }  
  16.  }  
  17. }  

接下来我们来准备一个Validator类。它用来做为Context,负责接收用户的请求并委托给strategy对象。在给出Validator类的代码之前,有必要提前了解用户是如何向Validateor类发送请求的,这有助于我们知道如何去编写Validator类的代码,如下:

[javascript] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. var validataFunc = function(){  
  2.   var validator = new Validator();  
  3.   validator.add(registerForm.userName,’isNonEmpty’,’用户名不能为空’);  
  4.   validator.add(registerForm.password,’minLength:6’,’密码长度不能少于6位’)  
  5.   validator.add(registerForm.phoneNumber,’isMobile’,’手机号码格式不正确’);  
  6.     
  7.   var errorMsg = validator.start();  
  8.   return errorMsg;  
  9. }  
  10.    
  11. var registerForm = document.getElementById(“registerForm”);  
  12. registerForm.onsubmit = function(){  
  13.   varerrorMsg = validataFunc(); //如果errorMsg有确切的返回值,说明未通过校验  
  14.   if(errorMsg){  
  15.     alert(errorMsg);  
  16.     return false;  
  17.   }  
  18. }<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>  

从这段代码中可以看到,我们先创建了一个validator对象,然后通过validator.add方法,往validator对象中添加一些校验规则。validator.add方法接受3个参数,元素、规则、提示信息。


从这段代码中可以看到,我们先创建了一个validator对象,然后通过validator.add方法,往validator对象中添加一些校验规则。validator.add方法接受3个参数,元素、规则、提示信息。

具体实现如下:

[javascript] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. var Validator = function(){  
  2.   this.cache = [];  
  3. }  
  4. Validator.prototype.add =function(dom,rule,errorMsg){  
  5.   var ary = rule.split(‘:’);  
  6.   this.cache.push(function(){  
  7.      var strategy =ary.shift();//用户挑选的strategy  
  8.      ary.unshift(dom.value);//把input的value添加进参数列表  
  9.      ary.push(errorMsg);//把errorMsg添加进参数列表  
  10.      return strategies[strategy].apply(dom,ary);  
  11.   });  
  12. };  
  13.    
  14. Validator.prototype.start = function(){  
  15.  for(var i=0,validatorFunc;validatorFunc = this.cache[i++]){  
  16.      var msg=validatorFunc(); //开始校验,并取得校验后的返回信息  
  17.      if(msg){  
  18.        return msg;  
  19.      }  
  20.   }  
  21. }  

使用策略模式重构代码之后,我们仅仅通过“配置”的方式就可以完成一个表单的校验,这些校验规则也可以复用在程序的任何地方,还能作为插件的形式,方便地被移植到其它项目中。

在修改某个校验规则的时候,只需要编写或者改写少量的代码。比如我们想将用户名输入框的校验规则改成用户名不能少于4个字符,可以看到,这时候的修改是毫不费力的如下:

[javascript] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. validator.add(registerForm.userName,’isNonEmpty’,’用户名不能为空’);  
  2. //改成:  
  3. validator.add(registerForm.userName,’minLength:10’,’用户名长度不能小于10位’);  

5.6.3 给某个文本输入框添加多种校验规则

为了让读者把注意力放在策略模式的使用上,目前我们的表单校验实现留有一点小遗憾:一个文本输入框只能对应一种校验规则,比如,用户名输入框只能校验输入是否为空:

validator.add(registerForm.userName,’isNonEmpty’,’用户名不能为空’);

如果我们既想校验它是否为空,又想校验它输入文本的长度不小于10怎么办,我们期望以如下的形式进行校验:

<pre name="code" class="javascript">validator.add(  registerForm.userName,   [  {strategty:’isNonEmpty’,errorMsg:’用户名不能为空’},
 {strategy:’minLength:6’,errorMsg:’用户名长度不能小于10位’}    ]   );  

如下:

[html] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. <html>  
  2. <body>  
  3.    <form action=”” id = “registerForm”method=”post”>  
  4.        请输入用户名:<input type=’text’ name=’userName’ />  
  5.        请输入密码:<input type=’text’ name = ‘password’ />  
  6.        请输入手机号码:<input type=’text’ name = ‘phoneNumber’ />  
  7.   </form>  
  8. </body>  
  9. </html><span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>  

/*************策略对象************/

[javascript] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. var strategies = {  
  2.    isNonEmpty:function(value,errorMsg){  
  3.          if(value==””){  
  4.                return errorMsg;  
  5.          }  
  6.    },  
  7.    minLength:function(value,length,errorMsg){  
  8.         if(value.length<length){  
  9.               return errorMsg;  
  10.         }  
  11.    },  
  12.    isMobile:function(value,errorMsg){  
  13.        if(!/(^1[3|5|8][0-9]{9}$)/.test(value)){  
  14.              return errorMsg;  
  15.        }  
  16.    }  
  17.    
  18. }  

/*************Validator************/

[javascript] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. var Validator = function(){  
  2.   this.cache= [];  
  3. }  
  4. validator.prototype.add =function(dom,rules){  
  5.   var self = this;  
  6.   for(var i=0,rule;rule = rules[i++]){  
  7.      (function(rule){  
  8.          var strategyAry = rule.strategy.split(‘:’);  
  9.          var errorMsg = rule.errorMsg;  
  10.          self.cache.push(function(){  
  11.               var strategy = strategyAry.shift();  
  12.               strategyAry.unshift(dom.value);  
  13.               strategyAry.push(errorMsg);  
  14.          });  
  15.     })(rule)  
  16.   } //end for  
  17. };  
  18.    
  19. Validator.prototype.start = function(){  
  20.   for(var i=0,validatorFunc;validatorFunc = this.cache[i++];){  
  21.       var errorMsg =validatorFunc();  
  22.       if(errorMsg){  
  23.          return errorMsg;  
  24.       }  
  25.    }  
  26. }  

/****************客户调用代码*****************/

[javascript] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. var registerForm = document.getElementById(‘registerForm’);  
  2. var validataFunc = function(){  
  3.     var validator =new Validator();  
  4.     validator.add(registerForm.username,[  
  5.                   {  
  6.                      strategy:’isNonEmpty’,  
  7.                      errorMsg:’用户名不能为空’  
  8.                   },  
  9.                   {  
  10.                      strategy:’minLength:10’,  
  11.                      errorMsg:’用户名长度不能小于10’  
  12.                   }  
  13.      ]);  
  14.     validator.add(registerForm.password,[  
  15.                  {  
  16.                      strategy:’minLength:6’,  
  17.                      errorMsg:’密码长度不能小于6’  
  18.                  }  
  19.      ]);  
  20.    validator.add(registerForm.phoneNumber,[  
  21.                 {  
  22.                    strategy:’isMobile’,  
  23.                    errorMsg:’手机号码格式不正确’  
  24.                 }  
  25.      ]);  
  26.    
  27.    var errorMsg =validator.start();  
  28.    return errorMsg;  
  29. }  
  30.    
  31. registerForm.onsubmit = function(){  
  32. var errorMsg =validataFunc();  
  33.   if(errorMsg){  
  34.     alert(errorMsg);  
  35.     return false;  
  36.   }  
  37. }  

5.7 策略模式的优缺点

策略模式是一种常用且有效的设计模式,本章提供了计算奖金、缓动动画、表单校验这三个例子来加深对策略模式的理解。

优点:

l   有利于组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句

l   提供了对开放----封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,易于理解,易于扩展。

l   策略模式中的算法也可以利用在系统的其他地方,从而避免许多重复的复制粘贴工作

l   在策略模式中利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的替代方案。 






0 0