策略模式
- 策略模式的定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
一、策略模式计算奖金
假设年终奖根据工资基数和绩效等级来发放。比如,绩效为 S 的人可以获得 4 倍工资,绩效为 A 的人可以获得 3 倍工资,绩效为 B 的人可以获得 2 倍工资。这时,可以编写代码来计算年终奖,需要接受两个参数:工资基数和绩效等级。
1.1、不使用策略模式
代码如下:
var calculateBonus = function (salary, level) {
if (level === 'S') {
return 4 * salary
}
if (level === 'A') {
return 3 * salary
}
if (level === 'B') {
return 2 * salary
}
}
calculateBonus(10000, 'S') // 40000
calculateBonus(8000, 'A') // 24000
calculateBonus(6000, 'B') // 12000
上面这段代码十分简单,但是存在以下几个缺点:
if-else语句很多,函数太庞大- 违反开放-封闭原则,如果需要增加等级 C,需要深入函数内部去修改
- 复用性差,如果需要在其他地方使用,那么只能复制粘贴过去
1.2、使用策略模式重构代码
策略模式的目的就是将算法的使用与算法的实现分离开来。
一个基于策略模式的程序至少由两部分组成。第一部分是一组策略类,策略封装了具体的算法,并负责具体的计算过程;第二部分是环境类 Context,Context 接收客户的请求,随后把请求委托给一个策略类。所以,Context 中要维持对某个策略对象的引用。
下面是使用策略模式重构的代码:
var performanceS = function () {}
performanceS.prototype.calculate = function (salary) {
return 4 * salary
}
var performanceA = function () {}
performanceA.prototype.calculate = function (salary) {
return 3 * salary
}
var performanceB = function () {}
performanceB.prototype.calculate = function (salary) {
return 2 * salary
}
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 () {
if (!this.strategy) {
throw new Error('未设置 strategy 属性')
}
return this.strategy.calculate(this.salary)
}
var bonus = new Bonus()
bonus.setSalary(1000)
bonus.setStrategy(new performanceS())
console.log(bonus.getBonus()) // 4000
bonus.setStrategy(new performanceA())
console.log(bonus.getBonus()) // 3000
策略模式的思想就是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。说的详细一点就是:定义一系列的算法,把它们各自封装成策略类,算法被封装在策略类内部的方法里。客户对 Context 发起请求的时候,Context 总是把请求委托给这些策略对象中间的某一个进行计算。
重构后的代码,变得更加清晰,各个类的职责更加明确。
1.3、JavaScript 版本的策略模式
在 JavaScript 中,函数也是对象,所以可以直接把 strategy 定义为函数:
var strategies = {
S: function (salary) {
return 4 * salary
},
A: function (salary) {
return 3 * salary
},
B: function (salary) {
return 2 * salary
},
}
var calculateBonus = function (salary, level) {
return strategies[level](salary)
}
calculateBonus(10000, 'S') // 40000
calculateBonus(8000, 'A') // 24000
calculateBonus(6000, 'B') // 12000
二、策略模式进行表单校验
假设一个注册的页面,在点击注册按钮之前,对输入的信息进行校验:
- 用户名不能为空
- 密码长度不能少于 6 位
- 手机号码必须符合格式
2.1、不使用策略模式
代码如下:
<form action="https://xxx.com/register" id="registerForm" method="POST">
请输入用户名:<input type="text" name="userName" /> 请输入密码:<input
type="password"
name="password"
/>
请输入手机号码:<input type="text" name="phone" />
<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.phone.value)) {
alert('手机号码格式不正确')
return false
}
}
</script>
上面的代码显然存在着缺点:
- registerForm.onsubmit 函数比较庞大,有很多的
if-else语句 - registerForm.onsubmit 缺乏弹性,如果需要修改密码长度至少 8 位,那么必须深入函数内部修改,违反开放-封闭原则
- 复用性差,如果在其它地方也需要这样一个校验,那么就只能复制粘贴过去了
2.2、使用策略模式重构代码
使用策略模式重构后的代码如下:
var strategies = {
isNonEmpty: function (value, errMsg) {
if (value === '') {
return errMsg
}
},
minLength: function (value, length, errMsg) {
if (value.length < length) {
return errMsg
}
},
isMobile: function (value, errMsg) {
if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
return errMsg
}
},
}
var Validator = function () {
this.cache = []
}
Validator.prototype.add = function (dom, rule, errMsg) {
var ary = rule.split(':')
this.cache.push(function () {
var strategy = ary.shift()
ary.unshift(dom.value)
ary.push(errMsg)
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
}
}
}
var validateFunc = function () {
var validator = new Validator()
validator.add(registerForm.userName, 'isNonEmpty', '用户名不能为空')
validator.add(registerForm.password, 'minLength:6', '密码不能少于 6 位')
validator.add(registerForm.phone, 'isMobile', '手机号格式不正确')
var errorMsg = validator.start()
return errorMsg
}
var registerForm = document.getElementById('registerForm')
registerForm.onsubmit = function () {
var errMsg = validateFunc()
if (errMsg) {
alert(errMsg)
return false
}
}
- 首先需要将校验逻辑都封装成策略对象
- Validator 类作为 Context,负责接收用户的请求并委托给 strategy 对象
- 通过 validator.add 方法往 validator 对象中添加一些校验规则
- 添加完规则之后,会调用 validator.start 方法来启动校验,通过是否有 errMsg 来判断是否阻止提交信息
重构后的代码,仅仅通过“配置”就可以完成一个表单的校验,而且可以复用到其他地方。
三、总结
-
策略模式的优点:
- 利用组合、委托和多态等技术和思想,有效避免多重条件选择语句。
- 对开放-封闭原则的完美支持,将算法封装在独立的 strategy 中,使得易于切换,易于扩展。
- 复用性好。
-
策略模式的缺点:
- 会在程序中增加许多策略类或者策略对象。
- 使用策略模式需要了解所有的 strategy,此时,strategy 要向客户暴露它的所有实现,违反最少知识原则。