js设计模式之策略模式

策略模式的定义是:定义一系列的算法(这些算法目标一致),把它们一个个封装起来,而且使它们能够相互替换。javascript

好比要实现从上海到广州,既能够坐火车,又能够坐高铁,还能够坐飞机。这取决与我的想法。在这里,不一样的到达方式就是不一样的策略,我的想法就是条件。html

1.计算奖金

以计算奖金为例,绩效为S的年终奖是4倍工资,绩效为A的年终奖是3倍工资,绩效为B的年终奖是2倍工资。那么这里奖金取决于两个条件,绩效和薪水。最初编码实现以下:java

const calculateBonus =  (performanceLevel, salary) => {
  if(performanceLevel === 'S') {
    return salary * 4;
  }

  if(performanceLevel === 'A') {
    return salary * 3;
  }

  if(performanceLevel === 'B') {
    return salary * 2;
  }
}
复制代码

这段代码十分简单,但存在显而易见的缺点。算法

  1. if语句过多,须要涵盖全部的条件。
  2. 弹性差,若是新增绩效C,那么须要什么calculateBonus的内部实现去修改代码,不符合开放封闭原则。
  3. 复用性差,计算奖金的算法不能直接复用,除非复制粘贴。

2.使用策略模式

策略模式是指定义一系列的算法,并将它们封装起来,这很符合开闭原则。策略模式的目的就是将算法的使用和算法的实现分离出来。bash

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

如今使用策略模式来重构以上代码,第一个版本是基于class,第二个版本是基于函数。dom

2.1基于class

class PerformanceS {
  calculate(salary) {
    return salary * 4;
  }
}

class PerformanceA {
  calculate(salary) {
    return salary * 3;
  }
}

class PerformanceB {
  calculate(salary) {
    return salary * 2;
  }
}

class Bonus {
  constructor(strategy, salary) {
    this.strategy = strategy;
    this.salary = salary;
  }

  getBonus() {
    if(!this.strategy) {
      return -1;
    }
    return this.strategy.calculate(this.salary);
  }
}

const bonus = new Bonus(new PerformanceA(), 2000);
console.log(bonus.getBonus()) // 6000
复制代码

它没有上述的三个缺点。在这里,有三个策略类,分别是PerformanceS、Performance A、PerformanceB。这里的context就是Bonus类,它接受客户的请求(bonus.getBonus),将请求委托给策略类。它保存着策略类的引用。函数

2.2基于函数

上述中,每个策略都是class,实际上,class也是一个函数。这里,能够直接用函数实现。post

const strategies = {
  'S': salary => salary * 4,
  'A': salary => salary * 3,
  'B': salary => salary * 2,
}

const getBonus = (performanceLevel, salary) => strategies[performanceLevel](salary)

console.log(getBonus('A', 2000)) // 6000
复制代码

3.多态在策略模式中的体现

经过使用策略模式重构代码,消除了程序中大片的条件语句。全部和奖金相关的逻辑都不在Context中,而是分布在各个策略对象中。Context并无计算奖金的能力,当它接收到客户的请求时,它将请求委托给某个策略对象计算,计算方法被封装在策略对象内部。当咱们发起“得到奖金”的请求时,Context将请求转发给策略类,策略类根据客户参数返回不一样的内容,这正是对象多态性的体现,这也体现了策略模式的定义--“它们能够相互替换”。动画

4.计算小球的缓动动画

咱们的目标是编写一个动画类和缓动算法,让小球以各类各样的缓动效果在页面中进行移动。

很明显,缓动算法是一个策略对象,它有几种不一样的策略。这些策略函数都接受四个参数:动画开始的位置s、动画结束的位置e、动画已消耗的时间t、动画总时间d。

const tween = {
  linear: (s, e, t, d) => { return e*t/d + s },
  easeIn: () => { /* some code */ },
  easeOut: () => { /* some code */ },
  easeInOut: () => { /* some code */ },
}
复制代码

页面上有一个div元素。

<div id='div' style='position: absolute; left: 0;'></div>
复制代码

如今要让这个div动起来,须要编写一个动画类。

const tween = {
  linear: () => { /* some code */ },
  easeIn: () => { /* some code */ },
  easeOut: () => { /* some code */ },
  easeInOut: () => { /* some code */ },
}

class Animation {
  constructor(dom) {
    this.dom = dom;
    this.startTime = 0;
    this.startPos = 0;
    this.endPos = 0;
    this.propertyName = null;
    this.easing = null;
    this.duration = null;
  }

  // 开始动画
  start(propertyName, endPos, duration, easing) {
    this.startTime = Date.now();
    // 初始化参数,省略其余
    const self = this;
    // 循环执行动画,若是动画已结束,那么清除定时器
    let timer = setInterval(() => {
      if(self.step() === false) {
        clearInterval(timer);
      }
    }, 1000/60);
  }

  // 计算下一次循环到的时候小球位置
  step() {
    const now = Date.now();
    if(now > this.startTime + this.duration) {
      return false;
    } else {
      // 得到小球在本次循环结束时的位置并更新位置
      // const pos = this.easing();
      // this.update(pos);
    }
  }

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

具体实现略去。这里的Animation类就是环境类Context,当接收到客户的请求(更新小球位置 self.step()),它将请求转发给策略内(this.easing()),策略类进行计算并返回结果。

5.更广义的“算法”

策略模式指的是定义一系列的算法,而且把他们封装起来。上述所说的计算奖金和缓动动画的例子都封装了一些策略方法。

从定义上看,策略模式就是用来封装算法的。但若是仅仅将策略模式用来封装算法,有些大材小用。在实际开发中,策略模式也能够用来封装一些的“业务规则”。只要这些业务规则目标一致,而且能够替换,那么就能够用策略模式来封装它们。以使用策略模式来完成表单校验为例。

6.表单校验

<form action='xxx' id='form' method='post'>
  <input type='text' name='username'>
  <input type='password' name='passsword'>
  <button>提交</button>
</form>
复制代码

验证规则以下:

const form = document.querySelector('form')
form.onsubmit = () => {
  if(form.username.value === '') {
    alert('用户名不能为空')
    return false;
  }

  if(form.password.value.length < 6) {
    alert('密码不能少于6位')
    return false;
  }
}
复制代码

这是一种很常见的思路,和最开始计算奖金同样。缺点也是同样。

6.1使用策略模式重构表单校验

第一步须要把这些校验逻辑封装成策略对象。

const strategies = {
  isNonEmpty: (value, errMsg) => {
    if(value === '') {
      return errMsg
    }
  },
  minLength: (value, errMsg) => {
    if(value.length < minLength) {
      return errMsg
    }
  }
}
复制代码

第二步对表单进行校验。

class Validator {
  constructor() {
    this.rules = [];
  }
  
  add(dom, rule, errMsg) {
    const arr = rule.split(':');
    this.rules.push(() => {
      const strategy = arr.shift();
      arr.unshift(dom.value);
      arr.push(errMsg);
      return strategies[strategy].apply(dom, arr);
    })
  }

  start() {
    for(let i = 0, validatorFunc; validatorFunc = this.rules[i++];) {
      let msg = validatorFunc();
      if(msg) {
        return msg;
      }
    }
  }

}

const form = document.querySelector('form')
form.onsubmit = (e) => {
  e.preventDefault();
  const validator = new Validator();
  validator.add(form.username, 'isNonEmpty', '用户名不能为空');
  validator.add(form.password, 'minLength:6', '密码长度不能小于6位');
  const errMsg = validator.start();
  if(errMsg) {
    alert(errMsg);
    return false;
  }
}
复制代码

上述例子中,校验逻辑是策略对象,其中包含策略的实现函数。Validator类是Context,用于将客户的请求(表单验证)转发到策略对象进行验证。与计算奖金的Bonus不一样的是,这里并无将验证参数经过构造函数传入,而是经过validator.add传入相关验证参数,经过validator.start()进行验证。

策略模式优缺点

  1. 策略模式利用组合、委托和多态等技术和思想,能够有效避免多重选择语句。
  2. 策略模式经过扩展策略类,对开放封闭原则彻底支持,使得它们易于切换和扩展。
  3. 策略模式的算法能够用在其余地方,避免复制。
  4. 策略模式利用组合和委托让Context拥有执行算法的能力,这也是继承的一种更轻便的替代方案。

前三点正是开头实现的计算奖金函数的缺点。 策略模式有一点缺点,不过并不严重。

  1. 会增长策略类或者策略对象,增长了复杂度。可是与Context解耦了,这样更便于扩展。
  2. 使用策略模式,必须了解全部的策略以便选择合适的策略,这是strategies要向客户暴露它的全部实现,不符合最少知识原则。
相关文章
相关标签/搜索