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

定义

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

5.1 使用策略模式计算奖金

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

1.最初代码的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
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.使用组合函数重构代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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语言来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//定义策略类
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会把这个请求委托给策略类中的某个算法

1
2
3
4
5
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中的策略模式

1
2
3
4
5
6
7
8
9
10
11
12
var strategies = {
"S": function( salary ){
return salary * 4;
},
"A": function( salary ){
return salary * 3;
},
"B": function( salary ){
return salary * 2;
}
};

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

1
2
3
4
5
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:动画持续的总时间
返回的是小球应该处于的当前位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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方法

1
2
3
4
5
6
7
8
9
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方法

1
2
3
4
5
6
7
8
9
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表单校验的第一个版本

未引入策略模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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策略模式重构代码

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var strategies={
isNonEmpty:function(value,errorMsg){//不为空
},
minLength: function( value, length, errorMsg ){ // 限制最小长度
if ( value.length < length ){
return errorMsg;
825 章 策略模式
}
},
isMobile: function( value, errorMsg ){ // 手机号码格式
if ( !/(^1[3|5|8][0-9]{9}$)/.test( value ) ){
return errorMsg;
}
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
}
}
}

执行和添加校验规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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呢?我们期望这样

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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了.完整代码参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/***********************策略对象**************************/
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 一等函数对象与策略模式

1
2
3
4
5
6
7
8
9
10
11
12
13
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)