设计模式-策略模式

策略模式

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

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

第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。

第二个部分是环境类 Context,Context 接受客户的请求,随后把请求委托给某一个策略类。

Context 中要维持对某个策略对象的引用。

实现

计算奖金

使用策略模式计算奖金
// 先把每种绩效的计算规则都封装在对应的策略类里面
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.setSrategy = function (strategy) {
  this.strategy = strategy // 设置员工绩效等级对应的策略对象
}
// 取得奖金数额
Bonus.prototype.getBonus = function () {
  return this.strategy.calculate(this.salary) // 把计算奖金的操作委托给对应的策略对象
}
// 计算奖金
var bonus = new Bonus()
bonus.setSalary(10000)
bonus.setSrategy(new performanceS()) // 设置策略对象
console.log('performanceS', bonus.getBonus()) // 获取奖金
bonus.setSrategy(new performanceA()) // 替换策略对象
console.log('performanceA', bonus.getBonus())

策略模式的实现:定义一系列的算法,把它们各自封装成策略类,算法被封装在策略类内部的方法里。在客户对Context发起请求的时候,Context总是把请求委托给这些策略对象中间的某一个进行计算。

使用策略模式之后,代码变得更加清晰,各个类的职责更加鲜明。

使用对象的方式改写
// 对象改写
var strategies = {
  S: function (salary) {
    return salary * 4
  },
  A: function (salary) {
    return salary * 3
  },
  B: function (salary) {
    return salary * 2
  }
}
var calculateBonus = function (level, salary) {
  return strategies[level](salary)
}
console.log('calculateBonus S', calculateBonus('S', 10000))
console.log('calculateBonus A', calculateBonus('A', 10000))

动画

缓动动画
// 缓动动画
// 参数的含义分别是动画已消耗的时间、小球原始位置、小球目标位置、动画持续的总时间
// 返回的值则是动画元素应该处在的当前位置
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
  }
}
Animate 类

利用这个动画类和一些缓动算法就可以让小球运动起来。

使用策略模式把算法传入动画类中,来达到各种不同的缓动效果,这些算法都可以轻易地被替换为另外一个算法,这是策略模式的经典运用之一。

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

// Animate类
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 = 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.step = function () {
  var t = +new Date // 取得当前时间
  // 如果当前事件大于动画开始时间加上动画持续时间之和,说明动画已经结束,此时需要修正小球的位置
  if (t >= this.startTime + this.duration) {
    this.update(this.endPos) // 更新小球的CSS属性值
    return false // start函数清除定时器
  }
  var pos = this.easing(t - this.startTime, this.startPos, this.endPos - this.startPos, this.duration)
  // pos为小球当前位置
  this.update(pos) // 更新小球的CSS属性值
}
// 更新小球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')

从定义上看,策略模式就是用来封装算法的。

在业务规则中使用,只要这些业务规则指向的目标一致,并且可以被替换使用,就可以用策略模式来封装。

表单校验

使用策略模式可以仅仅通过配置的方式完成一个表单的校验,这些校验规则也可以复用在程序的任何地方,还能作为插件的形式,方便地移植到其他项目中。

// 表单验证
// 校验逻辑封装为策略对象
var strategies = {
  // 不为空
  isNotEmpty: function (value, errorMsg) {
    if (value === '') {
      return errorMsg
    }
  },
  // 限制最小长度
  minLength: function (value, length, errorMsg) {
    if (value.length < length) {
      return errorMsg
    }
  },
  // 校验手机号码格式
  isMobile: function (value, errorMsg) {
    if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
      return errorMsg
    }
  }
}
// validator类作为context
var Validator = function () {
  this.cache = [] // 保存校验规则
}
// 仅验证一条规则
// Validator.prototype.add = function(dom, rule, errorMsg){
// 冒号前面的minLength代表客户挑选的strategy对象,冒号后面的数字6表示在校验过程中所必需的一些参数。
// 'minLength:6’的意思就是校验registerForm.password这个文本输入框的value最小长度为6。
// 如果这个字符串中不包含冒号,说明校验过程中不需要额外的参数信息。
//   var ary = rule.split(':') // 把strategy和参数分开
//   this.cache.push(function(){ // 把校验的步骤用控函数包装起来,并且放入cache
//     var strategy = ary.shift() // 用户挑选的strategy
//     ary.unshift(dom.value) // 把input的value添加进参数列表
//     ary.push(errorMsg)
//     return strategies[strategy].apply(dom, ary)
//   })
// }
// 多条验证规则
Validator.prototype.add = function (dom, rules) {
  var self = this
  for (var i = 0, rule; rule = rules[i++];) {
    (function (rule) {
      var strategyAry = rule.strategy.split(':')
      var errorMsg = rule.errorMsg
      self.cache.push(function () {
        var strategy = strategyAry.shift()
        strategyAry.unshift(dom.value)
        strategyAry.push(errorMsg)
        return strategies[strategy].apply(dom, strategyAry)
      })
    })(rule)
  }
}
Validator.prototype.start = function () {
  for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
    var msg = validatorFunc() // 开始校验,并取得校验后的返回信息
    if (msg) { // 如果有确切的返回值,说明校验没有通过
      return msg
    }
  }
}

// ================================== 使用 ======================================
var validatorFunc = function () {
  var validator = new Validator() // 创建一个validator对象
  // 添加一些校验规则, 单条验证规则的使用
  // validator.add(registerForm.userName, 'isNotEmpty', '用户名不能为空')
  // validator.add(registerForm.password, 'minLength:6', '密码长度不能少于6位')
  // validator.add(registerForm.phoneNumber, 'isMobile', '手机号码格式不正确')
  // 多条规则的验证
  validator.add(registerForm.userName, [{
    strategy: 'isNotEmpty',
    errorMsg: '用户名长度不能为空'
  }, {
    strategy: 'minLength:10',
    errorMsg: '用户名长度不能小于10位'
  }])
  var errorMsg = validator.start() // 获得校验结果
  return errorMsg // 返回校验结果
}

var registerForm = document.getElementById('registerForm')
registerForm.onsubmit = function () {
  var errorMsg = validatorFunc() // 如果errorMsg有确切发返回值,说明未通过校验
  if (errorMsg) {
    console.log(errorMsg)
    return false // 阻止表单提交
  }
}

优缺点

优点

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

缺点

  1. 使用策略模式会在程序中增加许多策略类或者策略对象。
  2. 要使用策略模式,必须了解所有的 strategy,必须了解各个 strategy 之间的不同点,这样才能选择一个合适的 strategy。strategy 要向客户暴露它的所有实现,这是违反最少知识原则的。

应用与实践

在以类为中心的传统面向对象语言中,不同的算法或者行为被封装在各个策略类中,Context 将请求委托给这些策略对象,这些策略对象会根据请求返回不同的执行结果,这样便能表现出对象的多态性。

在函数作为一等对象的语言中,策略模式是隐形的。strategy 就是值为函数的变量。

在 JavaScript 语言的策略模式中,策略类往往被函数所代替,这时策略模式就成为一种“隐形”的模式。