定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换

  • JavaScript版策略模式
  • 多态在策略模式中的体现
  • 策略模式的例子
    • 缓动动画
    • 表单校验
    • 策略模式在调用接口中的体现
    • Vue组件的策略模式
  • 策略模式的优缺点
  • 一等函数对象和策略模式

一个例子

有如下需求,根据不同员工的绩效等级,发放不同的年终奖。

  • 等级为S,发放4倍工资
  • 等级为A,发放3倍工资
  • 等级为B,发放2倍工资

根据这一需求,可以得到以下代码

  1. let calculateBouns = function(level, salary) {
  2. if(level === 'S') {
  3. return salary * 4
  4. }
  5. if(level === 'A') {
  6. return salary * 3
  7. }
  8. if(level === 'B') {
  9. return salary * 2
  10. }
  11. }

上述代码虽然可以实现业务需求,但存在以下几个问题:

  1. if-else语句过多,导致函数本身非常庞大,而且要覆盖到所有逻辑。
  2. 函数缺乏弹性,如果未来新增业务,则需要改动函数本身,违反开闭原则
  3. 内部算法可复用性差,项目中其他地方需要单独用到其中某个算法,只能重新复制粘贴。

如何去重构这段代码?

  • 组合函数重构
  • 策略模式重构

    组合函数重构

    既然违反开闭原则,且算法可复用差。那么将函数内部的每个算法单独封装到单独的函数中。和calculateBouns函数组合起来。
    1. let levelS = function(salary) {
    2. return salary * 4
    3. }
    4. // 其他等级同上
    5. let calculateBouns = function(level, salary) {
    6. if(level === 'S') {
    7. levelS(salary)
    8. }
    9. }
    尽管现在将年终奖计算的算法封装到外部,但依然没有解决大量逻辑分支的问题,未来此函数依然可能会变得愈发庞大。如果某个绩效的level发生改变,还是要深入改动calculateBouns本身。

    策略模式重构

    每个模式都是将不变的部分和变化的部分分离。

而策略模式是将算法的使用算法的实现分离。上述业务中,不变的是根据某个算法获取年终奖的数额;变化的是算法内部的实现方式。
一个策略模式的代码只要有两部分:

  • 一组策略类,封装了具体的算法及过程
  • 环境类,接受客户的请求,并把请求委托给某个策略类

    面向对象的思想实现

    定义一组策略类。并在内部实现计算过程和逻辑。

    1. let levelS = function () {}
    2. levelS.prototype.calculate = function(salary) {
    3. return salary * 4
    4. }
    5. let levelA = function () {}
    6. // 同上

    定义环境类。并向外暴露setSalarysetSrategygetBouns方法。

  • setSalary,设置Bonussalary(绩效)属性。

  • setSrategy,设置策略对象。
  • getBouns,获取计算后的年终奖数额。

    1. let Bonus = function() {
    2. this.salary = null
    3. this.strategy = null
    4. }
    5. Bouns.prototype.setSalary = function(salary) {
    6. this.salary = salary
    7. }
    8. Bouns.prototype.setSrategy = function(strategy) {
    9. this.strategy = strategy
    10. }
    11. Bouns.prototype.getBonus = function(strategy) {
    12. return this.strategy.calculate(this.salary)
    13. }

    当想要计算某个level的年终奖时,只需要创建一个bonus对象,通过setSalarysetSrategy设置工资和策略对象(业务算法),最后通过getBonus获取最终计算好的年终奖。

    1. let bonus = new Bonus()
    2. bonus.setSalary(10000) // 设置工资
    3. bonus.setSrategy(levelS) // 设置策略对象
    4. let result = bonus.getBonus() // 获取年终奖金额

    通过策略模式的重构,便解决了之前存在的几个问题:

  • 去除了大量逻辑分支

  • 对未来业务有良好的的支持,不论是新增还是修改某个算法逻辑,都不会影响到其他bonus对象的使用。只专注于要修改的算法
  • 对于单个算法而言,可以复用到其他可能使用到的地方

JavaScript版的策略模式

以上使用了面向对象思想的策略模式。下面是JavaScript版的策略模式实现。

  1. let strategies = {
  2. 'S': function(salary) {
  3. return salary * 4
  4. },
  5. // 同上
  6. }
  7. let calculateBouns = function (level, salary) {
  8. return strategies[level](salary)
  9. }

不论是什么方式实现策略模式,但不变的是策略模式的主题,将不变的部分和变化的部分分离。
组成部分也是一样,一组策略类和环境类。然而在JavaScript中,则是定义了一个策略对象和暴露出来的calculateBouns方法,该方法只负责接受levelsalary,并返回计算后的结果。

多态在策略模式中的体现

策略模式消除了大量逻辑分支。算法也分布在不同的策略对象中。而作为context(环境类,也可以理解为策略对象暴露出的方法)并没有计算奖金的能力,而是委托给对应的策略对象。

当对策略对象发起请求时,会返回不同的结果,这正是对象多态性的体现。策略对象也可以互相替换。

策略模式的例子

缓动动画

有如下需求,页面上有一个小球,需要让小球具备各种缓动效果。

定义一组缓动动画的算法

  1. let tween = {
  2. linear: function(t, b, c, d) {
  3. return something
  4. },
  5. easeIn: function(t, b, c, d) {
  6. return something
  7. },
  8. // 同上
  9. }

源码链接
策略模式最主要的是,如何从策略模式的背后,找到封装变化委托和多态性这些思想的价值。

表单验证

策略模式在调用接口中的体现

如下需求,给定一个选项id,根据该id的不同,调用不同的接口,不同接口的参数也可能不同。

id是01-10的范围内的字符串

  1. const ids = ['01', '02', ..., '10']

定义一组策略对象。由于每个接口的参数不同,所以将变化的参数也放在封装的业务函数中。

  1. let requests = {
  2. '01': async function() {
  3. const params = {}
  4. return await network.api01(params)
  5. },
  6. // 同上
  7. }

定义一个Context,用来将请求委托给相应的策略对象

  1. let getResult = function(id) {
  2. return requests[id]()
  3. }

这里getResult方法返回的是一个Promise对象

  1. getResult(ids[0]).then().catch()
  2. // 或者
  3. async func() {
  4. const result = getResult(ids[0])
  5. }

Vue组件的策略模式

策略模式的优缺点

优点:

  • 利用组合、委托和多态等技术思想,避免多重选择语句
  • 完美支持开闭原则,将算法/业务封装在独立的策略对象中。易于替换,扩展,理解
  • 利用组合和委托让Context拥有执行算法的能力,也是继承的一种替代方案

缺点:

  • 在策略模式的代码中,会有很多策略类或者策略对象
  • 使用该模式时,需要了解所有的策略对象,及其之间的不同点。也违反了最少知识原则

**

一等函数对象和策略模式

JavaScript中函数作为一等公民,策略模式是隐形的,已经融入到语言当中。函数可以作为变量进行传递,这也叫做高阶函数。
使用高阶函数封装不同的算法,并传入另一个函数中。当调用该函数时,不同的函数就会返回不同的结果。
使用JavaScript的特性,来实现策略模式。

  1. let S = function (salary) {
  2. return salary * 4
  3. }
  4. // 同上
  5. let calculateBouns = function(func, salary) {
  6. return func(salary)
  7. }
  8. calculateBouns(S, 10000)