JS常见设计模式 之 策略模式

159 阅读7分钟

在程序设计中,我们经常会遇到一种情况:我们要实现一个功能通常会有很多种方案,比如我们定义一个number类型变量num,我们需要把num变量加一,我们可以选择num++也可以选择num += 1亦或者是num = num + 1无论哪种方案或者算法,我们最终都能实现我们想要的结果。这些算法并不固定并且它们之间是可以随意替换,这种解决方案就是我接下来要介绍的策略模式

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

年终奖

有一天老陈和老彭唠嗑。

“老彭啊,你们公司今年的年终奖发了吗?”

“还没发,不过我也不抱多大的希望咯,我今年评价不太好就算发也不会发多少”

“这样啊,我们公司上周发了,我今年全年评价S级给我发了4个月年终咧!走今天我请客,咱们下馆子去!”

(ps:以上纯属故事,实际本人的公司根本就没有年终奖...)

相信大家都遇到过公司的年终是根据每个人当年的绩效评级去进行发放的情况,举个例子S级的员工发四个月工资,A级则为3倍,B级则为2,C则直接没有。假设财务部分需要我们编写一段程序,用来方便他们计算每个员工的年终奖。

简单的实现

这么简单的代码,我小陈岂不是信手拈来,闲杂人等速速退散,是时候展现真正的技术了,咔咔咔,小陈一顿操作3分钟就完事了。代码如下:

    const calBonus = function (level, salary) {
      if (level === 'S') {
        return salary * 4
      }

      if (level === 'A') {
        return salary * 3
      }

      if (level === 'B') {
        return salary * 2
      }

      if (level === 'C') {
        return salary * 1
      }
    }

    calBonus('S', 4000); // 输出: 16000
    calBonus('C', 8000); // 输出: 8000

怎么样小陈厉害吧很快就把代码写出来了,然而就在小陈还在沾沾自喜的时候,殊不知他的这段代码有着一些显而易见的缺点。

  • calBonus函数比较庞大,包含了许多if语句的判断,并且这些语句覆盖了所有的逻辑分支。

  • calBonus函数的可维护性比较差,公司新加了一个评分等级D,或者公司临时决定要把所有的级别的绩效都多发一个月。那我们就必须深入的去改造这段代码,这是违反开发-封闭原则的。

  • 算法的复用性很差,如果程序的其他地方需要重用这些计算奖金的方法怎么办,我们只能去进行赋值粘贴。

使用策略模式重构代码

基于JavaScript的语言特性,我们可以使用一个对象来管理各个级别对应的奖金数额去进行集中管理,一下是改造后的代码:

    const bonusLevelMgr = {
      'S': function (salary) {
        return salary * 4
      },
      'A': function (salary) {
        return salary * 3
      },
      'B': function (salary) {
        return salary * 2
      },
      'C': function (salary) {
        return salary * 1
      }
    }

    const calBonus = function(level, salary) {
      return bonusLevelMgr[level](salary)
    }

    console.log(calBonus('S', 4000)); // 输出: 16000
    console.log(calBonus('C', 8000)); // 输出: 8000

其实我们很多时候都能写出这样的代码,设计模式无处不在,只不过我们内心并没有一个明确的概念罢了!

表单校验

现在我们前端日常开发用的都是vue和react,那么相信大家在使用这两个框架的时候或多或少都会接触到表单校验这么一个东西,对于本人长期写后管系统的开发者而言,表单校验基本是每个工作日都会接触到的日常工作内容。那么我们在使用的时候有没有想过,表单校验是怎么去进行设计的呢,如果让我们去写表单校验,那么我们又该如何去写呢?

首先我先写一个简易版本的表单校验规则案例:

  <form action="https://www.xxx.com" id="loginForm" method="post">
    用户名:<input type="text" name="loginName"/>
    密码:<input type="password" name="password"/>
    <button>提交</button>
  </form>
  <script>
    const loginForm = document.getElementById('loginForm')
    loginForm.onsubmit = function() {
      if (loginForm.loginName.value === '') {
        alert('请输入用户名')
        return false
      }
      if (loginForm.password.value === '') {
        alert('请输入密码')
        return false
      }
    }
  </script>

我们的小陈依旧是这么的棒,三下五除二就把代码给整出来了!

但是这个代码依旧有着和上面年终奖例子类似的问题:

  • loginForm.onsubmit函数较为庞大特别是当需要校验的表单项目比较多的时候,比如我之前做的一个进件的系统,用户需要填写几十项表单数据,如果这几十项的表单数据全部都用if-else去进行判断,那么它将会是非常庞大且臃肿的函数
  • 缺乏弹性,如果我们想要添加新的校验规则或者修改当前已有的规则,比如我们添加一个用户名的限制必须为纯数字,我们需要深入loginForm.onsubmit函数去进行修改,违反了开放-封闭原则
  • 可复用性差,如果其他页面也需要一个表单验证,我们又得要复制这些代码去进行新的代码编写,整个项目将会非常的乱

使用策略模式重构表单

首先我们将每个需要检验逻辑作为key封装到一个策略对象中:

    const strategies = {
      isNonEmpty: function (value, errorMsg) {
        if (value === '') {
          return errorMsg
        }
      },
      isNumber: function(value, errorMsg) {
        if (!/^\d+$/.test(value)) {
          return errorMsg
        }
      }
    }

既然我们已经定义了检验逻辑,接下来我们看一下改如何使用这些检验类:

    const validataFunc = function() {
      const validator = new Validator()
      validator.add(loginForm.loginName, 'isNonEmpty', '请输入用户名')
      validator.add(loginForm.loginName, 'isNumber', '用户名需为纯数字')
      validator.add(loginForm.password, 'isNonEmpty', '请输入密码')

      const errorMsg = validator.start()
      return errorMsg
    }

    const loginForm = document.getElementById('loginForm')
    loginForm.onsubmit = function() {
      const errorMsg = validataFunc()
      if (errorMsg) {
        alert(errorMsg)
        return false
      }
    }

我们先创建一个用于检验的实例validator,然后我们再往这个类里面添加了每个表单字段对应的校验规则和对应的错误文案,最后再调用校验累的实例validator的start方法将校验不通过的错误信息返回。

最后再在表单的onsubmit方法中去执行校验方法并获取错误信息。

从上面代码中我们可以看到,我们对校验类的实例传入了三个参数,分别是表单字段、检验规则、错误文案,并且该实例有一个start方法供我们开始校验,根据这个思路我们可以来实现Validator类:

    function Validator() {
      this.cache = [] // 保存校验规则
    }

    Validator.prototype.add =  function(dom, rule, errorMsg) {
      this.cache.push(function () {
        return strategies[rule].apply(dom, [dom.value, errorMsg])
      })
    }

    Validator.prototype.start = function () {
      for(let i = 0, validatorFunc;validatorFunc = this.cache[i++];) {
        let msg = validatorFunc()
        if (msg) {
          return msg
        }
      }
    }

我们先定义一个数组用于存储所有的校验规则,然后当add方法被调用的时候,我们就把对应的校验方法存入数组中,最后start函数被调用的时候,我们就依次遍历存入的校验规则,然后将校验结果返回。

同类型的适用场景还有输入框的格式化,之前在写VUE技术栈的项目的时候,不同的输入框的所需要的格式化的规则是不一样的,我们选择的做法是用自定义指令配合传入参数,用一个策略对象管理所有的格式化的方法,然后根绝传入的参数不同去选择不用的格式化方法,省去了很多if-else的逻辑判断。

策略模式的优缺点

从上面的例子我们不难发现策略模式是非常高效且十分常用的设计模式,可以总结出来一个优点:

  • 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
  • 策略模式提供了对于开放-封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,易于理解,易于扩展。
  • 策略模式中的算法也可以复用在系统的其他地方,从而避免了许多重复的复制粘贴工作。

同样的策略模式也有一些缺点:

我们在使用策略模式的时候会维护很多的策略对象和策略类,但是这些维护成本和一个个去if-else判断的代码比起来可以说是微不足道的。

其次,要使用好策略模式,必须了解所有的strategy的不同点,最后选择出一个最合适的strategy。比如当我们选择去西藏旅游,我们可以自驾,可以高铁,可以飞机更可以骑行,我们需要根据自己的诉求和了解所有方案的不同点去选择一个最合适自己的路线方案。

《JavaScript设计模式与开发实践》策略模式章节的复习