《JavaScript设计模式与开发实践》——第五章(策略模式)学习记录

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

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

这段代码非常简单,但是缺点也是显而易见的。

  1. calculateBonus函数比较庞大,包含了很多if-else 语句,这些语句需要覆盖所有的逻辑分支。
  2. calculateBonus函数缺乏弹性,如果增加了一种新的绩效等级C,或者想把S绩效系数改为5,那我们必须深入calculateBonus函数内部实现,这是违反开放-封闭原则的。
  3. 算法复用性差,如果在程序的其他地方需要重用这些计算奖金的算法呢,我们就只能复制粘贴了。

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使用策略模式实现缓动动画

我们的目标是编写一个动画类和一些缓动算法,让小球以各种各样的缓动效果在页面中运动。分析一下在运动开始之前,需要提前记录一些有用的信息:

  1. 动画开始时,小球所在的原始位置;
  2. 小球移动的目标位置;
  3. 动画开始时的准确时间点;
  4. 小球运动持续的时间。
    先来了解一些常见的缓动算法,这些算法都接受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项目中,注册、登录、修改用户信息都离不开提交表单,假如我们正在编写一个注册页面,在点击注册按钮之前,有下面几条逻辑校验:

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

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

最后一起看一下策略模式的优缺点:

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

当然,策略模式也有一些缺点,但这些缺点并不严重。
首先,使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比把它们负责的逻辑堆砌在Context中要好。
其次,要使用策略模式,必须了解所有的strategy,必须了解各个strategy之间的不同点,这样才能选择一个合适的strategy。比如,我们要选择一种合适的旅游出行路线,必须先了解选择飞机,火车,自行车等方案的细节。此时strategy要向客户暴露它的所有实现,这是违反最少知识原则的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值