策略模式
当一种效果有多种实现方法(策略)的时候,可以把这些策略进行随意的替换。
策略模式看起来很不错的原因是他让一些要加很多 if ... else ...
语句改写成了一个对象里的属性,再结合一个函数对这个对象进行操作,告别了一大堆 if ... else ...
。
书中给了一个根据绩效计算奖金的例子,绩效有 S A B 三种,不同的绩效可以获得不同月数的奖金,如何实现“根据绩效计算获得多少奖金”更加优雅。
这个时候最直接的方法就是用一个函数,什么情况就返回什么情况下会获得的奖金:
function calculateBonus = function(绩效,每月工资) {
if (绩效 === 'S') {
return 每月工资 * 4
} else if (绩效 === 'A') {
return 每月工资 * 3
} else if (绩效 === 'B') {
return 每月工资 * 2
}
}
首先让我很不爽的就是函数里一堆的 if...else...
,还有就是如果以后多了个 C 的绩效,还需要直接找出这个函数,在这个函数里面写,这样违反了作者在书中提到的 开放-封闭 原则(还没讲到)。
解决方法就是把每个策略(不同绩效的计算方法)封装成独立的方法,把这些方法放在一个对象里,再来一个函数,根据传入的 绩效和每月工资,调用对象不同的方法,计算后返回:
const strategies = {
"S": function(salary) {
return salary * 4;
}
"A": function(salary) {
return salary * 3;
}
"B": function(salary) {
return salary * 2;
}
};
const calculateBonus = function(level, salary) {
return strategies[level](salary);
}
console.log( calculateBonus('S', 20000) ); // 输出: 80000
console.log( calculateBonus('A', 10000) ); // 输出: 30000
这样的好处就是,如果以后增加了 绩效C,直接strategies.C = function(salary) {}
,要重写绩效 S 的计算方法,可以strategies.S = function(salary)
.
应用场景就是:同时有多种策略(strategies)去做一件事(Context),这个场景中策略就是计算奖金的方法,Context 就是计算奖金(函数calculateBonus)这么个场景。计算奖金的逻辑没有放在Context中,而是分布在策略对象里, Context 做的只是决定要用哪种策略。
实战:使用策略模式实现缓动动画
这个实战的策略模式体现在了将缓动动画的几种策略放在了tween
对象里,动画只需要调用tween
里的方法就能实现缓动效果:
// 缓动公式就是根据 已经开始了多久、开始的位置、将要运动的距离、持续时间
// 计算出下一个要到达的位置
const 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;
}
};
const Animate = function(dom) {
this.dom = dom;
this.startTime = 0;
this.startPos = 0;
this.endPos = 0;
this.propertyName = null; // dom 节点需要被改变的 css 属性名
this.easing = null;
this.duration = null;
}
// start 负责启动动画和定时器
Animate.prototype.start = function(propertyName, endPos, duration, easing) {
this.startTime = +new Date;
this.startPos = this.dom.getBoundingClientRect()[propertyName];
this.propertyName = propertyName;
this.endPos = endPos;
this.duration = duration;
this.easing = tween[easing];
var self = this;
var timeId = setInterval(function() {
if (self.step() === false) {
clearInterval(timeId);
}
}, 19);
};
// 该方法负责计算下一个要到达的位置,然后交给 update() 去更新位置
Animate.prototype.step = function() {
const t = +new Date;
// 如果动画已经结束,return false, 并更新球的位置信息
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);
}
Animate.prototype.update = function(pos) {
this.dom.style[this.propertyName] = pos + 'px';
}
var div = document.getElementById('div');
const animate = new Animate(div);
animate.start('left', 500, 1000, 'strongEaseOut');
这个动画的更大意义其实是在了解一个基础的缓动动画是如何实现的,也能从侧面了解到一些库的调用方法,为什么是将一个元素传入一个构造函数:
- 首先将
Animate
对象实例化,传入一个dom元素 - 调用
start()
,传入需要的四个参数 -
start()
负责将参数记录为自身的属性,并启动定时器,每19ms调用一次step()
-
step()
负责判断运行时间是否超过了规定要完成的时间,如果是,下一个位置就直接到达结束位置,如果不是,就计算出下一个位置,并交给update()
更新元素的位置。 -
update()
修改 dom 的 css 属性让元素“移动”起来
还要一个有意思的点是将step()
和update()
分离了开来。update()
只负责更新 css 属性。
实战:表单校验
利用策略模式做表单校验,前面我们只是用策略模式封装了一些算法,而策略模式当然也可以用来封装我们的业务规则,表单校验就是一个很好的场景。
这里的表单校验实现让我想起了 antd 中的getFieldDecorator
。
表单如下:
<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>
校验策略:
const 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( value ) ){
return errorMsg;
}
}
};
实现一个 Validator 类作为 Context,负责接收用户请求并委托给 strategy 对象。很多库的实现就是如此,暴露出一个对象,我们往这个对象传入参数,实现交给库里的各种“策略”。
作者先站在了用户如何使用 Validator 的角度,模拟了用户的调用方式,这是一种“开发一个库”的思路。要开发一个库前,想想要怎么调用它,再根据调用方式去实现这个库。
如何使用 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() {
var errorMsg = validataFunc();
if (errorMsg) {
alert(errorMsg);
return false;
}
}
可以看到调用的方式为:实例化一个 validator
对象,调用 validator
的add
方法添加校验规则。
第一个参数是要校验的表单项
第二个是要校验的内容
第二个是校验失败的错误信息。
minLength: 6
实现了可以根据不同的长度要求动态地做长度限制的判断。
最后调用start()
来启动校验。
如果 errorMsg
返回的值不为undefined
,说明有表单项校验不通过。onsubmit
返回false
。
Validator 的实现:
var Validator = function() {
// 保存校验规则
this.cache = [];
};
Validator.prototype.add = function(dom, rule, errorMsg) {
var ary = rule.split(':');
this.cache.push(function() {
var strategy = ary.shift();
ary.unshift(dom.value);
ary.push(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;
}
}
}
给每个表单项增加多个校验规则
上面的校验虽然灵活,但是存在一个问题是,每个表单项只允许添加一种校验规则,那么如何让一个表单项可以同时接受多种校验规则呢。那就是把校验规则用对象传入:
validator.add(registerForm.userName, [{
strategy: 'isNonEmpty',
errorMsg: '用户名不能为空'
}, {
strategy: 'minLength: 6',
errorMsg: '用户名长度不能小于10位'
}]);
简直越来越像getFieldDecorator
,getFieldDecorator
是一个函数,支持传入rules,并返回一个新函数,这个函数的参数是表单项组件。
可以实现多个校验规则的Validator对象:
var Validator = function() {
// 保存校验规则
this.cache = [];
};
Validator.prototype.add = function(dom, rule) {
var self = this;
// 遍历 rules,这里用 forEach 更直观
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 msg = validataFunc();
if (msg) {
return msg;
}
}
}
调用方法:
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 位'
}]);
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;
}
};
getFieldDecorator
也是一个校验规则需要配一个message
。
看这本设计模式学到的更多可能不是设计模式,因为一些设计模式可能已经在我们的开发中不知觉地用上了,只是在这本书上发现了设计模式的名字。而是学到一些运用开发模式的技巧。要学会阅读源码,先了解设计模式是很有必要的。