前言

这是关于设计模式的系列文章,在每篇文章中将对常见设计模式进行讲解,因为针对前端方向,而且前端常用语言 JavaScript 本身是弱类型,面向对象(模拟面向对象)编程的实现相较于其他强类型语言实现更为繁琐,所以代码主要以 JavaScript 表现。

系列文章链接:

策略模式简介

“策略模式” 是将定义的一组算法封装起来,使其可以相互替换,封装的算法具有一定的独立性,让算法独立于客户端而变化,可以大大减少 if...elseswitch...case 等判断。


策略模式 UML 图
策略模式 UML 图


策略模式的实现

下面是一个关于会员打折的逻辑,根据顾客身份不同输出不同的支付金额,是未使用 “策略模式” 的实现。

/* 未使用策略模式 */
class Customer {
  constructor(type) {
    this.type = type;
  }
  pay(amount) {
    if (this.type === 'member') {
      return amount * 0.9;
    } else if (this.type === 'vip') {
      return amount * 0.8;
    } else {
      return amount;
    }
  }
}

const c1 = new Customer('normal');
const c2 = new Customer('member');
const c3 = new Customer('vip');

console.log(c1.pay(100)); // 100
console.log(c2.pay(100)); // 90
console.log(c3.pay(100)); // 80

上面的代码与 状态模式 一节中的问题类似,违反开放封闭原则和单一职责原则,代码冗余且判断条件过多,“状态模式” 虽然可以解决状态不同时不同复杂逻辑的抽离和解耦,但是并不能解决过多条件判断的问题,下面就是用 “策略模式” 来对这个点进行优化。

/* 使用策略模式优化 —— 策略类 */
class Customer {
  constructor(kind) {
    this.kind = kind;
  }
  pay(amount) {
    return this.kind.pay(amount);
  }
}

// 策略类
class Normal {
  pay(amount) {
    return amount;
  }
}

class Member {
  pay(amount) {
    return amount * 0.9;
  }
}

class VIP {
  pay(amount) {
    return amount * 0.8;
  }
}

const c1 = new Customer(new Normal());
const c2 = new Customer(new Member());
const c3 = new Customer(new VIP());

console.log(c1.pay(100)); // 100
console.log(c2.pay(100)); // 90
console.log(c3.pay(100)); // 80

上面是使用策略类对复杂判断逻辑的内容进行了抽象,并将原本 if...else 中的逻辑分别放在了不同的策略类中维护,如果每一个策略类中要维护的逻辑并不是很复杂,也可以使用第二种方案,即使用策略对象维护不同的逻辑。

/* 使用策略模式优化 —— 策略对象 */
class Customer {
  constructor() {
    // 策略对象
    this.kinds = {
      normal(amount) {
        return amount;
      },
      member(amount) {
        return amount * 0.9;
      },
      vip(amount) {
        return amount * 0.8;
      }
    };
  }
  pay(kind, amount) {
    return this.kinds[kind](amount);
  }
}

const c1 = new Customer();
const c2 = new Customer();
const c3 = new Customer();

console.log(c1.pay('normal', 100)); // 100
console.log(c2.pay('member', 100)); // 90
console.log(c3.pay('vip', 100)); // 80

策略模式的应用

jQuery 的 animate 动画

jQuery 的源码实现中,animate 方法就用到了 “策略模式”,通过不同的状态定义了动画不同的行为,使用代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>jQuery-animate</title>
  <style>
    #content{
      width: 100px;
      height: 100px;
      background-color: pink;
    }
  </style>
</head>
<body>
  <button id="bigger">变大</button>
  <div id="content"></div>
  <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
  <script>
    $('#bigger').on('click', function () {
      $('#content').animate({
        width: '200px',
        height: '200px'
      }, 1000, 'linear'); // linear 参数为动画策略的一种类型
    });
  </script>
</body>
</html>

表单校验

装饰器模式 一节中也有表单校验的应用,代码如下:

<!-- 表单校验应用装饰器模式 -->
<form>
  用户名:<input type="text" id="username">
  密码:<input type="text" id="password">
  <button id="submit-btn">提交</button>
<form>
<script>
  const submitBtn = document.getElementById('submit-btn');

  // 添加切面函数
  Function.prototype.before = function (beforeFn) {
    const _this = this;
    return function () {
      let result = beforeFn.apply(this, arguments);
      result && _this.apply(this, arguments);
    }
  }

  // 表单提交事件
  function submit() {
    console.log('提交表单');
  }

  // 验证用户名
  submit = submit.before(function () {
    const username = document.getElementById('username').value;
    if (!username) {
      return alert('请输入用户名');
    }
    return true;
  });

  // 验证
  submit = submit.before(function () {
    const password = document.getElementById('password').value;
    if (!password) {
      return alert('请输入密码');
    }
    return true;
  });

  submitBtn.addEventListener('click', submit);
</script>

“装饰器模式” 是将对每个表单校验逻辑,通过增加切面(AOP)的方式插入在了 submit 事件之前,如果有一个校验不通过则不会执行下一个切面的校验操作或提交表单,但是这样的表单校验有局限性,如果页面表单校验非常多需要对校验逻辑进行统一管理,并且大多数场景下是所有的表单都校验后对所有的表单进行错误提示,这是就需要 “策略模式” 的策略对象来管理所有的校验逻辑。

<!-- 表单校验应用策略模式 -->
<form id="userform">
  用户名:<input type="text" name="username">
  密码:<input type="text" name="password">
  手机号:<input type="text" name="mobile">
  <input type="submit" value="提交">
</form>
<script>
  const form = document.getElementById('userform');
  const validator = (function () {
    const rules = {
      noEmpty(val, msg) {
        if (val === '') return msg;
      },
      minLength(val, min, msg){
        if (val === '' || val.length < min) return msg;
      },
      isMobile(val, msg) {
        if (!/1\d{10}/.test(val)) return msg;
      }
    };

    // 存储
    const checks = [];

    // 增加校验的项目
    function add(element, rule) {
      checks.push(function () {
        // ['minLength', 6, '密码长度不能少于 6 位']
        const name = rule.shift();

        // [val, 6, '密码长度不能少于 6 位']
        rule.unshift(element.value);
        return rules[name] && rules[name].apply(element, rule);
      });
    }

    // 给策略对象增加新的功能
    function addRule(name, rule){
      rules[name] = rule;
    }

    // 开始校验
    function start() {
      for (let i = 0; i < checks.length; i++) {
        const msg = checks[i]();
        if (msg) return msg;
      }
    }

    return { add, addRule, start };
  })();

  // 添加自定义规则
  validator.addRule('maxLength', function (val, max, msg) {
    if (val === '' || val.length > max) return msg;
  });

  form.onsubmit = function () {
    validator.add(form.username, ['noEmpty', '用户名不能为空']);
    validator.add(form.password, ['minLength', 6, '密码长度不能少于 6 位']);
    validator.add(form.password, ['maxLength', 12, '密码长度不能大于 12 位']);
    validator.add(form.mobile, ['isMobile', '必须输入合法的手机号']);

    const msg = validator.start();
    alert(msg || '校验通过');
    return !msg;
  }
</script>

通过对比两段代码可以显而易见的看出 “策略模式” 在对于表单校验的功能上比 “装饰器模式” 更加健壮,可以在保证可维护性的基础上支持更多复杂的功能。

总结

“策略模式” 和 “状态模式” 都有上下文、策略和状态类,上下文把这些请求委托给这些类来执行,“策略模式” 中,各个类是平等的,没有关系,客户端需要知道算法主动切换,“状态模式” 中,状态的切换和行为被封装好了,客户端不需要了解细节,所以 “策略模式” 真正意义的解决了状态过多时条件判断过多的问题,最后附上 案例地址