JavaScript设计模式-策略模式

191 阅读4分钟

策略模式

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

一、策略模式计算奖金

假设年终奖根据工资基数和绩效等级来发放。比如,绩效为 S 的人可以获得 4 倍工资,绩效为 A 的人可以获得 3 倍工资,绩效为 B 的人可以获得 2 倍工资。这时,可以编写代码来计算年终奖,需要接受两个参数:工资基数和绩效等级。

1.1、不使用策略模式

代码如下:

var calculateBonus = function (salary, level) {
  if (level === 'S') {
    return 4 * salary
  }
  if (level === 'A') {
    return 3 * salary
  }
  if (level === 'B') {
    return 2 * salary
  }
}

calculateBonus(10000, 'S') // 40000
calculateBonus(8000, 'A') // 24000
calculateBonus(6000, 'B') // 12000

上面这段代码十分简单,但是存在以下几个缺点:

  • if-else语句很多,函数太庞大
  • 违反开放-封闭原则,如果需要增加等级 C,需要深入函数内部去修改
  • 复用性差,如果需要在其他地方使用,那么只能复制粘贴过去

1.2、使用策略模式重构代码

策略模式的目的就是将算法的使用与算法的实现分离开来。

一个基于策略模式的程序至少由两部分组成。第一部分是一组策略类,策略封装了具体的算法,并负责具体的计算过程;第二部分是环境类 Context,Context 接收客户的请求,随后把请求委托给一个策略类。所以,Context 中要维持对某个策略对象的引用

下面是使用策略模式重构的代码:

var performanceS = function () {}

performanceS.prototype.calculate = function (salary) {
  return 4 * salary
}

var performanceA = function () {}

performanceA.prototype.calculate = function (salary) {
  return 3 * salary
}

var performanceB = function () {}

performanceB.prototype.calculate = function (salary) {
  return 2 * salary
}

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.getBonus = function () {
  if (!this.strategy) {
    throw new Error('未设置 strategy 属性')
  }
  return this.strategy.calculate(this.salary)
}

var bonus = new Bonus()

bonus.setSalary(1000)
bonus.setStrategy(new performanceS())

console.log(bonus.getBonus()) // 4000

bonus.setStrategy(new performanceA())
console.log(bonus.getBonus()) // 3000

策略模式的思想就是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。说的详细一点就是:定义一系列的算法,把它们各自封装成策略类,算法被封装在策略类内部的方法里。客户对 Context 发起请求的时候,Context 总是把请求委托给这些策略对象中间的某一个进行计算。

重构后的代码,变得更加清晰,各个类的职责更加明确。

1.3、JavaScript 版本的策略模式

在 JavaScript 中,函数也是对象,所以可以直接把 strategy 定义为函数:

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

var calculateBonus = function (salary, level) {
  return strategies[level](salary)
}

calculateBonus(10000, 'S') // 40000
calculateBonus(8000, 'A') // 24000
calculateBonus(6000, 'B') // 12000

二、策略模式进行表单校验

假设一个注册的页面,在点击注册按钮之前,对输入的信息进行校验:

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

2.1、不使用策略模式

代码如下:

<form action="https://xxx.com/register" id="registerForm" method="POST">
  请输入用户名:<input type="text" name="userName" /> 请输入密码:<input
    type="password"
    name="password"
  />
  请输入手机号码:<input type="text" name="phone" />
  <button>注册</button>
</form>
<script>
  var registerForm = document.getElementById('registerForm')

  registerForm.onsubmit = function () {
    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.phone.value)) {
      alert('手机号码格式不正确')
      return false
    }
  }
</script>

上面的代码显然存在着缺点:

  • registerForm.onsubmit 函数比较庞大,有很多的if-else语句
  • registerForm.onsubmit 缺乏弹性,如果需要修改密码长度至少 8 位,那么必须深入函数内部修改,违反开放-封闭原则
  • 复用性差,如果在其它地方也需要这样一个校验,那么就只能复制粘贴过去了

2.2、使用策略模式重构代码

使用策略模式重构后的代码如下:

var strategies = {
  isNonEmpty: function (value, errMsg) {
    if (value === '') {
      return errMsg
    }
  },
  minLength: function (value, length, errMsg) {
    if (value.length < length) {
      return errMsg
    }
  },
  isMobile: function (value, errMsg) {
    if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
      return errMsg
    }
  },
}

var Validator = function () {
  this.cache = []
}
Validator.prototype.add = function (dom, rule, errMsg) {
  var ary = rule.split(':')
  this.cache.push(function () {
    var strategy = ary.shift()
    ary.unshift(dom.value)
    ary.push(errMsg)
    return strategies[strategy].apply(dom, ary)
  })
}
Validator.prototype.start = function () {
  for (var i = 0, validatorFunc; (validatorFunc = this.cache[i++]); ) {
    var msg = validatorFunc()
    if (msg) {
      return msg
    }
  }
}

var validateFunc = function () {
  var validator = new Validator()
  validator.add(registerForm.userName, 'isNonEmpty', '用户名不能为空')
  validator.add(registerForm.password, 'minLength:6', '密码不能少于 6 位')
  validator.add(registerForm.phone, 'isMobile', '手机号格式不正确')
  var errorMsg = validator.start()
  return errorMsg
}

var registerForm = document.getElementById('registerForm')
registerForm.onsubmit = function () {
  var errMsg = validateFunc()
  if (errMsg) {
    alert(errMsg)
    return false
  }
}
  • 首先需要将校验逻辑都封装成策略对象
  • Validator 类作为 Context,负责接收用户的请求并委托给 strategy 对象
  • 通过 validator.add 方法往 validator 对象中添加一些校验规则
  • 添加完规则之后,会调用 validator.start 方法来启动校验,通过是否有 errMsg 来判断是否阻止提交信息

重构后的代码,仅仅通过“配置”就可以完成一个表单的校验,而且可以复用到其他地方。

三、总结

  • 策略模式的优点:

    • 利用组合、委托和多态等技术和思想,有效避免多重条件选择语句。
    • 对开放-封闭原则的完美支持,将算法封装在独立的 strategy 中,使得易于切换,易于扩展。
    • 复用性好。
  • 策略模式的缺点:

    • 会在程序中增加许多策略类或者策略对象。
    • 使用策略模式需要了解所有的 strategy,此时,strategy 要向客户暴露它的所有实现,违反最少知识原则。