定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换。
- JavaScript版策略模式
- 多态在策略模式中的体现
- 策略模式的例子
- 缓动动画
- 表单校验
- 策略模式在调用接口中的体现
- Vue组件的策略模式
- 策略模式的优缺点
- 一等函数对象和策略模式
一个例子
有如下需求,根据不同员工的绩效等级,发放不同的年终奖。
- 等级为S,发放4倍工资
- 等级为A,发放3倍工资
- 等级为B,发放2倍工资
根据这一需求,可以得到以下代码
let calculateBouns = function(level, salary) {
if(level === 'S') {
return salary * 4
}
if(level === 'A') {
return salary * 3
}
if(level === 'B') {
return salary * 2
}
}
上述代码虽然可以实现业务需求,但存在以下几个问题:
if-else
语句过多,导致函数本身非常庞大,而且要覆盖到所有逻辑。- 函数缺乏弹性,如果未来新增业务,则需要改动函数本身,违反开闭原则。
- 内部算法可复用性差,项目中其他地方需要单独用到其中某个算法,只能重新复制粘贴。
如何去重构这段代码?
- 组合函数重构
- 策略模式重构
组合函数重构
既然违反开闭原则,且算法可复用差。那么将函数内部的每个算法单独封装到单独的函数中。和calculateBouns
函数组合起来。
尽管现在将年终奖计算的算法封装到外部,但依然没有解决大量逻辑分支的问题,未来此函数依然可能会变得愈发庞大。如果某个绩效的level发生改变,还是要深入改动let levelS = function(salary) {
return salary * 4
}
// 其他等级同上
let calculateBouns = function(level, salary) {
if(level === 'S') {
levelS(salary)
}
}
calculateBouns
本身。策略模式重构
每个模式都是将不变的部分和变化的部分分离。
而策略模式是将算法的使用和算法的实现分离。上述业务中,不变的是根据某个算法获取年终奖的数额;变化的是算法内部的实现方式。
一个策略模式的代码只要有两部分:
- 一组策略类,封装了具体的算法及过程
-
面向对象的思想实现
定义一组策略类。并在内部实现计算过程和逻辑。
let levelS = function () {}
levelS.prototype.calculate = function(salary) {
return salary * 4
}
let levelA = function () {}
// 同上
定义环境类。并向外暴露
setSalary
、setSrategy
、getBouns
方法。 setSalary
,设置Bonus
的salary
(绩效)属性。setSrategy
,设置策略对象。getBouns
,获取计算后的年终奖数额。let Bonus = function() {
this.salary = null
this.strategy = null
}
Bouns.prototype.setSalary = function(salary) {
this.salary = salary
}
Bouns.prototype.setSrategy = function(strategy) {
this.strategy = strategy
}
Bouns.prototype.getBonus = function(strategy) {
return this.strategy.calculate(this.salary)
}
当想要计算某个level的年终奖时,只需要创建一个
bonus
对象,通过setSalary
和setSrategy
设置工资和策略对象(业务算法),最后通过getBonus
获取最终计算好的年终奖。let bonus = new Bonus()
bonus.setSalary(10000) // 设置工资
bonus.setSrategy(levelS) // 设置策略对象
let result = bonus.getBonus() // 获取年终奖金额
通过策略模式的重构,便解决了之前存在的几个问题:
去除了大量逻辑分支
- 对未来业务有良好的的支持,不论是新增还是修改某个算法逻辑,都不会影响到其他bonus对象的使用。只专注于要修改的算法
- 对于单个算法而言,可以复用到其他可能使用到的地方
JavaScript版的策略模式
以上使用了面向对象思想的策略模式。下面是JavaScript版的策略模式实现。
let strategies = {
'S': function(salary) {
return salary * 4
},
// 同上
}
let calculateBouns = function (level, salary) {
return strategies[level](salary)
}
不论是什么方式实现策略模式,但不变的是策略模式的主题,将不变的部分和变化的部分分离。
组成部分也是一样,一组策略类和环境类。然而在JavaScript中,则是定义了一个策略对象和暴露出来的calculateBouns
方法,该方法只负责接受level
和salary
,并返回计算后的结果。
多态在策略模式中的体现
策略模式消除了大量逻辑分支。算法也分布在不同的策略对象中。而作为context(环境类,也可以理解为策略对象暴露出的方法)并没有计算奖金的能力,而是委托给对应的策略对象。
当对策略对象发起请求时,会返回不同的结果,这正是对象多态性的体现。策略对象也可以互相替换。
策略模式的例子
缓动动画
有如下需求,页面上有一个小球,需要让小球具备各种缓动效果。
定义一组缓动动画的算法
let tween = {
linear: function(t, b, c, d) {
return something
},
easeIn: function(t, b, c, d) {
return something
},
// 同上
}
源码链接
策略模式最主要的是,如何从策略模式的背后,找到封装变化委托和多态性这些思想的价值。
表单验证
策略模式在调用接口中的体现
如下需求,给定一个选项id,根据该id的不同,调用不同的接口,不同接口的参数也可能不同。
id是01-10的范围内的字符串
const ids = ['01', '02', ..., '10']
定义一组策略对象。由于每个接口的参数不同,所以将变化的参数也放在封装的业务函数中。
let requests = {
'01': async function() {
const params = {}
return await network.api01(params)
},
// 同上
}
定义一个Context,用来将请求委托给相应的策略对象
let getResult = function(id) {
return requests[id]()
}
这里getResult方法返回的是一个Promise对象
getResult(ids[0]).then().catch()
// 或者
async func() {
const result = getResult(ids[0])
}
Vue组件的策略模式
策略模式的优缺点
优点:
- 利用组合、委托和多态等技术思想,避免多重选择语句
- 完美支持开闭原则,将算法/业务封装在独立的策略对象中。易于替换,扩展,理解
- 利用组合和委托让Context拥有执行算法的能力,也是继承的一种替代方案
缺点:
- 在策略模式的代码中,会有很多策略类或者策略对象
- 使用该模式时,需要了解所有的策略对象,及其之间的不同点。也违反了最少知识原则
一等函数对象和策略模式
JavaScript中函数作为一等公民,策略模式是隐形的,已经融入到语言当中。函数可以作为变量进行传递,这也叫做高阶函数。
使用高阶函数封装不同的算法,并传入另一个函数中。当调用该函数时,不同的函数就会返回不同的结果。
使用JavaScript的特性,来实现策略模式。
let S = function (salary) {
return salary * 4
}
// 同上
let calculateBouns = function(func, salary) {
return func(salary)
}
calculateBouns(S, 10000)