第五章-策略模式
定义:定义一系列的算法,把它们一个个封装起来,并且使他们可以相互替换。
# 使用策略模式计算奖金
员工的年终奖与绩效挂钩,规则如:绩效为 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;
}
};
console.log(calculateBonus('S', 6000)); // 24000
console.log(calculateBonus('A', 20000)); // 60000
2
3
4
5
6
7
8
9
10
11
12
13
14
以上代码虽然实现了功能,但是却有以下几个缺点:
- calculateBonus 函数较庞大,包含了很多条件判断语句;
- 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);
}
};
console.log(calculateBonus('A', 10000)); // 30000
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
至此,我们的程序得到了一定的改善,但是非常有限,我们依然没能解决最重要的问题, 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;
};
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.calculate = function () {
return this.strategy.calculate(this.salary);
}
var bonus = new Bonus();
bonus.setSalary(10000)
bonus.setStrategy(new performanceS());
console.log(bonus.calculate()); // 40000
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
可以看到通过策略模式重构之后,代码变得更加清晰,各个类的职责更加明确。不给过这段代码是基于传统的 oop 语言的模仿,后面我们将了解如何使用 JavaScript 实现策略模式。
# JavaScript 版本的策略模式
在 JavaScript 中,函数也是对象,我们可以把不同的策略封装成一个策略对象,如下示例:
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('B', 6000)); // 12000
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 表单校验
假设我们正在编写一个注册的页面,在点击注册按钮之前,需要做一下几条验证
- 用户名不能为空
- 密码长度不能少于 6 位
- 手机号码必须符合格式
未使用策略模式之前的代码可能是这样的
var registerForm = document.getElementById('form');
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;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这是一种很常见的实现方式,但是有如下几个问题
- registerForm.onsubmit 函数比较庞大,包含了很多的 if-else 语句,这些语句需要覆盖所有的校验规则;
- registerForm.onsubmit 函数缺乏弹性,如果增加了一种新的校验规则,或者想把密码的长度校验从 6 改成 8,我们都必须深入 registerForm.onsubmit 函数的内部实现,这是违反开放-封闭原则的;
- 算法的复用性差,如果在程序中增加了另外一个表单,那个表单也需要类似的校验,可能需要把现在的代码再复制一遍;
# 使用策略模式重构表单校验
下面我们使用策略模式来对表单校验的代码进行重构,第一步就是要将这些表单校验逻辑都封装成策略对象
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;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
然后我们准备实现 Validator 类。Validator 类在这里作为 COntext,负责接收用户的请求并委托给 strategy 对象。在编写 Validator 之前,我们先来了解一下用户是如何调用 Validator 的,代码如下:
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('form');
registerForm.onsubmit = function() {
var errorMsg = validataFunc();
if (errorMsg) {
alert(errorMsg);
return false;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
然后我们再来实现 validator 类,如下:
var Validator = function() {
this.cache = []; // 保存校验规则
}
Validator.prototype.add = function(dom, rule, errorMsg) {
var ary = rule.split(':'); // isNonEmpty, minLength:6
this.cache.push(function() {
var strategy = ary.shift(); // isNonEmpty, minLength
ary.unshift(dom.value); // ['jack', 'isNoneEmpty']
ary.push(errorMsg); // ['jack', 'isNonEmpty', errorMsg]
return strategies[strategy].apply(dom, ary);
})
}
Validator.prototype.start = function() {
for (var i = 0, validataFunc; validataFunc = this.cache[i++];) {
var msg = validataFunc(); // 开始校验,并取得校验后的结果
if (msg) {
return msg;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
在使用策略模式重构之后,我们仅仅通过"配置"的方式就可以完成一个表单的校验,这些校验规则也可以复用在程序的任何地方,还能作为插件的形式,方便的被移植到其他项目中。
在修改某个校验规则的时候,只需要编写或者改写少量的代码。比如我们想将用户名输入框的校验规则改成用户名不能少于 10 个字符,可以看到,这时候的修改是毫不费力的。
validator.add(registerForm.username, 'minLength:10', '用户名长度不能小于 10 位');
# 给某个文本输入框添加多种校验规则
上面的代码只实现了一个输入框对应一种校验策略,但是实际情况中,大多都需要对一个文本进行多个规则的校验,那么我们来尝试改写一下,调用的方式应该是这样的:
validator.add(registerForm.userName, [
{stategy: 'isNonEmpty', errorMsg: '用户名不能为空'},
{stategy: 'minLength:6', errorMsg: '用户名长度不能小于 6 位'}
]);
2
3
4
然后我们对 Validator 类进行调整,如下:
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)
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 策略模式的优缺点
优点:
- 策略模式利用组合、委托和多态等技术和思想,可以有效的避免多重条件选择语句;
- 策略模式提供了对开放-封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它易于切换,易于理解,易于扩展;
- 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作;
- 在策略模式中利用组合和委托来让 Context 拥有执行算法的能力,这也是继承的一种更轻便的替代方案;
缺点:
- 在程序中增加了许多策略类或者策略对象;
- 要使用策略模式,必须了解所有的 strategy,然后选择一个合适的 strategy。