策略模式 Strategy Pattern
介绍
- 不同策略分开处理
- 避免出现大量 if…else 或者 switch…case
示例
购买通道
不同类型用户有不同的购买通道,这里的 buy 方法里有大量的 if…else
class User { constructor(type) { this.type = type }
buy() { if (this.type === 'ordinary') { console.log('普通用户购买') } else if (this.type === 'member') { console.log('会员用户购买') } else if (this.type === 'vip') { console.log('vip 用户购买') } }}
const user1 = new User('ordinary')user1.buy()const user2 = new User('member')user2.buy()const user3 = new User('vip')user3.buy()我们尝试把每一种用户都单独做一个类的处理,每个类都有 buy 方法
class OrdinaryUser { buy() { console.log('普通用户购买') }}class MemberUser { buy() { console.log('会员用户购买') }}class VipUser { buy() { console.log('vip 用户购买') }}
const user1 = new OrdinaryUser()user1.buy()const user2 = new MemberUser()user2.buy()const user3 = new VipUser()user3.buy()计算奖金
年底了,最激动人心的莫过于年终奖了,年终奖一般是根据员工的工资基数和年底绩效情况发放的。比如绩效为 S 的人年终奖为 4 倍工资,绩效为 A 的人年终奖为 3 倍工资,绩效为 B 的人年终奖为 2 倍工资。热情的开发想给财务提供一段代码方便计算员工的年终奖。
最初的代码实现
编写一个 calculateBonus 的方法来计算每个人的奖金数额,接收两个参数:员工的工资和绩效考核等级。
const calculateBonus = (performanceLevel, salary) => { if (performanceLevel === 'S') { return salary * 4 }
if (performanceLevel === 'A') { return salary * 3 }
if (performanceLevel === 'B') { return salary * 2 }}
calculateBonus('B', 10000) // 20000calculateBonus('S', 6000) // 24000- calculateBonus 方法有很多的 if…else,这些语句需要覆盖所有的逻辑分支
- calculateBonus 方法缺乏弹性,如果新增加绩效等级 C,或者想把绩效等级 A 的奖金系数改为 5,那么就需要修改 calculateBonus 方法,违法开放封闭原则
- 复用性差,如果有其他地方需要重用计算奖金的方法,只能复制粘贴
使用组合函数重构代码
如果我们把各种算法封装到一个小函数中,这些函数通过命名区分,就可以知道对应着哪种算法,也可以应用在程序的其他地方
const performanceS = salary => { return salary * 4}
const performanceA = salary => { return salary * 3}
const performanceB = salary => { return salary * 2}
const calculateBonus = (performanceLevel, salary) => { if (performanceLevel === 'S') { return performanceS(salary) }
if (performanceLevel === 'A') { return performanceA(salary) }
if (performanceLevel === 'B') { return performanceB(salary) }}
calculateBonus('A', 10000)这样搞虽然解决了一些复用性问题,但是还是没有解决前两点问题。
使用策略模式
这个例子中,算法的使用方式不变,都是通过基数乘绩效等级得到最终数额。但是每种绩效对应不同的计算规则。 基于策略模式的程序至少由两部分组成:
- 一组策略类,策略类封装了具体的算法,负责具体的计算过程
- 环境类 Context,Context 接受客户的请求,随后把请求委托给某一个策略类,也就是说 Context 中要维持对某个策略对象的引用
我们先把每种绩效的计算规则都封装在对应的策略类中:
class PerformanceS { caculate(salary) { return salary * 4 }}class PerformanceA { caculate(salary) { return salary * 3 }}class PerformanceB { caculate(salary) { return salary * 2 }}接下来定义奖金类:
class Bonus { constructor(salary, strategy) { // 原始工资 this.salary = salary // 绩效等级对应的策略对象 this.strategy = strategy }
setSalary(salary) { this.salary = salary }
setStrategy(strategy) { this.strategy = strategy }
getBonus() { if (!this.strategy) { throw new Error(`没有设置 ${strategy} 属性`) } return this.strategy.caculate(this.salary) }}
const bonus = new Bonus()bonus.setSalary(10000)bonus.setStrategy(new PerformanceS())console.log(bonus.getBonus())bonus.setStrategy(new PerformanceA())console.log(bonus.getBonus())我们先创建了一个 bonus 对象,给它设置了一些原始工资数额,再传入某个计算奖金的策略对象进行保存,调用 getBonus 方法计算奖金,其实 bonus 对象本身没有能力计算,而是把请求委托给之前保存好的策略对象。
JavaScript 版本的策略模式
上面是传统的面向对象语言的实现,JavaScript 中,函数也是对象,我们更直接的做法就是把 strategy 直接定义为函数。
const strategies = { "S": salary => { return salary * 4 }, "A": salary => { return salary * 3 }, "B": salary => { return salary * 2 }}Context 也没有必要用 Bonus 类来表示,依然用 calculateBonus 函数充当 Context 来接受用户的请求。
const calculateBonus = (level, salary) => { return strategies[level](salary)}
console.log(calculateBonus('S', 6000))console.log(calculateBonus('B', 10000))这样代码结构变得更简洁了呢,舒适!
多态在策略模式的体现
使用策略模式实现缓存动画
实现动画效果的原理
使用 JavaScript 实现动画效果的原理和动画片的制作类似,动画片是把一些差距不大的原画以较快的帧数播放,来达到视觉上的动画效果。在 JavaScript 中,可以通过连续改变元素的某个 CSS 属性,比如 left、top、background-position 来实现动画效果
思路和准备工作
目标是编写一个动画类和缓动算法,让小球以各种各样的缓动效果在页面中运动。运动开始之前需要提前记录一些有用信息:
- 动画开始时,小球所在的原始位置
- 小球移动的目标位置
- 动画开始时的准确时间点
- 小球持续运动的时间
我们可以用 setInterval 创建一个定时器,定时器每隔 19ms 循环一次。在定时器的每一帧里,我们需要把动画已消耗的时间、小球原始位置、小球目标位置和动画持续的总时间等信息传入缓动算法。算法通过这些参数,计算出小球当前应该所在的位置,最后再更新 dom 元素的 CSS 属性,使小球运动起来。
让小球运动起来
先了解一下常见的缓动算法,算法接受 4 个参数,这 4 个参数的含义分别是:
- 动画已消耗的时间
- 小球原始位置
- 小球目标位置
- 动画持续的总时间。
返回值是动画元素应该处在的当前位置。
const tween = { linear: (t, b, c, d) => { return c * t / d + b }, easeIn: (t, b, c, d) => { return c * (t /= d) * t + b }, strongEaseIn: (t, b, c, d) => { return c * (t /= d) * t * t * t * t + b }, strongEaseOut: (t, b, c, d) => { return c * ((t = t / d - 1) * t * t * t * t + 1) + b }, sineaseIn: (t, b, c, d) => { return c * (t /= d) * t * t + b }, sineaseOut: (t, b, c, d) => { return c * ((t = t / d - 1) * t * t + 1) + b }}<div id="wrapper" style="position: absolute; background: green;"> 我是 div </div>接下来定义 Animate 类,Animate 的构造函数接受一个参数,即将运动起来的 dom 节点。
class Animate { constructor(dom) { // 进行运动的 dom 节点 this.dom = dom; // 动画开始时间 this.startTime = 0 // 动画开始时,dom 节点的位置,即 dom 的初始位置 this.startPos = 0 // 动画结束时,dom 节点的位置,即 dom 的目标位置 this.endPos = 0 // dom 节点需要被改变的 css 属性名 this.propertyName = null // 缓动算法 this.easing = null // 动画持续时间 this.duration = null }
/** * 启动动画,在动画启动的瞬间,记录一些信息,供缓动算法以后用来计算小球的当前位置 * @param {string} propertyName 要改变的 css 属性名 * @param {number} endPos 小球运动的目标位置 * @param {number} duration 动画持续时间 * @param {string} easing 缓动算法 */ start(propertyName, endPos, duration, easing) { // 动画启动时间 this.startTime = new Date this.startPos = this.dom.getBoundingClientRect()[propertyName] this.propertyName = propertyName this.endPos = endPos this.duration = duration this.easing = tween[easing]
// 启动定时器,开始执行动画 const timeId = setInterval(() => { // 如果动画已经结束,清除定时器 if (this.step() === false) { clearInterval(timeId) } }, 19) }
// 小球运动的每一帧要做的事情,调用更新 css 属性的方法 step() { // 获取当前时间 const t = new Date // 如果当前时间大于动画开始时间加上动画持续时间之和,说明动画已经结束,需要修正小球位置。 // 因为这一帧开始之后,小球的位置已经接近了目标文职,但可能不完全等于目标位置。 if (t >= this.startTime + this.duration) { // 更新小球的 css 属性值 this.update(this.endPos) // 通知 start 方法清除定时器 return false } // 小球的当前位置 const pos = this.easing(t - this.startTime, this.startPos,this.endPos - this.startPos, this.duration) this.update(pos) }
update(pos) { this.dom.style[this.propertyName] = pos + 'px' }}
const div = document.getElementById('wrapper')const animate = new Animate(div)animate.start('left', 500, 1000, 'strongEaseOut')设计原则验证
- 不同策略,分开处理,而不是混合在一起
- 符合开放封闭原则