如何写出更优雅的条件判断

在咱们平时的开发中,if else是最经常使用的条件判断语句。在一些简单的场景下,if else用起来很爽,可是在稍微复杂一点儿的逻辑中,大量的if else就会让别人看的一脸蒙逼。
前端

若是别人要修改或者新增一个条件,那就要在这个上面继续增长条件。这样恶性循环下去,本来只有几个if else最后就有可能变成十几个,甚至几十个。
别说不可能,我就见过有人在React组件里面用了大量的if else,可读性和可维护性很是差。(固然,这个不算if else的锅,主要是组件设计的问题)java

这篇文章主要参与自《代码大全2》,原书中使用vb和java实现,这里我是基于TypeScript的实现,对书中内容加入了一些本身的理解。编程

从一个例子提及

日历

假如咱们要作一个日历组件,那咱们确定要知道一年12个月中每月都多少天,这个咱们要怎么判断呢?
最笨的方法固然是用if else啊。数组

if (month === 1) {
    return 31;
}
if (month === 2) {
    return 28;
}
...
if (month === 12) {
    return 31;
}
复制代码

这样一会儿就要写12次if,白白浪费了那么多时间,效率也很低。
这个时候就会有人想到用switch/case来作这个了,可是switch/case也不会比if简化不少,依然要写12个case啊!!!甚至若是还要考虑闰年呢?岂不是更麻烦?
咱们不妨转换一下思惟,每月份对应一个数字,月份都是按顺序的,咱们是否能够用一个数组来储存天数?到时候用下标来访问?安全

const month: number = new Date().getMonth(),
    year: number = new Date().getFullYear(),
    isLeapYear: boolean = year % 4 == 0 && year % 100 != 0 || year % 400 == 0;

const monthDays: number[] = [31, isLeapYear ? 29 : 28, 31, ... , 31];
const days: number = monthDays[month];
复制代码

概念

看完上面的例子,相信你对表驱动法有了必定地认识。这里引用一下《代码大全》中的总结。bash

表驱动法就是一种编程模式,从表里面查找信息而不使用逻辑语句。事实上,凡是能经过逻辑语句来选择的事物,均可以经过查表来选择。对简单的状况而言,使用逻辑语句更为容易和直白。但随着逻辑链的愈来愈复杂,查表法也就愈发显得更具吸引力。优化

使用表驱动法前须要思考两个问题,一个是如何从表中查询,毕竟不是全部场景都像上面那么简单的,若是if判断的是不一样的范围,这该怎么查?
另外一个则是你须要在表里面查询什么,是数据?仍是动做?亦或是索引?
基于这两个问题,这里将查询分为如下三种:ui

  1. 直接访问
  2. 索引访问
  3. 阶梯访问

直接访问表

咱们上面介绍的那个日历就是一个很好的直接访问表的例子,可是不少状况并无这么简单。spa

统计保险费率

假设你在写一个保险费率的程序,这个费率会根据年龄、性别、婚姻状态等不一样状况变化,若是你用逻辑控制结构(if、switch)来表示不一样费率,那么会很是麻烦。设计

if (gender === 'female') {
    if (hasMarried) {
        if (age < 18) {
            //
        } else {
            // 
        }
    } else if (age < 18) {
        //
    } else {
        // 
    }
} else {
    ...
}
复制代码

可是从上面的日历例子来看,这个年龄倒是个范围,不是个固定的值,无法用数组或者对象来作映射,那么该怎么办呢?这里涉及到了上面说的问题,如何从表中查询?
这个问题能够用阶梯访问表和直接访问表两种方法来解决,阶梯访问这个后续会介绍,这里只说直接访问表。
有两种解决方法:
一、复制信息从而可以直接使用键值
咱们能够给1-17年龄范围的每一个年龄都复制一份信息,而后直接用age来访问,同理对其余年龄段的也都同样。这种方法在于操做很简单,表的结构也很简单。但有个缺点就是会浪费空间,毕竟生成了不少冗余信息。
二、转换键值
咱们不妨再换种思路,若是咱们把年龄范围转换成键呢?这样就能够直接来访问了,惟一须要考虑的问题就是年龄如何转换为键值。
咱们固然能够继续用if else完成这种转换。前面已经说过,简单的if else是没什么问题的,表驱动只是为了优化复杂的逻辑判断,使其变得更灵活、易扩展。

enum ages {
    unAdult = 0
    adult = 1
}
enum genders {
    female = 0,
    male = 1
}
enum marry = {
    unmarried = 0,
    married = 1
}
const age2key = (age: number): string => {
    if (age < 18) {
        return ages.unAdult
    }
    return ages.adult
}
type premiumRateType = {
    [ages: string]: {
        [genders: string]: {
            [marry: string]: {
                rate: number
            }
        }
    }
}
const premiumRate: premiumRateType = {
    [ages.unAdult]: {
        [genders.female]: {
            [marry.unmarried]: {
                rate: 0.1
            },
            [marry.married]: {
                rate: 0.2
            }
        },
        [genders.male]: {
            [marry.unmarried]: {
                rate: 0.3
            },
            [marry.married]: {
                rate: 0.4
            }
        }
    },
    [ages.adult]: {
        [genders.female]: {
            [marry.unmarried]: {
                rate: 0.5
            },
            [marry.married]: {
                rate: 0.6
            }
        },
        [genders.male]: {
            [marry.unmarried]: {
                rate: 0.7
            },
            [marry.married]: {
                rate: 0.8
            }
        }
    }
}
const getRate = (age: number, hasMarried: 0 | 1, gender: 0 | 1) => {
     const ageKey: string = age2key(age);
     return premiumRate[ageKey]
        && premiumRate[ageKey][gender]
        && premiumRate[ageKey][gender][hasMarried]
}
复制代码

这样,一旦判断条件出现了变化,这里只须要修改premiumRate里面的数据就行了。
可是以为这个例子举得仍是不够好,后续又想了一些方法来优化,将代码修改成以下会更容易理解一些。

enum ages {
    unAdult = 0,
    adult = 1
}
enum genders {
    female = 0,
    male = 1
}
enum marry = {
    unmarried = 0,
    married = 1
}

const age2key = (age: number): string => {
    if (age < 18) {
        return ages.unAdult
    }
    return ages.adult
}
const rates: number[] = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]
const premiumRate = {
    0: [age.unAdult, genders.female, marry.unmarried],
    1: [age.unAdult, genders.female, marry.married],
    2: [age.unAdult, genders.male, marry.unmarried],
    3: [age.unAdult, genders.male, marry.unmarried],
    4: [age.adult, genders.female, marry.unmarried],
    5: [age.adult, genders.female, marry.married],
    6: [age.adult, genders.male, marry.unmarried],
    7: [age.adult, genders.male, marry.unmarried]
}
type BoolCode = 0 | 1
const getRate = (age: number, hasMarried: BoolCode, gender: BoolCode) => {
    const ageKey: BoolCode = age2key(age)
    let index: string = ''
    Object.keys(premiumRate).forEach((key, i) => {
        const condition: BoolCode[] = premiumRate[key]
        if (condition[0] === ageKey 
            && condition[1] === gender
            && condition[2] === hasMarried
        ) {
            index = key;
        }
    })
    return rates[index];
}
复制代码

这样修改后,结构更加清晰,也更容易维护。

索引访问表

咱们前面那个保险费率问题,在处理年龄范围的时候很头疼,这种范围每每不像上面那么容易获得key。
咱们当时提到了复制信息从而可以直接使用键值,可是这种方法浪费了不少空间,由于每一个年龄都会保存着一份数据,可是若是咱们只是保存索引,经过这个索引来查询数据呢?
假设人刚出生是0岁,最多能活到100岁,那么咱们须要建立一个长度为101的数组,数组的下标对应着人的年龄,这样在0-17的每一个年龄咱们都储存'<18',在18-65储存'18-65', 在65以上储存'>65'。
这样咱们经过年龄就能够拿到对应的索引,再经过索引来查询对应的数据。
看起来这种方法要比上面的直接访问表更复杂,可是在一些很难经过转换键值、数据占用空间很大的场景下能够试试经过索引来访问。

const ages: string[] = ['<18', '<18', '<18', '<18', ... , '18-65', '18-65', '18-65', '18-65', ... , '>65', '>65', '>65', '>65']
const ageKey: string = ages[age];
复制代码

阶梯访问表

一样是为了解决上面那个年龄范围的问题,阶梯访问没有索引访问直接,可是会更节省空间。
为了使用阶梯方法,你须要把每一个区间的上限写入一张表中,而后经过循环来检查年龄所在的区间,因此在使用阶梯访问的时候必定要注意检查区间的端点。

const ageRanges: number[] = [17, 65, 100],
  keys: string[] = ['<18', '18-65', '>65'],
  len: number = keys.length;
const getKey = (age: number): string => {
  for (let i = 0; i < len; i++) {
    console.log('i', i)
    console.log('ageRanges', ageRanges[i])
    if (age <= ageRanges[i]) {
      return keys[i]
    }
  }
  return keys[len-1];
}
复制代码

阶梯访问适合在索引访问没法适用的场景,好比若是是浮点数,就没法用索引访问建立一个数组来拿到索引。
在数据量比较大的状况下,考虑用二分查找来代替顺序查找,。
在大多数状况下,优先使用直接访问和索引访问,除非二者实在没法处理,才考虑使用阶梯访问。

从这三种访问表来看,主要是为了解决如何从表中查询,在不一样的场景应该使用合适的访问表。

表驱动的意义是将数据和逻辑剥离,在开发中,直接修改配置比修改逻辑要更加安全。数据的添加、删除比逻辑条件的添加、删除风险更低,数据来源也更加灵活。

引用知乎大V Ivony的一段话:

分析和阅读一段代码的时候,不少时候是有侧重面的,有时候侧重于数据,有时候侧重于逻辑。假设咱们有这样一个需求,当某某值小于100时,就如何如何。那这个里面的100就是数据,当需求变动为某某值小于200时,才如何如何,那么咱们关注的点在于这个数据的修改。而不是整个逻辑的修改,数据的剥离,有助于咱们更快的发现修改点和修改代码。

参考资料:

  1. 代码大全(第2版)
  2. 这个例子中的if else也要重构掉吗?

PS:欢迎你们关注个人公众号【前端小馆】,你们一块儿来讨论技术。

相关文章
相关标签/搜索