阅读 510

JavaScript 设计模式之策略模式

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

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

使用策略模式计算奖金

以计算年终奖为例,例如:绩效为S的人年终奖4倍工资,绩效为A的人年终奖3倍工资,绩效为B的人年终奖2倍,写一段代码来实现计算员工的年终奖。

1 最初的代码实现

var calculateBonus = function(performanceLevel, salary) {
  if (performanceLevel === 'S') {
    return salary * 4
  }
  if (performanceLevel === 'A') {
    return salary * 3
  }
  if (performanceLevel === 'B') {
    return salary * 2
  }
}
calculateBonus('B', 20000) // 输出:40000 
calculateBonus( 'S', 6000 ); // 输出:24000
复制代码

可以发现,这段代码非常简单,但是存在很多显而易见的缺点。

  • calculateBonus 包含很多 if-else 语句,需要覆盖所有的逻辑分支。
  • calculateBonus 缺乏弹性,如果增加了绩效C,或者调整了奖金系数,都必须重新深入 calculateBonus 函数的内部去修改,这是违反开放-封闭原则。
  • 算法的复用性查,如果其他地方需要用,只有复制粘贴。

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(performanceLevel, salary) {
  if (performanceLevel === 'S') {
    return performanceS(salary)
  }
  if (performanceLevel === 'A') {
    return performanceA(salary)
  }
  if (performanceLevel === 'B') {
    return performanceB(salary)
  }
}
calculateBonus('A', 10000) // 输出:30000
复制代码

3 使用策略模式重构代码

策略模式指定义一系列算法,把它们一个个封装起来。将不变的部分和变化的部分隔开是每个设计模式的主题,策略模式的目的就是将算法的使用与算法的实现分离开来。

一个基于策略模式的程序至少由两部分组成。第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。第二部分是环境类 ContextContext 接受客户的请求,随后把请求委托给某一个策略类。

第一个版本模仿传统面向对象语言中的实现,先把每种绩效的计算规则都封装在对应的策略类里面:

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
}
复制代码

接下来定义奖金类 Bonus

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.getBouns = function() {
  return this.strategy.calculate(this.salary) // 计算奖金的操作委托给对应的策略对象
}
复制代码

策略模式的思想:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。在 JavaScript 中表现为他们具有相同的目标和意图

接下来,来完成剩下的代码。先创建一个 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
复制代码

JavaScript 版本的策略模式

JavaScript 中,函数也是对象,因此更简单和直接的做法是把 strategy 直接定义为函数:

var strategies = {
  S: function(salary) {
    return salary * 4
  },
  A: function(salary) {
    return salary * 3
  },
  B: function(salary) {
    return salary * 2
  }
}
复制代码

同样,Context 也没必要必须用 Bonus 类来表示,定义 calculateBonus 函数。

var calculateBonus = function(level, salary) {
  return strategies[level](salary)
}
console.log(calculateBonus('S', 20000)) // 输出:80000
console.log(calculateBonus('A', 10000)) // 输出:30000
复制代码

使用策略模式实现缓动动画

我们的目的的实现一个小球按照不同的算法进行运动。

实现动画效果的原理

使用 JavaScript 实现动画效果的原理跟动画片的制作一样,动画片是把一些差距不大的原画以较快的帧数播放,来达到视觉上的动画效果。在 JavaScript 中,可以通过连续改编元素的某个 css 属性,比如 lefttopbackground-position 来实现动画效果

思路和一些准备工作

在运动开始前,需要记录一些信息,至少包括:

  • 动画开始时,小球所在的原始位置
  • 小球移动的目标位置
  • 动画开始时的准确时间点
  • 小球运动持续的时间

随后,使用 setInterval 创建一个定时器,每隔19ms循环一次,在定时器的每一帧里,将动画已消耗的时间、小球原始位置、小球目标位置以及动画持续总时间传入缓动算法,计算出小球当前应该所在的位置,然后更新 div 对应的 css 属性。如此小球便可顺利的运动起来。

让小球运动起来

缓动算法接受 4 个参数,这 4 个参数的含义分别是动画已消耗的时间、小球原始位置、小球目标位置、动画持续的总时间,返回的值则是动画元素应该处在的当前位置。

var tween = {
  linear: function (t, b, c, d) {
    return (c * t) / d + b;
  },
  easeIn: function (t, b, c, d) {
    return c * (t /= d) * t + b;
  },
  strongEaseIn: function (t, b, c, d) {
    return c * (t /= d) * t * t * t * t + b;
  },
  strongEaseOut: function (t, b, c, d) {
    return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
  },
  sineaseIn: function (t, b, c, d) {
    return c * (t /= d) * t * t + b;
  },
  sineaseOut: function (t, b, c, d) {
    return c * ((t = t / d - 1) * t * t + 1) + b;
  },
};
复制代码

下面的代码思想来自 jQuery 库,先在页面放置一个 div

<body>
  <div style="position: absolute; background: blue" id="div">我是 div</div>
</body>
复制代码

接下来定义 Animate 类,Animate 的构造函数接受一个参数:即将运动起来的 dom 节点。

var Animate = function (dom) {
  this.dom = dom; // 进行运动的 dom 节点
  this.startTime = 0; // 动画开始时间
  this.startPos = 0; // 动画开始时,dom 节点的位置,即 dom 的初始位置
  this.endPos = 0; // 动画结束时,dom 节点的位置,即 dom 的目标位置
  this.propertyName = null; // dom 节点需要被改变的 css 属性名
  this.easing = null; // 缓动算法
  this.duration = null; // 动画持续时间
};
复制代码

接下来 Animate.prototype.start 启动这个动画,需要记录一些信息供以后计算小球位置时使用,此方法还负责启动定时器。

Animate.prototype.start = function (
  propertyName,
  endPos,
  duration,
  easing
) {
  this.startTime = +new Date(); // 动画启动时间
  this.startPos = this.dom.getBoundingClientRect()[propertyName]; // dom 节点初始位置
  this.propertyName = propertyName; // dom节点需要被改编的 css 属性名
  this.endPos = endPos; // dom 节点目标位置
  this.duration = duration; // 动画持续时间
  this.easing = tween[easing]; // 缓动算法

  var self = this;
  var timeId = setInterval(function () {
    // 启动定时器,开始执行动画
    if (self.step() === false) {
      // 如果动画已结束,则清除定时器
      clearInterval(timeId);
    }
  }, 19);
};
复制代码

Animate.prototype.start 方法接受一下四个参数:

  • propertyName: 要改变的 css 属性名,比如 lefttop,分别表示左右移动和上下移动。
  • endPos:小球运动的目标位置。
  • duration:动画持续时间。
  • easing:缓动算法。

Animate.prototype.step 代表小球运动的每一帧要做的事情。

Animate.prototype.step = function () {
  var t = +new Date(); // 取得当前时间
  if (t >= this.startTime + this.duration) {
    // 动画结束时修正小球的位置
    this.update(this.endPos); // 更新小球的 CSS 属性值
    return false;
  }
  var pos = this.easing(
    t - this.startTime,
    this.startPos,
    this.endPos - this.startPos,
    this.duration
  );
  // pos 为小球当前位置
  this.update(pos); // 更新小球的 CSS 属性值
};
复制代码

Animate.prototype.update 更新小球 css 属性值:

Animate.prototype.update = function (pos) {
  this.dom.style[this.propertyName] = pos + "px";
};
复制代码

测试一下:

var div = document.getElementById("div");
var animate = new Animate(div);
animate.start("left", 500, 1000, "strongEaseOut");
复制代码

0802.gif

更广义的“算法”

策略模式指的是定义一系列的算法,并且把他们封装起来。

从定义上看,策略模式就是用来封装算法的。但如果把策略模式仅仅用来封装算法,有点大材小用。实际开发中,通常会把算法的含义扩散开来,使策略模式也可以用来封装一系列的“业务规则”。只要业务规则指向的目标一致,并且可以被替换使用,我们就可以使用策略模式来封装。

策略模式的优缺点

优点:

  • 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
  • 策略模式提供了对开放-封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它们易于切换,易于理解,易于扩展。
  • 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
  • 在策略模式中利用组合和委托来让 Context 拥有执行算法的能力,这也是继承的一种更轻便的替代方案。

缺点:

  • 使用策略模式会在程序中增加许多策略类或者策略对象
  • 使用策略模式必须了解所有的 strategy,了解各个 strategy 之间的不同点,才能选择一个合适的。

最后说一句

如果这篇文章对您有所帮助,或者有所启发的话,帮忙点赞关注一下,您的支持是我坚持写作最大的动力,多谢支持。

同系列文章

  1. JavaScript 设计模式之单例模式
  2. JavaScript 设计模式之策略模式
  3. JavaScript 设计模式之代理模式
  4. JavaScript 设计模式之迭代器模式
  5. JavaScript 设计模式之发布-订阅模式
  6. JavaScript 设计模式之命令模式
  7. JavaScript 设计模式之组合模式
  8. JavaScript 设计模式之模板方法模式
  9. JavaScript 设计模式之享元模式
  10. JavaScript 设计模式之职责链模式
  11. JavaScript 设计模式之中介者模式
  12. JavaScript 设计模式之装饰者模式
  13. JavaScript 设计模式之状态模式
  14. JavaScript 设计模式之适配器模式
文章分类
前端