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

第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


  

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’);

5.5 更广义的“算法”

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

5.6 表单校验的第一个版本

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

如下: 

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

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

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

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

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

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

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位’);

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

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

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;
  }
}

5.7 策略模式的优缺点

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

优点:

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

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

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

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

你可能感兴趣的:(JS学习)