策略模式的定义:定义一系列的算法,把它们意义封装起来,并且使他们可以相互替换。
策略模式计算奖金
奖金发放的场景:绩效为S的人年终奖为4倍工资,绩效为A的年终奖为3倍工资,绩效为B的年终奖为2倍工资,那我们的编程的代码。
最初代码的实现
var calculateBouns = function(performanceLevel, salary){
if(performanceLevel === 'S'){
return salary * 4;
}
if(performanceLevel === 'A'){
return salary*3;
}
if(performanceLevel === 'B'){
return salary*2;
}
};
calculateBouns ('B', 2000); // 4000
calculateBouns ('S', 6000); // 24000
现在的这段代码十分简单,但是存在很多的显而易见的问题。
- 如果有很多的等级的话,就会有很多的
if-else
的语句,这样的语句需要覆盖整个的逻辑分支。 - 该函数缺乏弹性,如果增加一个等级C的话,或是将S的系数进行更改的话,必须要进入到函数的内部进行实现,违反了开放-封闭原则。
- 算法的复用性很差,如果在其他的地方进行重复的逻辑,我们只能进行复制粘贴,不能直接进行代码的复用。
由于上面的代码存在很大的问题,所以我们决定对代码进行重构。
使用组合函数进行代码的重构
我们最容易想到的办法是通过组合函数进行代码的重构,,我们把各种算法封装到一个小的函数中,这些小的函数都有良好的命名,可以一目了然的知道用到了什么算法。还是上面的使用场景,进行代码的优化:
var performanceLevelS = function(){
return salary*4;
}
var performanceLevelA = function(){
return salary*3;
}
var performanceLevelB = function(){
return salary*2;
}
var calculateBouns = function(performanceLevel, salary){
if(performanceLevel === 'S'){
return performanceLevelS;
}
if(performanceLevel === 'A'){
return performanceLevelA;
}
if(performanceLevel === 'B'){
return performanceLevelB;
}
};
calculateBouns ('B', 2000); // 4000
上面的写法我们在一定的程度上解决了一定的问题,但是这样的改善是十分的有限的,当我们的逻辑很复杂的时候,calculateBouns
函数可能越来越大,而且系统的变化的时候,弹性有限。
使用策略模式重构代码
我们想到可以使用策略模式进行重构代码,策略模式的意义就是:定义一系列的算法,并将这些算法封装起来,把不变的部分和变化的部分分割开来是每一个设计模式的主题,策略模式也是不例外的,策略模式的目的就是将算法的使用和实现分割开来。
在例子中,我们注意到了,算法使用的方式是不变的,都是根据某个算法取得计算后的的奖金的金额,而且算法的实现是各异和变化的,不同的绩效对应着不同的计算公式。
一个基于策略模式的程序至少要有两个部分,第一部分是策略类:封装了具体的算法,并且负责具体计算的部分;第二部分是环境类context,用来接收客户的请求,然后将请求委托给某一个策略类。所以context
中要维系对于某个策略对象的引用。
首先使用策略类模仿传统的面向对象的实现,将绩效的计算规则封装到对应的策略类里面
var performanceLevelS = function(){};
performanceLevelS.prototype.calculate = function(salary){
return salary*4;
}
var performanceLevelA = function(){};
performanceLevelA.prototype.calculate = function(salary){
return salary*3;
}
var performanceLevelB = function(){};
performanceLevelB.prototype.calculate = function(salary){
return salary*2;
}
定义一下奖金类:
var Bonus = function(){
this.strategy = null;
this.salary = 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);
}
我们在看一下策略模式的思想:定义一系列的算法,把它们封装起来,并且使他们可以相互进行替换。我们再暂看说一下:定义一系列的算法,把它们各自封装成策略类,算法封装在策略类内部的方法里,在客户端对context
发起请求,Context
总是把请求委托给这些策略对象中间的某一个进行计算。
至于如何完成剩下的代码,我们首先先创建一个bonus
对象,并且给bonus对象设置一些原始的数据,然后呢,把某个计算奖金的策略对象也传入到bonus
对象内部并进行保存,当调用bonus.getBonus
的时候,bonus
对象本身并没有能力去计算,而是把请求委托给之前保存好的策略对象。
var bonus = new Bonus();
bonus.setSalary(1000); // 设置初始工资
bonus.setStrategy (new performanceLevelA ()); //设置策略
console.log(bonus.getBonus());
针对JavaScript版本的策略模式
上面的代码我们是模仿在传统的面向对象语言来进行实现的,实际上在JavaScript
语言中,函数也是对象,所以最简单最直接的方法是把strategy
指定定义为函数。
var strategis = {
"S":function(salary){
return salary * 4;
}
"A":function(salary){
return salary * 3;
}
"B":function(salary){
return salary * 2;
}
};
同样,context
也没有必要必须用Bonus
类来标识,我们依然使用calculateBonus
函数充当context
来接受用户的请求,经过改造,代码的结结构会更加简洁。
var strategis = {
"S":function(salary){
return salary * 4;
}
"A":function(salary){
return salary * 3;
}
"B":function(salary){
return salary * 2;
}
};
var calculateBonus = function(level, salary){
console.log(calculateBonus('S'),20000) // 80000
}
我们通过策略模式来进行代码的重构,我们消除了原程序中大片的条件分支语句。多有的有关计算的逻辑不放在context
中,而是分布在策略对象中,context
没有计算的能力,而是将这个职责委托给了某个策略对象。每一个策略对象负责的算法被封装在各自的对象内部,有一点向传统的多态,因此各个策略对象彼此之前是可以互换的。
表单验证
我们假设写一个注册的页面,在点击注册按钮之前,我们添加几条校验的逻辑。
- 用户名不能为空
- 密码长度不能小于6位
- 手机号码必须符合格式
当我们还没有引入策略模式的时候,我们的代码是下面这样的:
<form 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]{9}$)/.test(registerForm.phoneNumber.value) ){
alert('手机号码格式不正确');
return false;
}
}
这是最简单最常见的编码方式,但是它的缺点和计算奖金最开始的模板是一样的
-
registerForm.onsubmit
函数比较庞大,尤其是我们需要校验的表格比较负责的时候,包含了很多的if-else
语句,这些语句需要覆盖现在所有的校验规则 -
registerForm.onsubmit
函数缺乏弹性,如果增加新的校验规则,或者是想要将6为密码修改为8位密码,我们必须深入到函数的内部进行实现,违反了开闭原则 - 算法的复用性很差,如果还有一个表单需要校验,我们几乎要将这个逻辑完全的复制。
策略模式的表单验证
var strategies = {
isNotEmpty: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]{9}$)/.test(value)){
return errorMsg;
}
}
}
将各个算法进行简单的封装,为后续的调用提供方便,现在我们准备实现validator
类作为context
,负责接收用户的请求并委托给strategy
对象。
var validataFunc = function(){
var validator = new Validator();
//添加一些校验规则
validator.add(registerForm.userName, 'isNotEmpty', '用户名不为空');
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(){ //如果errorMsg有返回值,说明没有通过校验
if(errorMsg){
alert(errorMsg);
return false;//阻止表单的提交
}
}
在代码中,我们先创建一个validator
对象,然后通过validator.add
方法向validator
对象中添加一些校验规则。validator.add
方法接受3个参数,用下面的这段来吗来进行说明。validator.add(registerForm.password, 'minLength:6', '密码长度不能小于6位');
-
registerForm.password
为参加校验的input的输入框 -
minLength:6
用冒号进行分隔的字符串,冒号前是算法的名字,冒号后面是我们要传入的参数,如果不需要额外的信息,就不用添加冒号,直接传入算法的名字 - 第三个参数:当校验未通过的时候返回的错误信息
当我们向validator
对象里添加完一系列的的校验规则之后,会调用validator.start()
方法来启动校验,当该方法返回一个确切的errorMsg
的值的时候,这个时候onsubmit
方法会返回false
来阻止表单的提交。让我们最后来构建Validator
类的实现
var Validator = function (){
this.cache = []; //检验校验的规则
}
Validator.prototype.add = function(dom, rule, erroMsg){
for(var i = 0, validataFunc; validataFunc = this.cache[i++]){
var msg = validataFunc();
if(msg){
return msg;
}
}
}
我们以后想要加入规则,只需要配置一些简单的表单验证,这些规则也可以简单的复用到程序的任何地方,甚至可以以插件的形式移植到其他的项目中去。假设我们想要修改密码长度不可以小于10位,我们可以这么进行调用validator.add(registerForm.password, 'minLength:10', '密码长度不能小于10位')
;
给某一个文本框添加多种校验规则
如果我们想给某一个文本框添加多种校验规则的话,比如说:我们要校验输入的用户名不为空且长度不小于10位,我们怎么进行校验呢?
validator.add(registerForm.userName, [
{
strategy:'isNotEmpty',
errorMsg:'用户名不能为空'
}, {
strategy:'minLength:10',
errorMsg:'用户名长度不能小于10位'
}
]);
//策略对象
var strategies = {
isNotEmpty: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]{9}$)/.test(value)){
return errorMsg;
}
}
}
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, 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: 'isNotEmpty',
errorMsg: '用户名不能为空'
},
{
strategy: 'minLength:10',
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;
}
}
策略模式的优点:
- 策略模式利用组合、委托和多态,可以避免多重条件选择
- 可以完美的支持开放-关闭原则,将算法独立的封装在
strategy
中,是算法之间易于切换、易于理解、易于扩展。 - 策略模式的算法也可以复用到系统的其他地方,从而避免许多的重复粘贴
- 策略模式利用组合和委托
Context
拥有执行算法的能力,这也是继承的一种更加轻便的替代方案。