JavaScript中的策略模式

133 阅读6分钟

1.定义

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

2.使用策略模式算奖金

业务场景:根据绩效等级算奖金

普通写法

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

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

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

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

在这个例子里,算法的使用方式是不变的,都是根据某个算法取得计算后的奖金数额。而算法的实现是各异和变化的

使用策略模式写法

// 策略类
var strategies = {
  'S': function (salary) {
    return salary * 4
  },
  'A': function (salary) {
    return salary * 3
  },
  'B': function (salary) {
    return salary * 2
  }
}

// 环境类 context
var calculateBounds = function (level, salary) {
  return strategies[level](salary)
}
console.log(calculateBounds('S', 1000))
console.log(calculateBounds('A', 500))

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

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

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

通过使用策略模式重构代码,我们消除了原程序中大片的条件分支语句。所有跟计算奖金有关的逻辑不再放在Context 中,而是分布在各个策略对象中。Context 并没有计算奖金的能力,而是把这个职责委托给了某个策略对象。每个策略对象负责的算法已被各自封装在对象内部。当我们对这些策略对象发出“计算奖金”的请求时,它们会返回各自不同的计算结果,这正是对象多态性的体现,也是“它们可以相互替换”的目的。替换 Context 中当前保存的策略对象,便能执行不同的算法来得到我们想要的结果。

4.表单校验的策略设计模式

业务背景:假设我们正在编写一个注册的页面,在点击注册按钮之前,有如下几条校验逻辑。

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

普通写法

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.phoneNumber.value)) {
    alert("手机号码格式不正确");
    return false;
  }
};

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

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

使用策略模式写法

// 策略对象
var strategies = {
  isNonEmpty: 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|7][0-9]{9}$)/.test(value)) {
      return errorMsg;
    }
  },
};

// 环境类Context,Context 接受客户的请求,随后把请求委托给某一个策略类
var Validator = function () {
  this.cache = []; // 保存校验规则
};
Validator.prototype.add = function (dom, rules) {
  var self = this;
  for (var i = 0; i < rules.length; 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);
      });
    })(rules[i]);
  }
};
Validator.prototype.start = function () {
  for (var i = 0; i < this.cache.length; i++) {
    var ruleFunc = this.cache[i];
    var msg = ruleFunc();
    if (msg) {
      return msg;
    }
  }
};

// 用户调用时
var validataFunc = function () {
  var validator = new Validator();
  validator.add(registerForm.userName, [
    {
      strategy: "isNonEmpty",
      errorMsg: "用户名不能为空",
    },
    {
      strategy: "minLength:6",
      errorMsg: "用户名长度不能少于6位",
    },
  ]);
  validator.add(registerForm.password, [
    {
      strategy: "minLength:6",
      errorMsg: "密码长度不能少于6位",
    },
  ]);
  validator.add(registerForm.userName, [
    {
      strategy: "isMobile",
      errorMsg: "手机号格式不正确",
    },
  ]);
  var errorMsg = validator.start();
  return errorMsg;
};
registerForm.onsubmit = function () {
  var errorMsg = validataFunc();

  if (errorMsg) {
    alert(errorMsg);
    return false;
  }
};

validator.add 方法接受3 个参数,以下面这句代码说明: validator.add( registerForm.password, 'minLength:6', '密码长度不能少于6 位' );

  • registerForm.password 为参与校验的input 输入框。

  • 'minLength:6'是一个以冒号隔开的字符串。冒号前面的minLength代表客户挑选的strategy对象,冒号后面的数字6 表示在校验过程中所必需的一些参数。'minLength:6'的意思就是校验registerForm.password 这个文本输入框的value 最小长度为6。如果这个字符串中不包含冒号,说明校验过程中不需要额外的参数信息,比如'isNonEmpty'。

  • 第3 个参数是当校验未通过时返回的错误信息。

当我们往 validator 对象里添加完一系列的校验规则之后,会调用 validator.start()方法来启动校验。如果validator.start()返回了一个确切的errorMsg 字符串当作返回值,说明该次校验没有通过,此时需让registerForm.onsubmit 方法返回false 来阻止表单的提交

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

策略模式的优缺点

策略模式优点:

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

策略模式缺点:

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

总结

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

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