2_策略模式

pleaseAnswer / 2023-05-09 / 原文

1 简介

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

2 使用策略模式计算奖金

以年终奖的计算为例

公司的年终奖是根据员工的工资基数和年底绩效情况来发放的。你负责编写代码来给财务计算员工的年终奖

1. 代码实现

/**
 * caculateBonus 计算每个人的奖金数额
 * @param performanceLevel 员工的绩效考核等级
 * @param salary 员工的工资数额
 * @return 员工的年终奖
 */
function caculateBonus(performanceLevel, salary) {
  if(performanceLevel === 'S') {
    return salary * 4
  }
  if(performanceLevel === 'A') {
    return salary * 3
  }
  if(performanceLevel === 'B') {
    return salary * 2
  }
}
存在的问题
  • 包含多个if-else语句,覆盖了所有的逻辑分支
  • 函数缺乏弹性, 增加新的绩效等级需要深入函数内部 -- 违反开放封闭原则
  • 函数复用性差

2. 使用组合函数重构

  • 把各种算法封装到各自的函数里面
function performanceS(salary) {
  return salary * 4
}
function performanceA(salary) {
  return salary * 3
}
function performanceB(salary) {
  return salary * 2
}
function caculateBonus(performanceLevel, salary) {
  if(performanceLevel === 'S') {
    return performanceS(salary)
  }
  if(performanceLevel === 'A') {
    return performanceA(salary)
  }
  if(performanceLevel === 'B') {
    return performanceB(salary)
  }
}
存在的问题
  • calculateBonus 函数有可能越来越庞大,而且在系统变化的时候缺乏弹性

3. 使用策略模式重构

将不变的部分和变化的部分隔开

策略模式
  • 思想: 定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换

  • 目的:将算法的使用与算法的实现分离开来

  • 一个基于策略模式的程序至少由两部分组成

    1. 一组策略类--策略类封装了具体的算法,并负责具体的计算过程
    2. 环境类Context--Context接受客户的请求,随后把请求委托给某一个策略类[说明 Context 中要维持对某个策略对象的引用]
第一步:先把每种绩效的计算规则都封装在对应的策略类里面
class PerformanceS {
  constructor() {}
  calculate(salary) {
    return salary * 4
  }
}
class PerformanceA {
  constructor() {}
  calculate(salary) {
    return salary * 3
  }
}
class PerformanceB {
  constructor() {}
  calculate(salary) {
    return salary * 2
  }
}
第二步:定义奖金类 Bonus
class Bonus {
  constructor() {
    this.salary = null
    this.strategy = null
  }
  setSalary(salary) {
    this.salary = salary
  }
  setStrategy(strategy) {
    this.strategy = strategy
  }
  getBonus() {
    return this.strategy.calculate(this.salary)
  }
}
第三步:计算
let bonus = new Bonus()
bonus.setSalary(10000)
bonus.setStrategy(new PerformanceS()) // 设置策略对象
console.log(bonus.getBonus()) // 40000
bonus.setStrategy(new PerformanceA()) // 设置策略对象
console.log(bonus.getBonus()) // 30000

通过策略模式重构之后,代码变得更加清晰,各个类的职责更加鲜明

3 js版本的策略模式

之前的strategy对象从各个策略类中创建而来,这是模拟一些传统面向对象语言的实现。实际上在js中,函数也是对象。

1. 把strategy直接定义为对象
let strategies = {
  "S": salary => salary * 4,
  "A": salary => salary * 3,
  "B": salary => salary * 2,
}
2. 直接使用calculateBonus函数充当Context来接收用户的请求
let calculateBonus = (level, salary) => {
  return strategies[level](salary)
}
calculateBonus("S", 10000) // 40000
calculateBonus("A", 20000) // 60000

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

编写一个动画类和一些缓动算法让小球以各种各样的缓动效果在页面中运动

1. 实现动画效果的原理

  • 在js中通过连续改变元素的某个CSS属性来实现动画效果

left top background

2. 准备工作

在运动开始之前,需要提前记录一些有用的信息:

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

实现思路:

  1. 通过setTimeout每隔19ms将动画已消耗的时间、小球原始位置、小球目标位置、动画持续时间等信息传入缓动算法
  2. 算法计算出小球当前位置
  3. 更新小球对应css属性

3. 让小球动起来

第一步:实现缓动算法
/**
 * @param t 动画已消耗的时间
 * @param b 小球的原始位置
 * @param c 小球的目标位置
 * @param d 动画的持续时间
 * @return 动画元素应该处在的当前位置
 * */ 
let tween = {
  linear: (t, b, c, d) => c*t/d + b,
  easeIn: (t, b, c, d) => c*(t/=d) * t + b,
  strongEaseIn: (t, b, c, d) => c*(t/=d) * t^4 + b,
  strongEaseOut: (t, b, c, d) => c * ((t = t/d-1) * t^4 + 1) + b,
  sineaseIn: (t, b, c, d) => c * (t/=d) * t^2 + b,
  sineaseOut: (t, b, c, d) => c * ((t = t/d-1) * t^2 + 1) + b
}
第二步:实现动画类 -- Animate
class Animate {
  // 参数:即将运动起来的dom节点
  constructor(dom) {
    this.dom = dom // dom节点
    this.startTime = 0 // 动画开始时间
    this.startPos = 0 // dom起始位置
    this.endPos = 0 // dom目标位置
    this.propertyName = null // dom需改变的属性名
    this.easing = null // 缓动算法
    this.duration = null // 动画持续时间
  }
  start() {}
  step() {}
  update() {}
}
第三步:启动动画 -- Animate-start

在动画被启动的瞬间,要记录一些信息供缓动算法在以后计算小球当前位置时使用 + 记录完之后要启动定时器

/**
 * start 启动动画
 * @param propertyName dom需改变的属性名
 * @param endPos dom目标位置
 * @param duration 动画持续时间
 * @param 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]

  let self = this
  // 启动定时器执行动画
  let timeId = setInterval(() => {
    // 动画已完成,清除定时器
    if(self.step() === false) {
      clearInterval(timeId)
    }
  }, 19)
}
第四步:小球的行为 -- Animate-step

小球运动的每一帧要做的事情,该方法负责计算小球的当前位置 + 调用更新css属性值的方法

step() {
  let t = +new Date()
  // 动画已结束,修正小球位置
  if(t >= this.startTime + this.duration) {
    // 更新小球的css属性
    this.update(this.endPos)
    return false
  }
  // 当前小球的位置
  let pos = this.easing(
    t-this.startTime,
    this.startPos,
    this.endPos-this.startPos,
    this.duration
  )
  // 更新小球的css属性
  this.update(pos)
}
第五步:更新小球css属性 -- Animate-update
update(pos) {
  this.dom.style[this.propertyName] = pos + 'px'
}

策略模式并不复杂,关键是如何从策略模式的实现背后,找到封装变化、委托和多态性这些思想的价值

5 表单校验

在表单里,用户输入的数据提交给后台之前常常要做一些校验工作,可以避免因为提交不合法数据而带来的不必要网络开销

你正在编写一个注册的页面,在点击注册按钮之前,有如下几条校验逻辑:

  1. 用户名不能为空
  2. 密码长度不能少于 6 位
  3. 手机号码必须符合格式

1. 代码实现

registerForm.onsubmit = () => {
  if(registerForm.userName.value === '') {
    alert('用户名不能为空')
    return false
  }
  if(registerForm.password.value.length < 6) {
    alert('密码不能少于6位')
    return false
  }
  if(!/(^1[3|5|8][0-9]{9}$)/.test(registerForm.phoneNumber.value)) {
    alert('手机号码格式不正确')
    return false
  }
}
存在的问题
  • 包含多个if-else语句,这些语句需要覆盖多有的校验规则
  • 函数缺乏弹性,增加新的校验规则需要深入函数内部 -- 违反开放封闭原则
  • 复用性差

2. 用策略模式重构

第一步:将校验逻辑封装成策略对象
let strategies = {
  isNotEmpty: (value, errMsg) => {
    if(value === '') {
      return errMsg
    }
  },
  minLength: (value, length, errMsg) => {
    if(value.length < length) {
      return errMsg
    }
  },
  isMobile: (value, errMsg) => {
    if(!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
      return errMsg
    }
  }
}

准备实现 Validator 类。Validator 类作为 Context 负责接收用户的请求并委托给 strategy 对象。在实现 Validator 类之前有必要提前了解用户是如何向 Validator 类发送请求的,这有助于我们知道如何编写 Validator 类的代码

第二步:用户如何向Validator类发送请求
function validataFunc() {
  let validator = new Validator()
  // 往validator对象中添加一些校验规则
  validator.add(registerForm.userName, 'isNotEmpty', '用户名不能为空')
  validator.add(registerForm.password, 'minLength: 6', '密码长度不能少于6')
  validator.add(registerForm.phoneNumber, 'isMobile', '手机号格式不正确')
  // 启动校验
  let errMsg = validator.start()
  return errMsg
}

validator.add() 接收3个参数:

  1. registerForm.password 为参与校验的input输入框
  2. minLength: 6 冒号前的minLength表示所使用的strategy对象,冒号后的6表示校验过程中所必需的一些参数。若该字符串不包含冒号,说明校验过程中不需要额外的参数信息
  3. 第3个参数表示校验未通过时返回的错误信息

validator.start() 返回了errorMsg说明本次校验没通过,需让registerForm.onsubmit() 返回false来阻止表单的提交

第三步:实现Validator类
class Validator {
  constructor() {
    this.cache = [] // 保存校验规则
  }
  add(dom, rule, errMsg) {
    let ary = rule.split(':')
    this.cache.push(() => {
      let strategy = ary.shift()
      ary.unshift(dom.value)
      ary.push(errMsg)
      return strategies[strategy].apply(dom, ary)
    })
  }
  start() {
    for(let validatorFunc in this.cache) {
      let msg = validatorFunc()
      if(msg) {
        return msg
      }
    }
  }
}

重构之后仅通过配置便可完成一个表单的校验,这些校验规则也可以复用在其它任何地方,也能作为插件被移植到其他项目中

3. 给某个文本框添加多重校验规则

第一步:用户如何向Validator类发送请求
validator.add(registerForm.userName, [{
  strategy: 'isNotEmpty',
  errMsg: '用户名不能为空'
}, {
  strategy: 'minLength:6',
  errMsg: '用户长度不能小于10位'
}])
第二步:改进Validator类
class Validator {
  constructor() {
    this.cache = [] // 保存校验规则
  }
  add(dom, rules) {
    let self = this
    for(let rule in rules) {
      (function(rule) {
        let strategyAry = rule.strategy.split(':')
        let errMsg = rule.errMsg;
        self.cache.push(() => {
          let strategy = strategyAry.shift()
          strategyAry.unshift(dom.value)
          strategyAry.push(errMsg)
          return strategies[strategy].apply(dom, strategyAry)
        })
      })(rule)
    }
  }
  start() {
    for(let validatorFunc in this.cache) {
      let msg = validatorFunc()
      if(msg) {
        return msg
      }
    }
  }
}

6 策略模式的优缺点

优点

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

缺点

  1. 必须了解所有的 strategy
  2. strategy 要暴露其所有实现,违反了最少知识原则