JavaScript设计模式之策略模式

201 阅读5分钟

定义

定义一系列的算法,把他们一个个封装起来,并且使他们可以互相替换。

实例

年终奖计算

现在我们要开发一个算法,来计算公司年终奖。规则是根据评级不同年终奖的倍数也不同。

  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设计模式与开发实践