定义
定义一系列的算法,把他们一个个封装起来,并且使他们可以互相替换。
实例
年终奖计算
现在我们要开发一个算法,来计算公司年终奖。规则是根据评级不同年终奖的倍数也不同。
let strategies = {
S: (salary) => {
return salary * 4;
},
A: (salary) => {
return salary * 3;
},
B: (salary) => {
return salary * 2;
},
};
let calculateBouns = (level, salary) => {
return strategies[level](salary);
};
console.log(calculateBouns("S", 20000));
console.log(calculateBouns("A", 10000));
实现动画效果
下面我们编写一个动画类,让屏幕里的小球按照我们的规定运动起来。在开始之前,我们可以分析出来本实例至少包括以下信息:
- 动画开始时,小球的原始位置。(从哪来)
- 小球移动的目标位置。(到哪儿去)
- 动画开始时的时间点。(时间)
- 小球运动持续的时间。 随后,我们用setInterval创建一个定时器,每隔19ms循环一次。在定时器的每一帧里我们把各种信息传入缓动算法。该算法就会通过这几个参数,计算出小球当前的位置,最后再对小球的css进行设置。
在实现之前,我们可以了解一下常用的缓动算法,这些算法来自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
<div style="position: absolute; background-color: blue; width: 100px; height: 100px" id="div"></div>
接下来定义Animate类,Animate的构造函数接收一个参数就是div节点,代码如下
class Animate {
constructor(dom) {
if (!dom) {
throw new Error("缺少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方法负责启动这个动画,在启动的同时,我们要记录一些信息,以供更新算法使用。在记录结束后启动定时器。代码如下:
/**
* @description 初始化/启动小球
* @param {String} propertyName CSS属性名,比如'left'、'top'
* @param {Number} endPos
* @param {Number} duration
* @param {String} easing
*/
start(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];
// 启动定时器
let timeId = setInterval(() => {
if (this.step() === false) {
clearInterval(timeId);
}
}, 19);
}
再接下来是step方法,代表小球每一帧要做的事情。
/**
* @description 小球每一帧要做的事情,这就是这个实例的算法核心
*/
step() {
let t = +new Date();
// 为了矫正因为时间结束小球还没有运动到正确的目标位置
if (t >= this.startTime + this.duration) {
this.updata(this.endPos);
return false;
}
// pos为当前小球位置
let pos = this.easing(t - this.startTime, this.startPos, this.endPos - this.startPos, this.duration);
this.updata(pos);
}
在step方法中我们看到有一个判断,目的是在时间结束的时候,有可能小球还没有移动到我们的目标位置,所以我们在时间结束后手动的给他移动过去。同时return false,告诉start清除定时器。 最后是updata更新方法。
/**
* @description 更细小球的css属性
* @param {Number} pos 小球当前位置
*/
updata(pos) {
this.dom.style[this.propertyName] = pos + "px";
}
为了验证,我们可以试验一下。
let animate = new Animate(document.getElementById("div"));
animate.start("left", 1000, 5000, "easeIn");
这样就可以让小球运动起来了。
更广义的“算法”
策略模式指的是定义一系列算法,并把他们封装起来。本篇文章的计算年终奖和让小球缓动起来都封装了一些算法。
从定义上看,策略模式就是封装算法的。但如果只是把策略模式当作封装算法,有点大材小用了。在实际开发中,我们通常会把算法的含义扩散开来,使策略模式也可以封装一系列的“业务规则”。只要这些业务规则的目标一致,并且可以被替换使用,我们就可以使用策略模式来封装。
最后用策略模式封装一个表单校验。
表单校验
可校验多种规则。
<html>
<body>
<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>
<script>
/***********************策略对象**************************/
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( 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;
}
};
</script>
</body>
</html>
总结
优点:
- 策略模式利用组合、委托和多态等技术和思想,可以有效避免多重条件选择语句。
- 策略模式提供了开放-封闭原则的完美支持,将算法封装在独立的方法中,易于切换和扩展。
- 策略模式可以复制在系统的其他地方,避免了复制粘贴。 缺点:
- 会使程序增加很多策略类,但比都写在一个方法里面要好。
- 使用策略模式就必须要了解所有的方法,了解之间的不同,才能选择合适的其中一个方法去使用。就像上面的代码没有仔细看的话甚至不知道可以传入多个校验条件。也好比我们出去旅游,在很多个选择下,我们必须知道飞机、火车、动车的区别,我们才可以更好的安排出行计划。
在JavaScript中,策略模式往往会被函数所替代,毕竟JavaScript是函数作为一等对象的语言,这时策略模式就成为一种“隐形”的模式。尽管这样,从头到尾了解策略模式,不仅可以让我们对该模式有更加透彻的了解,也可以让我们明白使用函数的好处。
参考
JavaScript设计模式与开发实践