1 策略模式的定义
定义一系列的算法,把它们一个个封装起来,并且使用它们可以相互替换。 举个例子:在程序设计中,我们需要实现某一个功能其实有多种方案可以选择,例如压缩文件的程序,我们可以选择zip的算法,也可以选择gzip的算法。这些算法灵活多样,而且可以随意相互替换。
2 使用策略模式计算奖金
在单位年终的时候,会对职员进行年终评价,不同等级获得不一样的奖金。A级获得4倍工资,B级获得3倍工资,C级获得2倍工资,D级只有一倍工资。我们需要设计一个计算年终的函数。
function calculate = function(salary, level) {
switch(level) {
case 'A':
return salary * 4;
case 'B':
return salary * 3;
case 'C':
return salary * 2;
default:
return salary;
}
}
上面是一般我们会选择的计算方式,这样设计有缺点:
- calculate的函数比较大,Swicth-case中需要覆盖所有的逻辑分支
- calculate的函数缺乏弹性,如果需要新增一种等级
D,那么就需要修改calculate函数内部的实现。 - 算法的复用性差,如果程序的其他地方需要重用这些计算奖金的算法,我们只能选择粘贴复制
2.1 使用策略模式重构代码
策略模式是定义一系列的算法,把它们一个个封装起来。将不变的部分和变化的部分隔开是每个设计模式的主题,策略模式也不例外,策略模式的目的就是将算法的使用与算法的实现分离开来。例子中,可以按一下方式理解:
- 算法的实现是变化的,每种绩效对应的不同的计算规则
- 算法的使用方式是不变的,都是根据某个算法取得计算后的奖金数额
策略模式的程序至少由两部分组成:
- 策略类: 封装具体的算法,并负责具体的计算过程
- 环境类Context: 接收用户请求,随后将请求委托给策略类
2.1.1 模仿面向对象的方式实现
- performanceA, performanceB, performanceC, performanceOther :都是可变的策略类,封装计算规则。
- Bonus: 环境类部分,接收用户请求,委托给策略类进行计算
// 策略类
var performanceA = function() {}
performanceA.calculate = function(salary) {
return salary * 4;
}
var performanceB = function() {}
performanceB.calculate = function(salary) {
return salary * 3;
}
var performanceC = function() {}
performanceC.calculate = function(salary) {
return salary * 2;
}
var performanceOther = function() {}
performanceOther.calculate = function(salary) {
return salary;
}
// 环境类Context
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);
}
2.1.2 javascript版本的策略模式
上面strategy对象是从各个策略类中创建出来的,那是模拟传统的面向对象语言实现的。在javascript中,函数也就是对象,所以能够更加简单和直接的把strategy直接定义为函数。
var strategies = {
'A': function(salary) {
return 4 * salary;
},
'B': function(salary) {
return 3 * salary;
},
'C': function(salary) {
return 2 * salary;
}
}
var calculateBonus = function(salary, level) {
return strategies[level](salary);
}
2.1.3 多态在策略模式中的体现
通过上面的重构,我们消除了switch分支的条件语句。把计算奖金的逻辑不再放入到context中,而是分布在各个策略对象中。
- 每个策略对象负责的算法被各自封在了对象内部
- Context没有计算奖金的能力,通过职责委托给了某个策略对象
当对这些策略发起请求时计算奖金时,会根据各自不同的计算返回不同的结果,而这也是对象多态的体现。也是它们能够相互替换的目的。替换context中的当前保存的策略,遍能够知晓不同的算法来得到我们想要的结果。
3 使用策略模式进行表单校验
我们在编写注册界面的时候,点击注册按钮前需要对表单进行校验工作:
- 用户名不能为空
- 密码长度不能少于6位
- 手机号码必须符合格式
3.1 普通实现
首先我们不使用策略模式进行实现。该实现方式与计算奖金的实现问题一模一样。
- registerForm.onSubmit函数很庞大,包含了if-else, 包含了所有的校验规则
- registerForm.onSubmit函数缺乏弹性,如果想新增一个校验规则,或则修改规则,那么就需要深入到该函数的内部实现。违背了
开放-封闭原则 - 算法复用性差。如果项目的其他位置也需要相同的校验,需要拷贝复制
<html>
<body>
<form name="registerForm">
请输入用户名: <input type="text" name="userName"/ >
请输入密码: <input type="text" name="password"/ >
请输入手机号码: <input type="text" name="phoneNumber"/ >
<button>提交</button>
</form>
<script>
var registerForm = document.forms['registerForm'];
registerForm.onsubmit = function(event){
if ( registerForm.userName.value === '' ){
alert ( '用户名不能为空' );
event.preventDefault();
}
if ( registerForm.password.value.length < 6 ){
alert ( '密码长度不能少于 6 位' );
event.preventDefault();
}
if ( !/(^1[3|5|8][0-9]{9}$)/.test( registerForm.phoneNumber.value ) ){
alert ( '手机号码格式不正确' );
event.preventDefault();
}
}
</script>
</body>
</html>
3.2 使用策略模式重构
我们需要遵循的规则,依然是这两条:
- 提取所有的可变原则,将校验规则封装起来作为策略类
- 提取context内容,接收用户请求,通过委托给策略类进行计算 下面,我们实现的内容需求:
- 调用validate.add()方法:添加校验规则(参数1:需要校验的字符串, 参数2:校验的规则数组,参数4:可选的正则)
- 调用valiadte.start()方法:开始校验
<html>
<body>
<form name="registerForm">
请输入用户名: <input type="text" name="userName"/ >
请输入密码: <input type="text" name="password"/ >
请输入手机号码: <input type="text" name="phoneNumber"/ >
<button>提交</button>
</form>
<script>
// 策略类
var strategies = {
isNotEmpty: function(str, errorMsg) {
if (str === '') {
return errorMsg;
}
},
minLength: function(str, errorMsg, length) {
if (str.length < length) {
return errorMsg;
}
},
isRegExp: function(str, errorMsg, regExp) {
if (!regExp.test(str)) {
return errorMsg;
}
}
}
// context类: 负责接收用户传入的请求,并委托给策略类。不可变
var Validate = function() {
var cache = [];
return {
add: function(str, rules, regExp) {
rules.map(function(rule) {
var key = Object.keys(rule)[0];
var errorMsg = rule[key];
var ary = key.split(':');
console.log(key, ary);
cache.push(function() {
// 加入有:分割,第一个则是策略
var strategy = ary.shift();
return strategies[strategy].call(null, str, errorMsg, regExp || ary.shift());
});
});
},
start: function() {
var msg = '';
for (var i = 0; i < cache.length ; i++) {
msg = cache[i]();
if (msg) {
alert(msg);
break;
}
}
return msg;
}
}
};
function validateRegister(registerForm) {
var validate = Validate();
validate.add(registerForm.userName.value, [{'isNotEmpty': '用户名不能为空'},{'minLength:3':'密码长度不能少于 3 位'}]);
validate.add(registerForm.password.value, [{'minLength:6':'密码长度不能少于 6 位'}]);
validate.add(registerForm.phoneNumber.value, [{'isRegExp':'手机号码格式不正确'}], /(^1[3|5|8][0-9]{9}$)/);
var returnMsg = validate.start();
return returnMsg ? false : true;
}
registerForm.onsubmit = function(event){
var isPass = validateRegister(registerForm);
if (!isPass) {
event.preventDefault();
console.log('no validate');
} else {
console.log('pass');
registerForm.submit();
}
}
</script>
</body>
</html>
4 策略模式的优缺点
策略模式有点:
- 利用组合,委托和多态等技术和思想,可以有效避免多重条件选择语句
- 提供了对外开放-封闭的原则的完美支持。将算法封装在独立的strategy内,使得它们容易切换,易于理解和拓展
- 策略模式中的算法可以提供给其他地方,避免了重复粘贴复制
- 利用组合与委托让context拥有执行算法的能力。这也是继承的一种更轻便的替代方案
缺点:
- 使用策略模式会让程序增加许多策略类或者策略对象。
- 使用策略模式,必须要了解所有的strategy之前的不同点,这样才能选择一个适合的strategy。