表驱动法 -优化逻辑分支
定义
表驱动法(Table-Driven Approach)是一种编程模式,可以将输入变量作为直接或间接索引在表里查找所需的结果或处理函数,而不使用逻辑语句(if-else 和 switch-case)。索引表可以是一个数组、map、或者其它数据结构。
事实上,凡是能通过逻辑语句来选择的事物,都可以通过查表来选择。对简单的情况而言,使用逻辑语句更为容易和直白,但随着逻辑链的越来越复杂,查表法也就愈发显得更具有吸引力。
使用总则
适当的情况下,采用表驱动法,所生成的代码会比复杂的逻辑代码更简单,更容易修改,而且效率更高。
应用
查询方式
可以分为:直接访问、索引访问、阶梯访问三种
直接访问
举例:查询今天是星期几?
常规写法:
const day = new Date().getDay()
let day_zh;
if(day === 0){day_zh = '星期日'
}else if(day === 1) {day_zh = '星期一'
}
...
else{day_zh = '星期六'
}// 或者 用 switch case
switch(day) {case 0:day_zh = '星期日'break;case 1:day_zh = '星期一'break;...
}
优化写法:将数据存到查询表里
const week = ['星期日', '星期一',..., '星期六']
const day = new Date().getDay(
const day_zh = week[day]
举例2:保险费率的查询,费率会根据年龄、性别、婚姻状态等不同情况变化
常规写法:使用逻辑分支(if 或 switch) 会非常麻烦
if (gender === 'female') {if (hasMarried) {if (age < 18) {//} else {// }} else if (age < 18) {//} else {// }
} 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
}// 查询表
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,可以通过构建索引表、查询表来解决。
举例:
保险费率根据不同年龄费率不同,不满18岁费率0.2,18-65岁费率0.4,65岁及以上费率0.6。
假设人刚出生是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];
索引访问优点
-
节省空间,比直接索引的全表法节约空间
-
灵活, 根据不同的字段生成不同的索引表
-
可维护性强
阶梯访问
不如索引结构直接,但是要比索引访问方法节省空间。
阶梯结构的基本思想:表中的记录对于不同数据范围有效,而不是对不同的数据点有效。
举例:考试按照分数进行等级评定
优 >= 90
良 < 90
中 < 80
差 < 60
常规写法
let level = '优'
if(score <60 ){level = '差'
}else if(score < 80) {level = '中'
}else if(score < 90) {level = '良'
}
优化写法
const levelTable = ['差', '中', '良', '优']
const scoreCeilTable = [60, 80, 90, 100]function getLevel(score) {const len = scoreCeilTable.lengthfor(let i = 0; i < len; i++) {const scoreCeil = scoreCeilTable[i]if(score <= scoreCeil) {return levelTable[i]}}return levelTable[len - 1]
}
注意事项:
-
留心边界端点
注意边界:< 与 <=,确认循环能够在找出最高一级区间后恰当地终止。
-
二分查找替代顺序查找
上面例子用了顺序查找,当数据比较大时,查找成本还是比较大的,应该考虑用二分查
-
考虑用索引访问来取代阶梯技术
阶梯查找比较耗时,如果速度非常重要,可以用索引访问来取代阶梯,用空间换时间,但也要分情况,因为离散空间是不能够完全替代连续空间的
存储类型
查询表从存储上可以分为数据表、逻辑表。
-
数据表:仅存储结果数据
-
逻辑表:逻辑处理的代码或者引用
逻辑表
举例:
常规写法:
if (key = "Key A")
{执行 Key A 相关的行为。
}
else if (key = "Key B")
{执行 Key B 相关的行为。
}
优化写法
let table = {A: {data: "数据1",action () {console.log('action 1')}},B: {data: "数据2",action () {console.log('action 2')}}
}function handleTable(key) {return table[key]
}console.log(handleTable('A').data)
handleTable('A').action()
总结
表驱动的意义是将数据和逻辑剥离,在开发中,直接修改配置比修改逻辑要更加安全。数据的添加、删除比逻辑条件的添加、删除风险更低,数据来源也更加灵活。
在大多数情况下,优先使用直接访问和索引访问,除非两者实在无法处理,才考虑使用阶梯访问。
使用表的关键在于如何构建查询表内容,如何从表中查询,如何选择合适的查询表。
参考资料
一文道尽“表驱动法”
使用表驱动写出更优雅的条件判断
十分钟魔法练习:表驱动编程
使用表驱动法替代普通的判断分支语句(JS的深入探索)