定义
在现实中,很多时候要去到一个目的地有多种途径。比如要去旅游,可以根据具体情况来选择出行的线路
- 如果没有时间但不在乎钱,可以选择坐飞机
- 如果没有钱,可以选择坐大巴
- 如果像我一样穷,可以骑共享单车
在程序的设计中,也常常遇到类似的问题,要实现某一个功能有多种方案可选择,比如一个压缩文件的程序,可以选择zip算法,也可以选择gzip算法。
这些算法灵活多样,而且可以随意切换,这种解决方案正是策略模式
策略模式的定义是:
定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。
使用策略模式计算奖金
很多公司的年终奖是根据员工的工资基数和年底绩效情况来发放,例如,绩效为S的人年终奖有4倍工资,A的有3倍,B的有2倍等等如此。现在用一段代码来计算年终奖。
最初代码实现
可以编写一个calculateBonus的函数来计算每个人的奖金数额。calculateBonus要接收两个参数:员工的绩效和初始工资。
//模拟奖金发放的最初代码
/**
*
* @param {*} p 绩效
* @param {*} s 工资
*/
var calculateBonus=function(p,s){
if(p==='s'){
return s*4
}
if(p=='a'){
return s*3
}
if(p=='b'){
return s*2
}
}
代码简单易懂,却存在问题。
calculateBonus函数比较庞大,包含了很多if-else语句,这些语句需要覆盖所有的逻辑分支calculateBonus函数缺乏弹性,如果新增了一种新的绩效等级C,或者想把绩效S的奖金系数改为5,那么就必须深入calculateBonus函数的内部实现,这违法了开放-封闭原则。开放-封闭即对程序来说,扩展是开放的,对修改是封闭的。
- 算法复用性差,如果程序其他地方需要这些计算奖金的算法则需要复制粘贴进行移植。
使用策略模式重构
策略模式是指定义一些列算法,把他们一个个封装起来。策略模式的目的就是将算法的使用和算法的实现分离开来。
在这个例子中,算法的使用是不变的,都是根据某个算法取得计算后的结果,而算法的实现是各异和变化的,每种绩效对应着不同的计算规则。
一个策略模式的程序至少由两部分组成:
策略类:封装具体算法,负责计算的过程
环境类Context:Context负责接收客户的请求,随后把请求委托给某一个策略类。
先把每种绩效的计算规则都封装到相应的策略类中
//先把各种计算规则封装在策略类里面
var performanceS=function(){
}
performanceS.prototype.calculate=function(s){
return s*4
}
var performanceA=function(){
}
performanceA.prototype.calculate=function(s){
return s*3
}
var performanceB=function(){
}
performanceB.prototype.calculate=function(s){
return s*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.getBonus=function(){
if(!this.strategy){
throw new Error('未设置绩效对象')
}
return this.strategy.calculate(this.salary) //把计算奖金的操作委托给策略对象
}
再回顾一下策略模式的思想:
定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换。
“并且使它们可以互相替换”,这句话在很大程度上是相对于静态类型语言而言的,因为静态类型语言中有类型检查机制,所以各个策略类需要实现相同的接口。当它们的真正类型被隐藏在接口后面时,它们才能被相互替代。而在JavaScript这种类型模糊的语言中,任何对象都可以被替换使用。因此,这里的互相替换表现为它们具有相同的目标和意图
再详细一点就是,定义一系列算法,把它们各自封装成策略类,算法被封装在策略类内部的方法里。在客户对Context发起请求时,Context总是把请求委托给这些策略对象中间的某一个进行计算。
var bonus=new Bonus()
bonus.setSalary(10000)
bonus.setStrategy(new performanceS()) //设置策略对象
console.log(bonus.getBonus());
当调用bonus.getBonus()来计算奖金的时候,bonus对象本身并没有能力进行计算,而是把请求委托给了之前保存好的策略对象。
使用策略对象重构之后,代码变得清晰,各个类的职责鲜明。
多态在策略模式中的体现
所谓多态,是指同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。换句话说,给不同的对象发送同一消息的时候,这些对象会根据这个消息分别给出不同的反馈。
通过使用策略模式重构代码,我们消除了原程序中大片的条件分支语句。所有跟计算奖金有关的逻辑不再放在Context中,而是分布在各个策略对象中。
Context并没有计算奖金的能力,而是把这个职责委托给了某个策略对象,每个策略对象负责的算法已被各自封装在对象内部。当我们对这些策略对象发出“计算奖金”的请求时,它们会返回各自不同的计算结果,这正是对象多态性的体现。
表单检验
在一个Web项目中,表单验证是常见的需求。
现在编写一个登录页面,点击提交按钮时有如下两条校验逻辑:
- 用户名不能为空
- 密码长度不能少于6位
表单校验第一个版本
在没有引入策略模式之前的写法
<body>
<form action="http://xxx.com/register" id="registerForm" method="post">
用户名:<input type="text" name="userName" />
密码:<input type="text" name="password" />
<button>submit</button>
</form>
<script>
var registerForm = document.getElementById("registerForm")
registerForm.onsubmit = function () {
if (registerForm.userName.value === '') {
alert('username is not empty')
return false
}
if (registerForm.password.value.length < 6) {
alert('password error')
return false
}
}
</body>
这是一种常见的写法,但是有以下缺点:
registerForm.onsubmit函数比较庞大,包含了较多的if-else语句,这些语句需要涵盖所有校验规则- 函数缺乏弹性,如果新增了一种新的校验规则,或者想把密码校验长度进行修改,则要进入
registerForm.onsubmit内部进行修改,违反了开放-封闭原则
- 代码复用性差,如果有表单也需要相同的逻辑验证,则需要把这些校验逻辑进行复制移植
使用策略模式重构
用策略模式重构,首先要把校验逻辑进行封装,把它们封装成策略对象
//策略模式,把表单验证的逻辑封装成策略对象
var strategies = {
isNonEmpty: function (value, errorMsg) {//不能为空
if (value === '') {
return errorMsg
}
},
minLength: function (value, length, errorMsg) {//限定最小长度
if (value.length < length) {
return errorMsg
}
}
}
实现一个类作为Context,用来接收用户的请求,并且把请求委托给策略对象。
//准备一个Validator类,作为Context,接收用户请求,并委托给strategies对象
var Validator = function () {
this.cache = [] //保存校验的规则
}
Validator.prototype.add = function (dom, rule, errorMsg) {
let ary = rule.split(":") //把strategy和参数分开
this.cache.push(function () {
let strategy = ary.shift() //获取校验规则(策略对象)
ary.unshift(dom.value) //参数列表 头插表单的value数据
ary.push(errorMsg) //把errorMsg放进参数列表中
//这时的参数列表就是 [dom.value,参数,errorMsg]
return strategies[strategy].apply(dom, ary)
})
}
Validator.prototype.start = function () {
for (let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
let msg = validatorFunc() //挨个校验
if (msg) {
return msg //有返回errorMsg则直接返回
}
}
}
用add方法添加校验规则,start进行挨个验证
let validataFunc = function () {
let validator = new Validator()
validator.add(registerForm.userName, 'isNonEmpty', 'username is not empty')
validator.add(registerForm.password, 'minLength:6', 'password error')
let errorMsg = validator.start()
return errorMsg
}
registerForm.onsubmit = function () {
let errorMsg = validataFunc()
if (errorMsg) {
alert(errorMsg)
return false
}
}
使用策略模式,我们仅仅通过配置的方式就可以完成一个表单的校验,这些校验规则可以复用在程序中任何需要进行校验的地方。
在修改某个规则或新增规则时,只需要编写或修改少量代码即可。
总结
策略模式是一种常用且有效的设计模式,它有以下优点:
-
策略模式利用组合、委托、多态等技术和思想,可以有效地避免多重条件选择语句
-
策略模式提供了对开放-封闭原则的完美支持,将算法进行封装,使得它们易于切换、理解和扩展
-
策略模式中的算法可以进行复用