在程序设计中,我们常常遇到类似的情况,要实现一个功能有多种方案可以选择。
比如我们要去某个地方旅游,可以根据具体的实际情况来选择出行路线。
1. 如果没有时间但是不在乎钱,可以选择坐飞机。
2. 如果没有钱,可以选择坐大巴或者火车。
3. 如果再穷一点,可以选择自行车。
比如一个压缩文件的程序,既可以选择zip算法,也可以选择gzip算法。
...
策略模式的定义:定义一系列的算法,把他们一个个封装起来,并且使它们可以互相替换。
1. 使用策略模式计算奖金
1.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('B', 20000)) // 40000
console.log(calculateBonus('S', 6000)) // 24000
可以发现,上段代码十分简单,但是存在着显而易见的缺点。
1. calculateBonus函数比较庞大,包含了很多if-else语言,这些语句需要覆盖所有的逻辑分支。
2. calculateBonus函数缺乏弹性,如果增加了一种新的绩效等级C, 或者想把绩效S的奖金系数改为5,
那么我们必须深入calculateBonus函数的内部实现,这是违反开发-封闭原则的。
3. 算法的复用性差,如果再程序的其他地方需要谨慎重用这些计算奖金的算法呢?我们的选择只有复制黏贴。
1.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 (performance, salary) {
if (performance === 'S') {
return performanceS(salary)
}
if (performance === 'A') {
return performanceA(salary)
}
if (performance === 'B') {
return performanceB(salary)
}
}
console.log(calculateBonus('A', 10000)) // 30000
看上面的代码,目前虽然程序得到了一定的改善,但是这种改善非常有限,我们依然没有解决最重要的问题。calculateBonus函数有可能越来越庞大,而且在系统变化的时候缺乏弹性。
1.3 使用策略模式重构代码
使用策略模式来重构代码。策略模式指的是定义一些列的算法,把他们一个个封装起来。将不变的部分和变化的部分隔开是每个设计模式的主题,策略模式也不例外,策略模式的目的就算将算法的使用与算法的实现分离开来。 在这个例子里,算法的使用方式是不变的,都是根据某个算法取得计算后的奖金数额。而算法的实现是各异和变化的,每种绩效对应着不同的计算规则。
一个基于策略模式的程序至少由两个部分组成。第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。第二部分是环境类Context,Context接受客户的请求,随后把请求委托给某一策略类。要做到这点。说明Content中要维持对某个策略对象的引用。
(1).先把每种绩效的计算规则都封装在对应的策略类里面
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
}
(2).接下来定义奖金类 Bonus
var Bonus = function() {
this.salary = null // 原始工资
this.strategy = null // 绩效等级对应的策略对象
}
Bonus.prototype.setSalary = function(salary) {
this.salary = salary // 设置员工的原始工资
}
Bonus.prototype.setStrategy = function(startegy) {
this.strategy = startegy // 设置员工绩效等级对应的策略对象
console.log("整个策略对象:", this.strategy)
}
Bonus.prototype.getBonus = function() { // 取得奖金数额
console.log("调用策略对象上的方法并传入原始工资")
return this.strategy.calculate(this.salary) // 把计算奖金的操作委托给对方的策略对象
}
(3).使用Bonus类
策略模式思想:定义一系列的算法,把他们一个个封装起来,并且使它们可以互相替换。
就是定义一系列的算法,把他们各种封装成策略类,算法被封装在策略类内部的方法里。
在客户对Context发起请求的时候,Context总是把请求委托给这些策略对象中间的某一个进行计算。
以下代码过程:先创建一个bonus对象,并且给bonus对象设置一些原始的数据,比如员工的原始工资数额。接下来把某个计算奖金的策略也传入bonus对象内部保存起来。当调用bonus.getBonus()来计算奖金的时候,bonus对象本身并没有能力进行计算。而是把请求委托给了之前保存好的策略对象:
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
2. 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 Object.hasOwn(strategies, level) ? strategies[level](salary) : null
return strategies[level](salary)
}
console.log(calculateBonus('S', 2000)) // 8000
console.log(calculateBonus('A', 2000)) // 6000
3. 多态在策略模式中的体现
通过使用策略模式重构代码,我们消除了原程序中大片的条件分支语句。所有跟计算奖金有关的逻辑不再放在Context中,而是分布在各个策略对象中。Context并没有计算奖金的能力,而是把这个职责委托给了某个策略对象。每个策略对象赋值的算法已被各自封装在对象内部。当我们对这些策略对象发出了"计算奖金"的请求时,它会返回各自的不同计算结果,这正是对象多态性的提醒,也是"他们可以相互替换"的目的。替换Context中当前保存的策略对象,并能执行不同的算法来得到我们想要的结果。
4. 表单校验
<form action="http://xxx.com/register" id="registerForm" method="post">
请输入用户名:<input type="text" name="userName" /><br/>
请输入密码:<input type="text" name="password" /><br/>
请输入手机号码:<input type="text" name="phoneNumber" /><br/>
<button>提交</button>
</form>
// 策略对象
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: '用户名长度不能小于6位'}
])
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) {
console.log(errorMsg)
return false
}
console.log("验证正确")
}
5. 策略模式的优缺点
缺点:
- 使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比他们负责的逻辑堆砌在Context中要好。
- 要使用策略模式,必须了解所有的strategy,必须了解各个strategy,必须了解各个strategy之间的不同点。这样才能选择一个合适的strategy。比如,我要选择一种合适的旅游出行路线,必须先了解选择飞机,火车,自行车等方案的细节。此时strategy要向客户暴露它的所有实现,这是违反最少知识原则的。