策略模式

201 阅读8分钟

概念

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

案例:使用策略模式计算奖金

假设,公司的年终奖是根据员工的工资基数和年底绩效情况来发放的。如,绩效为S的人年终奖有4倍工资,绩效为A的年终奖有3倍工资,而绩效为B的人年终奖是2倍工资

1.最初的代码实现

编写一个名为calculateBonus的函数来计算每个人的奖金数额。很显然,calculateBonus函数要正确工作,就需要两个参数:员工的工资数额和他的绩效考核等级

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

console.log(calculateBonus('B', 2000))
console.log(calculateBonus('S', 6000))

可以发现,这段代码十分简单,但是存在着显而易见的缺点:

  1. calculateBonus函数比较庞大,包含很多if-else语句,这些语句需要覆盖所有的逻辑分支
  2. calculateBonus函数缺乏弹性,如果增加了一种新的绩效等级C,或者想把绩效S的奖金系数改为5,那么我们必须深入calculateBonus函数内容实现,这是违反开发-封闭原则
  3. 算法的复用性差,如果在程序的其他地方需要重用这些代码,我们的选择只有复制和粘贴

2.使用组合函数重构代码

let performanceS = function (salary) {
    return salary * 4;
}

let performanceA = function (salary) {
    return salary * 3
}

let performanceC = function (salary) {
    return salary * 2
}

let calculateBonus = (performanceleve, salary) => {
    if (performanceleve === 'S') {
        return performanceS(salary)
    }
    if (performanceleve === 'A') {
        return performanceA(salary)
    }
    if (performanceleve === 'B') {
        return performanceC(salary)
    }
}

console.log(calculateBonus('A', 8999))

目前,程序得到了一定的改善,但这种改善非常有限,我们依然没有解决最重要的问题:calculateBonus函数有可能越来越大,而且在系统变化时候缺乏弹性

3.使用策略模式重构代码

将不变的部分和变化的部分隔开是每个设计模式的主题。策略模式的目的就是将算法的使用和算法的实现分离开来。

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

重构上面的代码,第一个版本是模仿传统面向对象语言中的实现。

//策略类
let perfprmanceS = function () { };
perfprmanceS.prototype.calculate = function (salary) {
    return salary * 4;
}
let perfprmanceA = function () { }
perfprmanceA.prototype.calculate = function (salary) {
    return salary * 3;
}
let perfprmanceB = function () { }
perfprmanceB.prototype.calculate = function (salary) {
    return salary * 2
}

// 奖金类Bonus
let Bonus = function () {
    this.salary = null //原始工资
    this.strategy = null//绩效等级对应的策略对象
}

Bonus.prototype.setSalary = function (salary) {
    this.salary = salary//设置员工的原始工资
}
Bonus.prototype.setStarategy = function (strategy) {
    this.strategy = strategy //设置员工绩效等级对应的策略对象
}

Bonus.prototype.getBonus = function () {//获取奖金数额
    if (!this.strategy) {
        throw new Error("未设置strategy")
    }
    return this.strategy.calculate(this.salary);//把计算奖金的操作委托给对应的策略对象
}

let bonus = new Bonus();
bonus.setSalary(1000);
bonus.setStarategy(new perfprmanceS())
console.log(bonus.getBonus())

bonus.setSalary(5000);
bonus.setStarategy(new perfprmanceB())
console.log(bonus.getBonus())

代码重构之后,代码变的更加清晰,各个类的职责更加鲜明

Javascript 版本的策略模式

在之前,我们让strategy对象从各个策略类中创建而来,这是模拟一些传统面向对象语言的实现。实际上在Javascript语言中,函数也是对象,所以更简单和直接的做法是把strategy直接定义为函数

let strategies = {
  S: (salary) => salary * 4,
  A: (salary) => salary * 3,
  B: (salary) => salary * 2,
};

同样,Context也没有必要必须用Bonus类来表示,我们依然用calculateBonus函数充当Context来接受用户的请求。经过改造,代码的结构变得更加简洁

let calculateBonus = (level, salary) => strategies[level](salary);
console.log(calculateBonus("A", 3));

案例:表单验证

从定义上来看,策略模式就是用来封装算法的,但如果把策略模式仅仅用来封装算法,未免有一点大才小用。在实际的开发中,我们通常会把算法的含义扩散开来,使策略模式也可以用来封装一些列业务规则,只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以用策略模式来封装它们

案例介绍

在一个 WBE 项目中,注册,登录,修改用户信息等功能的实现都离不开提交表单

在将用户输入的数据交给后台之前,常常要做一些客户端力所能及的校验工作,比如注册的时候需要校验是否填写了用户名,密码的长度是否符合规定,等等。这样可以避免因为提交不合法数据而带来的不必要网络开销。

假设我们正在编写一个注册页面,在点击注册按钮之前,有几条校验逻辑

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

表单校验的顶一个版本

  <form action="" id="registerForm" method="post">
    请输入用户名:<input type="text" name="userName" />
    请输入密码:<input type="text" name="password" />
    请输入手机号:<input type="text" name="phoneNumber" />
    <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 (phoneNumber.userName.value === '') {
        alert('手机号不能为空')
        return false
      }
    }
  </script>

这是一种很常见的代码编写方式,它的缺点跟计算奖金的最初版本版一模一样

  • registerForm.onsubmit函数比较庞大,包含了很多if-else语句,这些语句需要覆盖所有的校验规则
  • registerForm.onsubmit函数缺乏弹性,如果增加了一种新的校验规则,或者想把密码的长度校验从6改成8,我们都必须深入registerForm.onsubmit函数内部实现,这是违反开发-封闭原则的
  • 算法的服用性差,如果在程序中增加另外一个表单,这个表单也需要进行一些类似的校验,那我们很可能将这些校验逻辑复制得漫天遍野

用策略模式重构表单校验

第一步,将这些校验逻辑都封装成策略对象

let strategise = {
  //不为空
  isNonEmpty: (value, errorMsg) => {
    if (value === "") {
      return errorMsg;
    }
  },
  // 限制最小长度
  minLength: (value, length, errorMsg) => {
    if (value.length < length) {
      return errorMsg;
    }
  },
  // 手机号格式
  isMobile: (value, errorMsg) => {
    if (!/0?(13|14|15|17|18)[0-9]{9}/.test(value)) {
      return errorMsg;
    }
  },
};

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

    let Validator = function () { };
    let validaFunc = function () {
      let validator = new Validator(); //创建一个validator对象
      // 添加一些校验规则
      validator.add(registerForm.userName, 'isNonEmpty', '用户名不能为空');
      validator.add(registerForm.password, 'minLength:6', '密码的长度不能少于6位');
      validator.add(registerForm.phoneNumber, 'isMobile', '手机号码格式不正确');

      let errorMsg = validator.start();//获得校验结果
      return errorMsg;//返回校验结果
    };
    registerForm.onsubmit = function (e) {
      e.preventDefault();

      let errorMsg = validaFunc();
      if (errorMsg) {
        alert(errorMsg);
        return false;
      }
    }

最后是Validator类的实现

    let Validator = function () {
      this.cache = [];//保存校验规则
    };
    Validator.prototype.add = function (dom, rule, errorMsg) {
      let ary = rule.split(":"); //把strategy和参数分开
      this.cache.push(function () {//吧校验的步骤用空函数包装起来,并且放入Cache
        let strategy = ary.shift();//用户挑选的 strategy
        ary.unshift(dom.value);//吧input的value添加进来
        ary.push(errorMsg);//把errorMsg添加进参数列表
        return strategise[strategy].apply(dom, ary)
      })
    }

    Validator.prototype.start = function () {
      for (let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
        let msg = validatorFunc();//开始校验,并取得校验后的返回信息
        if (msg) {//如果有确切的返回值,说明校验没有通过
          return msg
        }

      }
    }

使用策略模式重构代码之后,我们仅仅通过”配置“的方式就可以完成一个表单的校验,这些规则也可以重复的在程序任何地方,还能作为插件的方式,方便被移植到其他项目中

在校验某个校验规则的时候,只需要编写或者少量的代码

给某个文本输入框添加多种校验规则

目前为止我们的表单验证实现留有一点小遗憾:一个文本输入框只能对应一种校验规则,比如,用户名输入框只能校验输入是否为空。如果我们既想校验它是否为空,又想校验它输入文本的长度不小于10呢?我们期望以这样的形式进行校验

validator.add(registerForm.userName, [{strategy: 'isNonEmpty', errorMsg:'用户名不能为空'},{strategy: 'minLength:10', errorMsg:'用户名长度不能小于10位'}]);

代码实现

<form action="" id="registerForm" method="post">
    请输入用户名:<input type="text" name="userName" />
    请输入密码:<input type="text" name="password" />
    请输入手机号:<input type="text" name="phoneNumber" />
    <button>提交</button>
</form>
  • 策略对象
let strategies = {
  //不为空
  isNonEmpty: (value, errorMsg) => {
    if (value === "") {
      return errorMsg;
    }
  },
  // 限制最小长度

  minLength: (value, length, errorMsg) => {
    if (value.length < length) {
      return errorMsg;
    }
  },
  // 手机号格式
  isMobile: (value, errorMsg) => {
    if (!/0?(13|14|15|17|18)[0-9]{9}/.test(value)) {
      return errorMsg;
    }
  },
};
  • Validator类
let Validator = function () {
  this.cache = [];
};
Validator.prototype.add = function (dom, rules) {
  let _this = this;
  for (let i = 0, rule; (rule = rules[i++]); ) {
    (function (rule) {
      let strategyAry = rule.strategy.split(":");
      let errorMsg = rule.errorMsg;
      _this.cache.push(function () {
        let strategy = strategyAry.shift();
        strategyAry.unshift(dom.value);
        strategyAry.push(errorMsg);
        return strategies[strategy].apply(dom, strategyAry);
      });
    })(rule);
  }
};

Validator.prototype.start = function () {
  for (let i = 0, validatorFunc; (validatorFunc = this.cache[i++]); ) {
    let errorMsg = validatorFunc();
    if (errorMsg) {
      return errorMsg;
    }
  }
};
  • 客户调用代码
    var registerForm = document.getElementById('registerForm');
    let validaFunc = function () {
      let validator = new Validator(); //创建一个validator对象
      // 添加一些校验规则
      validator.add(registerForm.userName, [{ strategy: 'isNonEmpty', errorMsg: '用户名不能为空' }, { strategy: 'minLength:10', errorMsg: '用户名长度不能小于10位' }]);
      validator.add(registerForm.password, 'minLength:6', '密码的长度不能少于6位');
      validator.add(registerForm.phoneNumber, 'isMobile', '手机号码格式不正确');

      let errorMsg = validator.start();//获得校验结果
      return errorMsg;//返回校验结果
    };
    registerForm.onsubmit = function (e) {
      e.preventDefault();

      let errorMsg = validaFunc();
      if (errorMsg) {
        alert(errorMsg);
        return false;
      }
    }

策略模式的优点

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