策略模式就是将一系列算法封装起来,并使它们相互之间可以替换。被封装起来的算法具有独立性,外部不可改变其特性。

生活中有很多场景其实都是可以利用策略模式来体现的,比如你要出门旅游,资金又有限,目的地已经确定了,路线和交通方式有很多,你可以选一个最符合资金情况的方案。

场景: 表单验证

小A接到一个新的需求,开发一个用户注册页面,其中表单包含了用户名、密码、确认密码以及手机号码,要求所有数据都不为空,密码至少6位,确认密码必须与密码相等。

先编写好表单:

<form action="" id="form">
  姓名:<input type="text" id="username"><br>
  密码:<input type="password" id="password1"><br>
  确认密码:<input type="password" id="password2"><br>
  手机号:<input type="text" id="phone"><br>
  <input type="submit" value="提交">
</form>

然后直接做表单验证:

function getValue (id) {
  return document.getElementById(id).value;
}
var formData = document.getElementById('form')
formData.onsubmit = function () {
  var name = getValue('username');
  var pwd1 = getValue('password1');
  var pwd2 = getValue('password2');
  var tel = getValue('phone');
  if (name.replace(/(^\s*)|(\s*$)/g, "") === "") {
    alert('用户名不能为空')
    return false
  }
  if (pwd1.replace(/(^\s*)|(\s*$)/g, "")  === "") {
    alert('密码不能为空')
    return false
  }
  if (pwd2.replace(/(^\s*)|(\s*$)/g, "")  === "") {
    alert('确认密码不能为空')
    return false
  }
  if (pwd2 !== pwd1) {
    alert('确认密码与原密码不相同!')
    return false
  }
  if (tel.replace(/(^\s*)|(\s*$)/g, "") === "") {
    alert('手机号码不能为空')
    return false
  }
  if (!/^1[3,4,5,7,8,9][0-9]\d{8}$/.test(tel)) {
    alert('手机号码格式不正确')
    return false
  }
  alert('注册成功')
}

大功告成。可是仅仅4个表单数据,就用了6个if去判断,如果这个页面不是用户注册,而是某个管理页面中的表单,包含了十多个表单数据呢,一直if写到底?

那么策略模式就来解决表单验证中规则复用、一条数据多个校验规则的问题。

我们先把上面所有的if判断改写一下,封装成一个个方法,为了能够让所有表单均可使用,我们把它封装到一个原型对象中:

function Validate () {}
Validate.prototype.rules = {
  // 是否手机号
  isMobile: function (str) {
    var rule = /^1[3,4,5,7,8,9][0-9]\d{8}$/;
    return rule.test(str);
  },
  // 是否必填
  isRequired: function (str) {
    // 除去首尾空格
    var value = str.replace(/(^\s*)|(\s*$)/g, "");
    return value !== "";
  },
  // 最小长度
  minLength: function (str, length) {
    var strLength = str.length;
    return strLength >= length;
  },
  // 是否相等
  isEqual: function () {
    // 可以接收多个参数比较
    var args = Array.prototype.slice.call(arguments);
    // 取首项与后面所有的项比较,如果每个都相等,就返回true
    var equal = args.every(function(value) {
      return value === args[0];
    })
    return equal;
  }
} 

接下来封装开始验证的方法,这个方法会接收一个参数,是用户传递进来的校验规则对象,对象的键名是字段名,键值是数组类型,可包含多个校验规则,而每一个规则拥有三个属性,分别为规则名、需要校验的值以及校验不通过的提示语。

为了方便封装校验方法时作对比,我们先把表单提交的校验信息编写好:

formData.onsubmit = function () {
  event.preventDefault()
  var validator = new Validate();
  var result = validator.test({
    'username': [{rule: 'isRequired', value: this.username.value, message: '用户名不能为空!'}],
    'password1': [
      {rule: 'isRequired', value: this.password1.value, message: '密码不能为空!'},
      {rule: 'minLength', value: [this.password1.value, 6], message: '密码长度不能小于6个字符!'}
    ],
    'password2': [
      {rule: 'isRequired', value: this.password2.value, message: '确认密码不能为空!'},
      {rule: 'minLength', value: [this.password2.value, 6], message: '确认密码长度不能小于6个字符!'},
      {rule: 'isEqual', value: [this.password2.value, this.password1.value], message: '确认密码与原密码不相同!'}
    ],
    'isMobile': [
      {rule: 'isRequired', value: this.phone.value, message: '手机号不能为空!'},
      {rule: 'isMobile', value: this.phone.value, message: '手机号格式不正确!'}
    ]
  })
  if (result) {
    console.log(result)
  } else {
    console.log('校验通过')
  }
}

ok,开始封装校验方法:

Validate.prototype.test = function (rules) {
  var v = this;
  var valid; // 保存校验结果
  for (var key in rules) { // 遍历校验规则对象
    for (var i = 0; i < rules[key].length; i++) { // 遍历每一个字段的校验规则
      var ruleName = rules[key][i].rule; // 获取每一个校验规则的规则名
      var value = rules[key][i].value; // 获取每一个校验规则的校验值
      if (!Array.isArray(value)) { // 统一校验值为数组类型
        value = new Array(value)
      }
        // 调用校验规则方法进行校验
      var result = v.rules[ruleName].apply(this, value); 
      if (!result) { 
      // 如果校验不通过,就获取校验结果信息,并立即跳出循环不再执行,节约消耗
        valid = {
          errValue: key,
          errMsg: rules[key][i].message
        }
        break;
      }
    }
    if (valid) {
     // 如果有了校验结果,代表存在不通过的字段,则立即停止循环,节约消耗
      break;
    }
  }
  return valid; // 把校验结果反悔出去
}
我们来测试一下:
// 输入: ‘菜鸟库’、不输入、不输入、不输入
// 打印结果:{errValue: "password1", errMsg: "密码不能为空!"}

// 输入: ‘菜鸟库’、12345、456123、不输入
// 打印结果:{errValue: "password1", errMsg: "密码长度不能小于6个字符!"}

// 输入:‘菜鸟库’、123456、456123、不输入
// 打印结果:{errValue: "password2", errMsg: "确认密码与原密码不相同!"}

// 输入:‘菜鸟库’、123456、123456、12345678911
// 打印结果: {errValue: "isMobile", errMsg: "手机号格式不正确!"}

// 输入: ‘菜鸟库’、123456、123456、[手机号已移除]
// 打印结果: 校验通过

完美,表单验证的方法就封装完成了。

看完后同学们有没有有一个发现,尽管采用策略模式能够封装很多算法,但是对于用户,也就是没有参与封装的其他开发者来说,他们并不知道有哪些方法可以使用,如果不去阅读这些算法,很容易走回以前的老套路或者重复封装。这也是策略模式一个比较大的缺点。

其次,如果算法非常复杂,且存在某几个算法拥有相同的某个逻辑的时候,这些算法不能共享这个逻辑,因为每个算法之间必须相互独立。

当然,它的优点是有目共睹的,将一个个算法封装起来,提高代码复用率,减少代码冗余;策略模式可看作为if/else判断的另一种表现形式,在达到相同目的的同时,极大的减少了代码量以及代码维护成本。

补充

补充:策略模式(Strategy Pattern)把一系列可互换的算法封装成独立的策略对象,让算法独立于使用它的客户端变化,是消除 if/else 大段分支的经典方案。

要点回顾:

  • 三个角色:
    • 上下文(Context):持有策略引用,提供调用入口。
    • 抽象策略(Strategy):定义算法接口。
    • 具体策略(ConcreteStrategy):实现具体算法。
  • 常见应用:
    • 表单验证(本文示例)。
    • 支付方式(支付宝、微信、银联)。
    • 排序 / 压缩 / 加密算法按需切换。
    • 登录鉴权(手机号、邮箱、第三方 OAuth)。
  • 优点:消除分支、易扩展、算法可独立测试。
  • 缺点:策略类数量增多、对调用者暴露实现细节、策略之间无法共享某些公共逻辑。

补充:在 TypeScript 中可以用「联合类型 + 函数表」实现更轻量的策略模式,避免大量策略类。