JavaScript设计模式笔记-策略模式

定义

策略模式的定义是:定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换.通俗来讲就是用一个盒子装很多颗糖果 如果有需要了那么让一个人去拿 这样如果我们想增加不同的糖果只需要往盒子里面放就行了

5.1 使用策略模式计算奖金

绩效S的人年终奖有4倍工资,A的是3倍,B的是2倍

1.最初代码的实现

var calculateBonus=function(per,salary){ 
   if(per==='S'){ 
       return salary*4 
   } 
    if(per==='A'){ 
       return salary*3 
   } 
    if(per==='B'){ 
       return salary*2 
   } 
} 
   calculateBonus( 'B', 20000 ); // 输出:40000 
   calculateBonus( 'S', 6000 ); // 输出:24000 

这样写的话.如果要增加绩效C的话 需要再写一个if else分支. 前文说了..设计模式就是为了消除这一大堆分支所出现的.

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 ); 
   } 
}; 
calculateBonus( 'A' , 10000 ); // 输出:30000 

这样虽然得到一定的改善了.那么calculateBonus越来越大 if分支也会越来越多

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; 
   }; 
//定义奖金类 
   var Bonus=function(){ 
       this.salary=null;//原始工资 
       this.strategy=null;//对应的策略类,也就是Context要维持对某个策略对象的引用 
   } 
   Bonus.prototype.setSalary=function(salary){ 
       this.salary=salary 
   } 
   Bonus.prototype.setStrategy=function(strategy){ 
       this.strategy=strategy 
   } 
   Bonus.prototype.getBonuns=function(){ 
       return this.strategy.calculate(this.salary) 
   } 

定义一系列算法,把它们各自封装成策略类,算法被封装在策略类内部的方法里.在客户对Context发起请求的时候,Context总是把请求委托给这些策略对象中间的某一个进行计算.那么基于上述的话,我们完成剩下的代码, 创建一个策略类,然后对Bonus也就是Context发起请求,Bonus会把这个请求委托给策略类中的某个算法

var bonus=new Bonus(); 
bonus.setStrategy( new performanceS() ); // 设置策略对象 
console.log( bonus.getBonus() ); // 输出:40000 
bonus.setStrategy( new performanceA() ); // 设置策略对象 
console.log( bonus.getBonus() ); // 输出:30000 

5.2 JavaScript中的策略模式

var strategies = { 
   "S": function( salary ){ 
       return salary * 4; 
   }, 
   "A": function( salary ){ 
       return salary * 3; 
   }, 
   "B": function( salary ){ 
       return salary * 2; 

   } 
}; 

Context并没有要求要用类来表示,那么可以用函数来实现

var calculateBonus=function(level,salary){ 
   return strategies[level](salary) 
} 
console.log( calculateBonus( 'S', 20000 ) ); // 输出:80000 
console.log( calculateBonus( 'A', 10000 ) ); // 输出:30000 

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

在使用策略模式重构时,我们消除了原程序中大片的ifelse.所有跟奖金计算有关的逻辑都放在策略类中,Context仅仅是遵守单一职责,把这些请求委托给策略类.

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

5.4.1 实现动画效果的原理

实际上就是更改元素的CSS如 left top background-position.

5.4.2 思路和一些准备工作

我们的目标是编写一个动画类和一些缓动算法,让小球以各种各样的缓动效果在页面中运动在运动开始之前,我们得获取一些信息

  • 动画开始时候,小球所在的原始位置
  • 小球移动的目标位置
  • 动画开始时的准确时间点
  • 小球运动持续的时间

随后我们会用setInterval创建一个定时器,每隔19ms执行一次,执行的时候会通过算法更新小球运动的信息.

5.4.3 让小球运动起来

首先我们可以从flash移植缓动算法封装成策略类t:动画已经消耗的时间b:小球原始位置c:小球目标位置d:动画持续的总时间返回的是小球应该处于的当前位置

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

编写HTML

<body> 
   <div style="position:absolute;background:blue" id="div">我是div</div> 
</body> 

随后我们来定义一个animate类也就是Context 由Context来委托请求

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];//这个方法返回一个矩形对象,包含四个属性:left、top、right和bottom。分别表示元素各边与页面上边和左边的距离。 
   this.propertyName=propertyName;//dom节点需要被改变的css属性名 
   this.endPos=endPos;//动画结束位置 
   this.duration=duration;//动画持续时间 
   this.easing=tween[easing];//缓动算法 

   var self=this; 
   var timeId=setInterval(function(){ 
       if(self.step()===false){ 
           clearInterval(timeId) 
       } 
   },19) 
} 

  • propertyName:需要改变的css
  • endPos:结束位置
  • duration:持续时间
  • easing:缓动算法

接下来定义step方法

Animate.prototype.step=function(){ 
   var t=+new Date;//获取当前时间 
   if(t>=this.startTime+this.duration){ //如果动画已经执行完毕 也就是持续时间 
       this.update(this.endPos);//为了防止有一些位置没有移动 
       return false 
   }; 
   var pos=this.easing(t-this.startTime,this.startPos,this.endPos-this.startPos,this.duration); 
   this.update(pos) 
} 

接下来定义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( 'top', 1500, 500, 'strongEaseIn' ); 

策略模式实现并不复杂,关键是如何从策略模式的实现背后,找到封装变化,委托和多态性这些思想的价值.

5.5 更广义的算法

从定义上看策略模式是封装算法的.但是在实际开发中,使用策略模式也可以用来封装一系列的’业务规则’,只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以用策略模式来封装它们

5.6 表单验证

比如我们需要实现以下几条验证

  • 用户名不为空
  • 密码长度不能少于6位
  • 手机号码必须符合格式

5.6.1表单校验的第一个版本

未引入策略模式

    <form action="http:// xxx.com/register" 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; 
           } 
       } 

首先看到一大堆ifelse那么基本上可以断定这个程序是有被重构的余地的.

5.6.2策略模式重构代码

首先创建一个策略类用来保存业务逻辑

var strategies={ 
   isNonEmpty:function(value,errorMsg){//不为空 

   }, 
   minLength: function( value, length, errorMsg ){ // 限制最小长度 
       if ( value.length < length ){ 
           return errorMsg; 
           82 第5 章 策略模式 
       } 
   }, 
   isMobile: function( value, errorMsg ){ // 手机号码格式 
       if ( !/(^1[3|5|8][0-9]{9}$)/.test( value ) ){ 
           return errorMsg; 
       } 
   } 
} 

其次我们实现Context也就是Validator类,负责接收用户的请求并且委托给strategy对象.

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);//自定义错误消息 
       return strategies[strategy].apply(dom,ary);//策略类中的this指向的是dom元素 
   }) 
} 
Validator.prototype.start=function(){ 
   for(var i=0,validatorFunc;validatorFunc=this.cache[i++];){ 
       var msg=validatorFunc();//开始校验,并且取得校验后的结果 
       if(msg){ //如果有错误信息那么就没校验通过,返回错误信息 
           return msg 
       } 
   } 
} 

执行和添加校验规则

var validateFunc=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; // 阻止表单提交 
   } 
}; 

首先这个策略模式 定义了策略类也就是校验规则 其实用了一个对象Context委托请求给策略类 也就是Validator函数 至于validateFunc这个函数是用来启动策略模式的.

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

如果我们期望一个文本框用户名不能为空,且它输入的长度不小于10呢?我们期望这样

validator.add( registerForm.userName, [{ 
   strategy: 'isNonEmpty', 
   errorMsg: '用户名不能为空' 
}, { 
   strategy: 'minLength:6', 
   errorMsg: '用户名长度不能小于10 位' 
}]); 

实际要改写也很简单 我们只需要改动Context委托对象,也就是Valiprototype.add函数

Validator.prototype.add=function(dom,rules){ 
   var self=this; 
   for(var i=0;rule;rule=rules[i++]){ 
       (function(rule){ 
           var ary=rule.strategy.split(':');//通过:来获取参数 
           var errorMsg=rule.errorMsg; 
           this.cache.push(function(){//把校验的步骤用空函数包装起来 
               var strategy=ary.shift();//用户挑选的strategy 
               ary.unshift(dom.value);//把input的value添加进参数列表 
               ary.push(errorMsg);//自定义错误消息 
               return strategies[strategy].apply(dom,ary);//策略类中的this指向的是dom元素 
           }) 
       })(rule) 
   } 
} 

只需要加一层闭包添加多重验证就okay了.完整代码参考

/***********************策略对象**************************/ 
var strategies = { 
   isNonEmpty: function( value, errorMsg ){ 
       console.log(277,this) 
       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 类**************************/ 
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, 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:6', 
       errorMsg: '用户名长度不能小于10 位' 
   }]); 
   validator.add( registerForm.password, [{ 
       strategy: 'minLength:6', 
       errorMsg: '密码长度不能小于6 位' 
   }]); 
   var errorMsg = validator.start(); 
   return errorMsg; 
} 
registerForm.onsubmit = function(){ 
   var errorMsg = validataFunc(); 
   if ( errorMsg ){ 
       alert ( errorMsg ); 
       return false; 
   } 

}; 

5.7策略模式的优缺点

  • 策略模式利用组合,委托和多态等技术和思想,可以有效的避免多重条件选择语句.
  • 策略模式提供了对外开发-封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,易于理解,易于扩展
  • 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作
  • 在策略模式中利用组合和委托来让Context拥有执行算法能力,这也是继承的一种更轻便的代替方案

当然策略模式也有一些缺点,但并不严重首先使用策略模式会增加许多策略类火灾策略对象,但实际上这比把它们负责的逻辑堆砌在Context中要好

5.8 一等函数对象与策略模式

var S=function(){ 
   return salary*4 
} 
var A=function(){ 
   return salary*3 
} 
var B=function(){ 
   return salary*2 
} 
var calculateBonus=function(func,salary){ 
   return func(salary) 
} 
calculateBonus(S,10000)