本文共 11311 字,大约阅读时间需要 37 分钟。
在程序设计中我们往往会遇到实现某一功能有多种方案可以选择。比如一个压缩,我们可以选择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
实际上在语言中,函数也是对象,所以更简单和直接的做法是把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; } };
以下代码思想来源于库,由于本节内容是策略模式,而非编写一个完整的动画库,因此我们省去了动画的队列控制等更多完整功能。
定义一个div
接下来Animate.prototype.start方法负责启动这个动画,在动画被启动的瞬间,要记录一些信息,供缓动算法在以后计算当前位置的时候使用(本例中是位置),此方法还负责启动定时器。
propertyName:要改变的CSS属性名,如‘left’、‘top’分别表示左右移动和上下移动
endPos:小球运动的目标位置
duration:动画持续时间
easing:缓动算法
再接下来是Animate.prototype.step方法,该方法代表小球运动的每一帧要做的事情。Animate.prototype.update是用来负责计算当前位置和更新位置
(1)注释的意思,如果当前时间大于开始时间加上动画持续时间之和,说明动画已经结束,此时要修正小球的位置。主要用于修正最终的目标位置。
负责更新CSS属性值的Animate.prototype.update方法:
可以验证结果:
从定义上看,策略模式就是用来封装算法的。但如果把策略模式仅仅用来封闭算法,未免大材小用。在实际开发中,我们通常会把算法的含义扩散开来,使策略模式也可以用来封装一系列“业务规则”。只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以用策略模式来封装它。
提交表单数据,在数据交给后台之前,常常要做的一些客户端力所能及的校验工作,比如注册的时候需要校验是否填写了用户名,密码长度等等。这样可以避免因为提交不合法数据而带来的不必要网络开销。
如下:
这是一种常见的代码编写方式,它的缺点跟计算资金的最初版本一模一样。
registerForm.onsubmit函数比较庞大,包含了很多if-else语句,这些语句需要覆盖所有的校验规则
registerForm.onsubmit函数缺乏弹性,如果增加了一种新的校验规则,或者想把密码的长度从6改成8,我们必须深入registerForm.onsubmit函数的内部实现,这是违反开放—封闭原则的
算法的复用性差,如果在程序中增加另一个表单,这个表单也需要进行一些类似的校验,那我们很可能将这些校验逻辑复制得漫天野。
下我们将用策略模式来重构表单校验,第一步我们要把校验逻辑都封装成策略对象:
接下来我们来准备一个Validator类。它用来做为Context,负责接收用户的请求并委托给strategy对象。在给出Validator类的代码之前,有必要提前了解用户是如何向Validateor类发送请求的,这有助于我们知道如何去编写Validator类的代码,如下:
从这段代码中可以看到,我们先创建了一个validator对象,然后通过validator.add方法,往validator对象中添加一些校验规则。validator.add方法接受3个参数,元素、规则、提示信息。
从这段代码中可以看到,我们先创建了一个validator对象,然后通过validator.add方法,往validator对象中添加一些校验规则。validator.add方法接受3个参数,元素、规则、提示信息。
具体实现如下:
使用策略模式重构代码之后,我们仅仅通过“配置”的方式就可以完成一个表单的校验,这些校验规则也可以复用在程序的任何地方,还能作为插件的形式,方便地被移植到其它项目中。
在修改某个校验规则的时候,只需要编写或者改写少量的代码。比如我们想将用户名输入框的校验规则改成用户名不能少于4个字符,可以看到,这时候的修改是毫不费力的如下:
为了让读者把注意力放在策略模式的使用上,目前我们的表单校验实现留有一点小遗憾:一个文本输入框只能对应一种校验规则,比如,用户名输入框只能校验输入是否为空:
validator.add(registerForm.userName,’isNonEmpty’,’用户名不能为空’);
如果我们既想校验它是否为空,又想校验它输入文本的长度不小于10怎么办,我们期望以如下的形式进行校验:
validator.add( registerForm.userName, [ {strategty:’isNonEmpty’,errorMsg:’用户名不能为空’},
{strategy:’minLength:6’,errorMsg:’用户名长度不能小于10位’} ] );
如下:
/*************策略对象************/
/*************Validator类************/
/****************客户调用代码*****************/
策略模式是一种常用且有效的设计模式,本章提供了计算奖金、缓动动画、表单校验这三个例子来加深对策略模式的理解。
优点:
l 有利于组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句
l 提供了对开放----封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,易于理解,易于扩展。
l 策略模式中的算法也可以利用在系统的其他地方,从而避免许多重复的复制粘贴工作
l 在策略模式中利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的替代方案。
转载地址:http://ogzxi.baihongyu.com/