JavaScript设计模式系列—模式篇(二)策略模式

转载请注明预见才能遇见的博客:http://my.csdn.net/

原文地址:https://blog.csdn.net/pcaxb/article/details/100516461

JavaScript设计模式系列—模式篇(二)策略模式

目录

JavaScript设计模式系列—模式篇(二)策略模式

1.1 使用策略模式计算奖金

1. 最初的代码实现

2. 使用组合函数重构代码

3. 使用策略模式重构代码

1.2 JavaScript 版本的策略模式

1.3 多态在策略模式中的体现

1.4 使用策略模式实现缓动动画

1.5 更广义的“算法”

1.6 表单校验

1.表单校验的第一个版本

2.用策略模式重构表单校验

3.给某个文本输入框添加多种校验规则

1.7 策略模式的优缺点

1.8 一等函数对象与策略模式

1.9 小结


策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。在程序设计中,我们也常常遇到类似的情况,要实现某一个功能有多种方案可以选择。比如一个压缩文件的程序,既可以选择 zip算法,也可以选择 gzip算法。这些算法灵活多样,而且可以随意互相替换。这种解决方案就是策略模式。

1.1 使用策略模式计算奖金

很多公司的年终奖是根据员工的工资基数和年底绩效情况来发放的。例如,绩效为 S的人年终奖有 4倍工资,绩效为 A的人年终奖有 3倍工资,而绩效为 B的人年终奖是 2倍工资。假设财务部要求我们提供一段代码,来方便他们计算员工的年终奖。

1. 最初的代码实现

//员工的工资数额和他的绩效考核等级
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 ); // 输出:40000
calculateBonus( 'S', 6000 ); // 输出:24000

calculateBonus 函数比较庞大,包含了很多 if-else 语句,这些语句需要覆盖所有的逻辑分支。calculateBonus 函数缺乏弹性,如果增加了一种新的绩效等级 C,或者想把绩效 S 的奖金系数改为 5,那我们必须深入 calculateBonus 函数的内部实现,这是违反开放封闭原则的。算法的复用性差,如果在程序的其他地方需要重用这些计算奖金的算法呢?我们的选择只有复制和粘贴。

2. 使用组合函数重构代码

把各种算法封装到一个个的小函数里面

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 函数有可能越来越庞大,而且在系统变化的时候缺乏弹性。

3. 使用策略模式重构代码

策略模式指的是定义一系列的算法,把它们一个个封装起来。将不变的部分和变化的部分隔开是每个设计模式的主题,策略模式也不例外,策略模式的目的就是将算法的使用与算法的实现分离开来。

一个基于策略模式的程序至少由两部分组成。第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。 第二个部分是环境类 Context,Context接受客户的请求,随后把请求委托给某一个策略类。

//把每种绩效的计算规则都封装在对应的策略类里面
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:

var Bonus = function(){
	this.salary = null; // 原始工资
	this.strategy = null; // 绩效等级对应的策略对象
};
Bonus.prototype.setSalary = function( salary ){
	this.salary = salary; // 设置员工的原始工资
};
Bonus.prototype.setStrategy = function( strategy ){
	this.strategy = strategy; // 设置员工绩效等级对应的策略对象
};
Bonus.prototype.getBonus = function(){ // 取得奖金数额
	return this.strategy.calculate( this.salary ); // 把计算奖金的操作委托给对应的策略对象
};

var bonus = new Bonus();
bonus.setSalary( 10000 );// 设置员工的原始工资

bonus.setStrategy( new performanceS() ); // 设置策略对象
console.log( bonus.getBonus() ); // 输出:40000
bonus.setStrategy( new performanceA() ); // 设置策略对象
console.log( bonus.getBonus() ); // 输出:30000

定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。就是:定义一系列的算法,把它们各自封装成策略类,算法被封装在策略类内部的方法里。在客户对 Context发起请求的时候,Context总是把请求委托给这些策略对象中间的某一个进行计算。

1.2 JavaScript 版本的策略模式

实际上在 JavaScript 语言中,函数也是对象,所以更简单和直接的做法是把 strategy直接定义为函数:

var strategies = {
	"S": function( salary ){
		return salary * 4;
	},
	"A": function( salary ){
		return salary * 3;
	},
	"B": function( salary ){
		return salary * 2;
	}
};
var calculateBonus = function( level, salary ){
	return strategies[ level ]( salary );
};

console.log( calculateBonus( 'S', 20000 ) ); // 输出:80000
console.log( calculateBonus( 'A', 10000 ) ); // 输出:30000

Context 也没有必要必须用 Bonus 类来表示,我们依然用 calculateBonus 函数充当Context来接受用户的请求。

1.3 多态在策略模式中的体现

通过使用策略模式重构代码,我们消除了原程序中大片的条件分支语句。所有跟计算奖金有关的逻辑不再放在 Context中,而是分布在各个策略对象中。Context并没有计算奖金的能力,而是把这个职责委托给了某个策略对象。每个策略对象负责的算法已被各自封装在对象内部。当我们对这些策略对象发出“计算奖金”的请求时,它们会返回各自不同的计算结果,这正是对象多态性的体现,也是“它们可以相互替换”的目的。替换 Context中当前保存的策略对象,便能执行不同的算法来得到我们想要的结果。

 

1.4 使用策略模式实现缓动动画

在 JavaScript中,可以通过连续改变元素的某个 CSS属性,比如 left 、 top 、 background-position 来实现动画效果。

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

<script type="text/javascript">
	//动画已消耗的时间、小球原始位置、小球目标位置、动画持续的总时间,返回的值则是动画元素应该处在的当前位置。
	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;
		}
	};
	
	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 ]; // dom 节点初始位置
		this.propertyName = propertyName; // dom 节点需要被改变的CSS 属性名
		this.endPos = endPos; // dom 节点目标位置
		this.duration = duration; // 动画持续事件
		this.easing = tween[ easing ]; // 缓动算法
		var self = this;
		var timeId = setInterval(function(){ // 启动定时器,开始执行动画
			if ( self.step() === false ){ // 如果动画已结束,则清除定时器
				clearInterval( timeId );
			}
		}, 19 );
	};

	Animate.prototype.step = function(){
	var t = +new Date; // 取得当前时间
	if ( t >= this.startTime + this.duration ){ // (1)
		this.update( this.endPos ); // 更新小球的CSS 属性值
		return false;
	}
	var pos = this.easing( t - this.startTime, this.startPos, this.endPos - this.startPos, this.duration );
	// pos 为小球当前位置
		this.update( pos ); // 更新小球的CSS 属性值
	};

	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' );
</script>

本节我们学会了怎样编写一个动画类,利用这个动画类和一些缓动算法就可以让小球运动起来。我们使用策略模式把算法传入动画类中,来达到各种不同的缓动效果,这些算法都可以轻易地被替换为另外一个算法,这是策略模式的经典运用之一。策略模式的实现并不复杂,关键是如何从策略模式的实现背后,找到封装变化、委托和多态性这些思想的价值。

1.5 更广义的“算法”

策略模式指的是定义一系列的算法,并且把它们封装起来。本章我们介绍的计算奖金和缓动动画的例子都封装了一些算法。从定义上看,策略模式就是用来封装算法的。但如果把策略模式仅仅用来封装算法,未免有一点大材小用。在实际开发中,我们通常会把算法的含义扩散开来,使策略模式也可以用来封装一系列的“业务规则”。只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以用策略模式来封装它们。

1.6 表单校验

用户名不能为空,密码长度不能少于 6位,手机号码必须符合格式。

1.表单校验的第一个版本


<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 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;
		}
	}
</script>

registerForm.onsubmit 函数比较庞大,包含了很多 if-else 语句,这些语句需要覆盖所有的校验规则。registerForm.onsubmit 函数缺乏弹性,如果增加了一种新的校验规则,或者想把密码的长度校验从 6改成 8,我们都必须深入 registerForm.onsubmit 函数的内部实现,这是违反开放 — 封闭原则的。算法的复用性差,如果在程序中增加了另外一个表单,这个表单也需要进行一些类似的校验,那我们很可能将这些校验逻辑复制得漫天遍野。

2.用策略模式重构表单校验

<script type="text/javascript">
	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;
			}
		}
	};

	var validataFunc = 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; // 阻止表单提交
		}
	};

	var Validator = function(){
		this.cache = []; // 保存校验规则
	};

	Validator.prototype.add = function( dom, rule, errorMsg ){
		var ary = rule.split( ':' ); // 把strategy 和参数分开
		this.cache.push(function(){ // 把校验的步骤用空函数包装起来,并且放入cache
			var strategy = ary.shift(); // 用户挑选的strategy
			ary.unshift( dom.value ); // 把input 的value 添加进参数列表
			ary.push( errorMsg ); // 把errorMsg 添加进参数列表
			return strategies[ strategy ].apply( dom, ary );
		});
	};

	Validator.prototype.start = function(){
		for ( var i = 0, validatorFunc; validatorFunc = this.cache[ i++ ]; ){
			var msg = validatorFunc(); // 开始校验,并取得校验后的返回信息
			if ( msg ){ // 如果有确切的返回值,说明校验没有通过
				return msg;
			}
		}
	};

</script>

使用策略模式重构代码之后,我们仅仅通过“配置”的方式就可以完成一个表单的校验,这些校验规则也可以复用在程序的任何地方,还能作为插件的形式,方便地被移植到其他项目中。

3.给某个文本输入框添加多种校验规则

<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>

1.7 策略模式的优缺点

从这三个例子中,我们可以总结出策略模式的一些优点:

策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。

策略模式提供了对开放 — 封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它们易于切换,易于理解,易于扩展。

策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。

在策略模式中利用组合和委托来让 Context拥有执行算法的能力,这也是继承的一种更轻便的替代方案。

当然,策略模式也有一些缺点:

首先,使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比把它们负责的逻辑堆砌在 Context中要好。

其次,要使用策略模式,必须了解所有的 strategy ,必须了解各个 strategy 之间的不同点,这样才能选择一个合适的 strategy 。比如,我们要选择一种合适的旅游出行路线,必须先了解选择飞机、火车、自行车等方案的细节。此时 strategy 要向客户暴露它的所有实现,这是违反最少知识原则的。

1.8 一等函数对象与策略模式

在以类为中心的传统面向对象语言中,不同的算法或者行为被封装在各个策略类中,Context 将请求委托给这些策略对象,这些策略对象会根据请求返回不同的执行结果,这样便能表现出对象的多态性。

Peter Norvig 在他的演讲中曾说过:“在函数作为一等对象的语言中,策略模式是隐形的。strategy 就是值为函数的变量。”在 JavaScript中,除了使用类来封装算法和行为之外,使用函数当然也是一种选择。这些“算法”可以被封装到函数中并且四处传递,也就是我们常说的“高阶函数”。实际上在 JavaScript这种将函数作为一等对象的语言里,策略模式已经融入到了语言本身当中,我们经常用高阶函数来封装不同的行为,并且把它传递到另一个函数中。当我们对这些函数发出“调用”的消息时,不同的函数会返回不同的执行结果。在 JavaScript中,“函数对象的多态性”来得更加简单。

1.9 小结

在 JavaScript语言的策略模式中,策略类往往被函数所代替,这时策略模式就成为一种“隐形”的模式。尽管这样,从头到尾地了解策略模式,不仅可以让我们对该模式有更加透彻的了解,也可以使我们明白使用函数的好处。

 

 JavaScript设计模式系列—模式篇(二)策略模式

博客地址:https://blog.csdn.net/pcaxb/article/details/100516461

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值