浅析-特定场景下if-else的优化方案(上)

117 阅读9分钟

前言

      相信很多人有这样的经历,在项目比较忙的时候,都是先考虑实现,用当时以为最好的方式先实现方案,在项目不忙的时候,再看下以前代码,想下有什么更好的实现方案,或者优化方案。例如,当我们遇到需要多个条件判断的业务操作时,最简单直接的办法就是用多个if-else判断,虽然能够实现,但当判断条件过多,甚至是多元判断时,我们的代码就会变得非常不好阅读和维护。下面就和大家分享一下自己常用的在特定场合下,代替if-else,switch的解决方案。如果大家有什么想法,欢迎提出来,多多交流。

举个例子

  • 需求: 写一个 returnWeekday() 方法返回"今天是星期几"。

       当我们开始拿到需求的时候,看到一系列的逻辑判断,首先想到的应该就是 if 语句了。( 这里也可以用switch-case )

     function returnWeekday() { 
        let string = '今天是星期'
        let date = new Date().getDay()
        if (date === 0) {
          string += '日'
        } else if (date === 1) {
          string += '一'
        } else if (date === 2) {
          string += '二'
        } else if (date === 3) {
          string += '三'
        } else if (date === 4) {
          string += '四'
        } else if (date === 5) {
          string += '五'
        } else if (date === 6) {
          string += '六'
        }
        return string
      }
      console.log(returnWeekday())

当我们写完了这样的代码,第一感觉就是 if else 块是不是太多了。

假设哪一天,需求变了,我们这个 returnWeekday() 方法就需要多加一层判断了。

我们的希望是已经封装好的方法,不要频繁的修改。可是需求的变动是你无法控制的。

所以我们继续思考该怎么优化。

  • 初步尝试

     我们看到这里的 case 是数字,和数组的下标是一致的。所以我们可以考虑使用数组来优化。

     function returnWeekday() {
        let str = '今天是星期'
        let date = new Date().getDay()
        // 使用数组
        let dateArr = [天, 一, 二, 三, 四, 五, 六]
        return str + dateArr[date]
      }
      console.log(returnWeekday())

     以上代码是不是比 switch 语句和 if 语句清晰多了。而且就算一周变为八天,只需要修改 dateArr 数组就好了。

  • 再次尝试

     需要返回的是一行文字,那我们是不是可以使用字符串的方法去实现(字符串有个和使用数组下标类似的方法)

// charAt 定位方法
      function returnWeekday() {
        return '今天是星期' + '日一二三四五六'.charAt(new Date().getDay())
      }
      console.log(returnWeekday())

以上是的需求刚好是每个条件都有规律的数字,如果我们每个条件都是不规律的字符串呢?

举个例子

      if (status === '1') {
        return '未审批'
      } else if (status === '2') {
        return '审批中'
      } else if (status === '3') {
        return '审批通过'
      } else if (status === '4') {
        return '审批退回'
      } else if (status === '5') {
        return '审批异常'
      }
  • 使用Object对象

   适用于单层多个判断

   这是一种比较常见的方式,用Object对象把判断收集到一起

     const statusArr = {
        '1': '未审批',
        '2': '审批中',
        '3': '审批通过',
        '4': '审批退回',
        '5': '审批异常',
      }

      function getStatus(Num) {
        return statusArr[Num]
      }

      getStatus('1') //未审批

这样是不是变得优雅一些了呢?可能看下来可能优化的效果没有非常显著,但是将逻辑通过数据的形式维护起来,可读性更好。

以上是单层多个判断,如果我们加一层呢?

  • 比如当我们加上一个角色判断时

    if (role === '打工人') {
        if (status === '1') {
          //一些操作
        } else if (status === '2') {
          //一些操作
        } else if (status === '3') {
          //一些操作
        } else if (status === '4') {
          //一些操作
        } else if (status === '5') {
          //一些操作
        }
      } else if (role === '老板') {
        if (status === '1') {
          //一些操作
        } else if (status === '2') {
          //一些操作
        } else if (status === '3') {
          //一些操作
        } else if (status === '4') {
          //一些操作
        } else if (status === '5') {
          //一些操作
        }
      }
    

可以看到,当同一个方法,不同角色不同状态执行的操作不同时,代码将会变得冗长。

优化的方法:同样使用一个对象收集这种不同的状态,但是由于多元的关系,将角色和状态进行字符串的拼接。

    let statusArr = {
            '打工人_1': ()=>{ /*一些操作*/ },
            '打工人_2': ()=>{ /*一些操作*/ },
            '打工人_3': ()=>{ /*一些操作*/ },
            '打工人_4': ()=>{ /*一些操作*/ },
            '打工人_5': ()=>{ /*一些操作*/ },
            '老板_1': ()=>{ /*一些操作*/ },
            '老板_2': ()=>{ /*一些操作*/ },
            '老板_3': ()=>{ /*一些操作*/ },
            '老板_4': ()=>{ /*一些操作*/ },
            '老板_5': ()=>{ /*一些操作*/ },

          }
      function getStatus(role,status) {
            return statusArr[`${role}_${status}`]
      }

可以看到,改造之后,更加简明易读。不过个人认为这种拼接的方式始终不够规范,易读很可能只针对自己,对于接手的可能语义化还不够,下面介绍一种更好的方式。

  • 使用对象的方式存放在Map对象上

上面这种方式之所以要拼接在一起,是因为对象的键需要是字符串的限制,如果使用Map对象,键就可以是一个对象、数组或者更多类型,方便了很多。(Map对象文档)

let statusMap = new Map([
  [{role: '打工人', status : '1'}, ()=>{ /*一些操作*/ }],
  [{role: '打工人', status : '2'}, ()=>{ /*一些操作*/ }],
  [{role: '打工人', status : '3'}, ()=>{ /*一些操作*/ }],
  [{role: '打工人', status : '4'}, ()=>{ /*一些操作*/ }],
  [{role: '打工人', status : '5'}, ()=>{ /*一些操作*/ }],
  [{role: '老板', status : '1'}, ()=>{ /*一些操作*/ }],
  [{role: '老板', status : '2'}, ()=>{ /*一些操作*/ }],
  [{role: '老板', status : '3'}, ()=>{ /*一些操作*/ }],
  [{role: '老板', status : '4'}, ()=>{ /*一些操作*/ }],
  [{role: '老板', status : '5'}, ()=>{ /*一些操作*/ }],
])
let getStatus = function(role,status) {
    statusMap.forEach((value,key)=>{
  if(JSON.stringify(key) == JSON.stringify({role: role,status: status})){
      value()
      }
    })
}
getStatus('打工人','1') // 一些操作

解释一下这段代码:

     将状态和角色组成对象放入Map对象中作为键名,将不同的方法作为对应的值。getStatus方法找到对应的操作并执行

     这里有一个小坑,由于键名是对象,所以查找Map对象时不能用Map.get()的方法,原因是由于对象的引用类型的问题,所以我用了这种方式转成字符串来比较。实测没有问题。

  • 假如打工人情况下,status1-4的处理逻辑都一样怎么办

    可以尝试以下:

let statusMap = new Map([
  [{role: '打工人', status : '1'}, ()=>{ /* 事件A */ }],
  [{role: '打工人', status : '2'}, ()=>{ /* 事件A */ }],
  [{role: '打工人', status : '3'}, ()=>{ /* 事件A */ }],
  [{role: '打工人', status : '4'}, ()=>{ /* 事件A */ }],
  [{role: '打工人', status : '5'}, ()=>{ /* 事件B */ }],
])

这样写已经能满足日常需求了,但认真一点讲,上面重写了4次事件A还是有点不爽,假如判断条件变得特别复杂,比如role有3种状态,status有10种状态,那你需要定义30条处理逻辑,而往往这些逻辑里面很多都是相同的,那可以这样实现:

let statusMap = new Map([
  [/^打工人_[1-4]$/, ()=>{ /* 事件A */ }],
  [/^打工人_5$/,()=>{ /* 事件B */ }],
])
 let getStatus = function (role, status) {
         statusMap2.forEach((value, key) => {
           if (key.test(`${role}_${status}`)) {
            return value();
           }
         //  key.test(`${role}_${status}`) && value();
        });
      };
  getStatus("打工人", "1");

       也就是说利用数组循环的特性,符合正则条件的逻辑都会被执行,那就可以同时执行公共逻辑和单独逻辑,因为正则的存在,你可以打开想象力解锁更多的玩法,本文就不赘述了。

  以上几种都是基于处理逻辑和配置数据分离的优化方法,这种写法也叫 表驱动法 或者 策略模式,这些方法,适用于一些逻辑不是太复杂的处理,对于一些复杂的处理逻辑,可以考虑使用责任链模式。

责任链模式

  • 什么是责任链模式

     责任链模式就是某个请求需要多个对象进行处理,从而避免请求的发送者和接收之间的耦合关系。将这些对象连成一条链子,并沿着这条链子传递该请求,直到有对象处理它为止。

责任链模式中的角色:发送者、接受者

责任链模式的流程:

  1. 发送者知道链中的第一个接受者,它向这个接受者发出请求

  2. 每一个接受者都对请求进行分析,要么处理它,要么往下传递

  3. 每一个接受者知道的其他对象只有一个,即它的下家对象

  4. 如果没有任何接受者处理请求,那么请求将从链上离开,不同的实现对此有不同的反应

  • 举个例子

       需求: 公司针对支付过定金的用户有一定的优惠政策。在正式购买后,已经支付过 500 元定金的用户会收到 100 元的商城优惠券,200 元定金的用户可以收到 50 元的优惠券,而之前没有支付定金的用户只能进入普通购买模式,也就是没有优惠券,且在库存有限的情况下不一定保证能买到。

  1. orderType:表示订单类型(定金用户或者普通购买用户),code 的值 为 1 的时候是 500 元定金用户,为 2 的时候是 200 元定金用户,为 3 的时候是普通购买用户。
  2. pay:表示用户是否已经支付定金,值为 true 或者 false, 虽然用户已经下过 500 元定金的订单,但如果他一直没有支付定金,现在只能降级进入普通购买模式。
  3. stock: 表示当前用于普通购买的手机库存数量,已经支付 过 500 元或者 200元定金的用户不受此限制。

先实现下这个需求

let order = function (orderType, pay, stock) {
        if (orderType === 1) {
          if (pay === true) {
            console.log("500元定金,得到100元优惠券");
          } else {
            //未支付定金,降级到普通购买模式
            if (stock > 0) {
              //用于普通购买的手机还有库存
              console.log("普通购买,没有优惠券");
            } else {
              console.log("手机库存不足");
            }
          }
        } else if (orderType === 2) {
          if (pay === true) {
            console.log("200元定金预约,得到50优惠券");
          } else {
            if (stock > 0) {
              console.log("普通购买,没有优惠券");
            } else {
              console.log("手机库存不足");
            }
          }
        } else if (orderType === 3) {
          if (stock > 0) {
            console.log("普通购买,没有优惠券");
          } else {
            console.log("手机库存不足");
          }
        }
      };
      order(1, true, 500); // 500元定金,得到100元优惠券

虽然我们得到了意料中的运行结果,但这远远算不上一段值得夸奖的代码。虽然目前项目能正常运行,如果中间增加一个新的优惠逻辑,那么你就要修改这个庞大的 order方法,维护工作变得复杂。

小结

      字数限制,请看下一篇   浅析-特定场景下if-else的优化方案(下)