『面试的底气』—— 设计模式之策略模式

定义

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

实现一个简单的策略模式

举一个例子来说明,很多公司每个月都绩效考核,考核完后还要发放绩效奖金,例如绩效为 S 的绩效奖金有0.2倍工资,绩效为 A 的绩效奖金有0.1倍工资,绩效为 B 的绩效奖金是 0 倍工资,绩效为 C 的绩效奖金是 -0.1 倍工资。假设人事部要求我们提供一段代码,来方便他们计算员工的每个月的绩效奖金。

cosnt calculatebonus = (level, salary) =>{
  if (level === 'S') {
    return salary * 0.2;
  }
  if (level === 'A') {
    return salary * 0.1;
  }
  if (level === 'B') {
    return salary * 0;
  }
  if (level === 'C') {
    return salary * -0.1;
  }
};
calculatebonus('S', 20000); // 输出:4000
calculatebonus('A', 6000); // 输出:600 

上述代码十分简单实现人事部的需求,但是存在着显而易见的缺点。

  • calculatebonus函数比较庞大,包含了很多if-else语句,这些语句需要覆盖所有的逻辑 分支。
  • calculatebonus函数可扩展性差,如果增加了一种新的绩效等级 D,或者想把绩效 C 的奖金 系数改为 0,那我们必须深入calculatebonus函数的内部实现,这是违反开放-封闭原则的。
  • 算法的复用性差,如果在其它地方需要重用这些计算绩效奖金的算法呢?难道用复制黏贴拷贝过去。

用策略模式就可与很好的解决上述的缺点。因此,我们需要重构这段代码。

策略模式指的是定义一系列的算法,把它们一个个封装起来,将算法的使用与算法的实现分离开来。将不变的部分和变化的部分隔开是每个设计模式的主题,策略模式也不例外。

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

那么第一步先把计算绩效奖金的算法封装出来,做出一个个策略类。

class LevelS {
  calculate(salary) {
    return salary * 0.2;
  }
}
class levelA {
  calculate(salary) {
    return salary * 0.1;
  }
}
class levelB {
  calculate(salary) {
    return salary * 0;
  }
}
class levelC {
  calculate(salary) {
    return salary * -0.1;
  }
}

然后创建一个环境类(计算绩效工资的类Bonus

class Bonus {
  constructor(...arguments) {
    this.salary = null;//工资
    this.strategy = null;//计算绩效工资的策略对象
  }
  setSalary(salary) {
    this.salary = salary; // 设置工资
  }
  setStrategy(strategy) {
    this.strategy = strategy; // 设置计算绩效工资的策略对象
  }
  getBonus(){
    return this.strategy.calculate(this.salary);//计算绩效工资
  }
}

如何使用呢?再回顾一下策略模式的定义:“定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换”。再解释详细一点。定义一系列的算法,把它们各自封装成策略类,算法被封装在策略类内部的方法里。在客户对环境类发起请求的时候,环境类总是把请求委托某个策略类,调用策略类中的算法计算。

在上面的例子中,Bonus类就是环境类,客户对环境类发起请求就是实例化一个环境类。调用bonus.setStrategy来委托给某个策略类。

const bonus = new Bonus();
bonus.setSalary( 20000 );//设置工资
bonus.setStrategy( new LevelS() ); // 设置策略对象
console.log( bonus.getBonus() ); // 输出:4000
bonus.setSalary( 6000 );//设置工资
bonus.setStrategy( new LevelA() ); // 设置策略对象
console.log( bonus.getBonus() ); // 输出:600

用类来实现代理模式中的虚拟代理,而不使用类的方式也可以实现代理模式,比如用高阶函数也可以实现一个代理模式。本文就用高阶函数来动态创建一个缓存代理来再次介绍代理模式。

缓存代理

缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参 数跟之前一致,则可以直接返回前面存储的运算结果。

高阶函数简单的可以理解为把一个函数作为参数传入一个函数中。那么如何用高阶函数动态创建一个缓存代理,就是实现一个专门用于创建缓存代理的工厂函数,把要代理的函数传入这个工厂函数,返回一个代理函数。

const createProxyFactory = (fn) =>{
  let cache = {};
  return function () {
    const args = Array.prototype.join.call(arguments, ',');
    if (args in cache) {
      return cache[args];
    }
    return cache[args] = fn.apply(this, arguments);
  }
};

比如现在要为一个计算乘积的mult函数实现缓存代理,

const mult = function () {
  let a = 1;
  for (let i = 0, l = arguments.length; i < l; i++) {
    a = a * arguments[i];
  }
  return a;
};

mult函数当作参数传入createProxyFactory这个创建缓存代理的函数中即可为mult函数实现缓存代理。

小结

代理模式可以符合单一职责原则,使被代理函数的职责单一,使被代理函数高内聚。可以满足开放-封闭原则,使得被代理函数封闭,对其扩展新功能在代理函数中实现,使被代理函数低耦合。

虽然代理模式非常有用,但我们在编写业务代码的时候,往往不需要去预先猜测是否需要使用代理模式。 当真正发现不方便直接访问某个对象或改变某个函数的内部代码时,再编写代理也不迟。

『面试的底气』—— 设计模式之策略模式的相似文章

『面试的底气』—— 详细说明订阅-发布模式在下面场景的应用,设计模式之发布-订阅模式分析『面试的底气』—— 设计模式遵循的原则,设计模式之单一职责原则分析『面试的底气』—— 设计模式之代理模式分析