表驱动法

什么是表驱动法

所谓表驱动法(Table-Driven Approach),简单讲是指用查表的方法获取值。 我们平时查字典以及念初中时查《数学用表》找立方根就是典型的表驱动法。在数值不多的时候我们可以用逻辑语句(if 或 switch case)的方法来获取值,但随着数值的增多逻辑语句就会越来越长,此时表驱动法的优势就显现出来了。

表驱动法的使用主要分为三种方式:直接访问表,索引访问表和阶梯访问表,下面分别举例说明一下这三种方法。

直接访问表

直接访问是最简单也是最常用的一种方法,查表本质其实就是去索引“键”来获得“值”,有点像获得数组值一样,给定下标index,然后 array[index] 就获得数组在相应下标处的数值。

举个我工作中的栗子:需要根据国家代码,确定国家的货币符号和国家的 icon 图标(这里国家代码和取值是确定且唯一的)。正常的时候我们可能会采用 if else,或者 switch case 的方式来处理这个问题,你可能会这样写这个函数(这里用 switch 来举例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function getCurrencyAndIcon (sid) {
switch (sid) {
case 0: {
return {
currency: '$',
icon: 'icon-us'
}
},
case 1: {
return {
currency: 'C$',
icon: 'icon-ca'
}
},
case 2: {
return {
currency: '円',
icon: 'icon-jp'
}
},
case 3: {
return {
currency: '£',
icon: 'icon-uk'
}
},
case 4: {
return {
currency: '€',
icon: 'icon-de'
}
}
}
}
// 注意:这里在case 中 return 退出函数了,所以不需要加 break

上面的写法其实也没有问题,也是常用的写法,但是这里不妨问一下,这里只是4个国家,但是假如是 200 个国家呢?你还是这样写? 那你的函数会有多长?是不是感觉不能忍?

现在我们采用表驱动法中的直接访问来实现这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const array = [ // 创建一个对象数组
{
currency: '$',
icon: 'icon-us'
},
{
currency: 'C$',
icon: 'icon-ca'
},
{
currency: '円',
icon: 'icon-jp'
},
{
currency: '£',
icon: 'icon-uk'
},
{
currency: '€',
icon: 'icon-de'
}
]
function getCurrencyAndIcon (sid) {
return array[sid] // 直接返回索引对应的值即可
}

以上对比代码量上看起来可能感觉差的不是太多?实际上是差别还是很大的,尤其是当数据量大的时候,尤其是你如果用 if else 来写更是明显,因为篇幅问题,这里只举栗了4个数据,而且所有的配置只要修改 array 数组即可,不会像用 switch 和 if else 语句实现的时候增删改时需要改原函数代码。

这里可能大家会遇到,索引是不同的随机数字或者字符串之类的,这也没关系,把数组改成对象就行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const dataObj = { // 创建一个对象
100: {
currency: '$',
icon: 'icon-us'
},
200: {
currency: 'C$',
icon: 'icon-ca'
},
400: {
currency: '円',
icon: 'icon-jp'
},
600:{
currency: '£',
icon: 'icon-uk'
},
900{
currency: '€',
icon: 'icon-de'
}
}
function getCurrencyAndIcon (sid) {
return dataObj[sid] // 直接返回索引对应的值即可
}

索引访问表

对于索引访问表,它适用于这样的情况,假设你经营一家商店,有100种商品,每种商品都有一个 id 号,但很多商品的描述都差不多,所以 只有30条不同的描述,现在的问题是建立商品与商品描述的表,如何建立?还是同上面的直接访问表的做法来一一对应吗?那样描述会扩充到100的,会有70个描述是重复的!如何解决这个问题呢?方法是建立一个100长的索引,然后这些索引指向相应的描述,注意不同的索引可以指向相同的描述,这样就解决了表数据冗余的问题啦。

这里举例采用12个商品,每个商品对应一个 id 号,然后12个商品只有4个不同的描述来举例子处理一下这个问题:

假如你拿到这个需求,不采用表驱动法的情况下,你可能会直接用 switch case 或者 if else 来处理这个需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 这里假设id 1001 1002 1003 商品的描述相同,1004 1005 1010的描述相同
// 剩下的 1006 1007 1008 1009 商品描述相同,1011 1012描述相同
function getProductDescription (id) {
if (id === 1001 || id === 1002 || id === 1003) {
return '商品描述1'
} else if (id === 1004 || id === 1005 || id === 1010) {
return '商品描述2'
} else if (id === 1006 || id === 1007 || id === 1008 || id ==1009) {
return '商品描述3'
} else if (id === 1011 || id === 1012) {
return '商品描述4'
}
}

采用直接访问表的情况下,可能是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 这里假设id 1001 1002 1003 商品的描述相同,1004 1005 1010的描述相同
// 剩下的 1006 1007 1008 1009 商品描述相同,1011 1012描述相同
const dataObj = {
1001: '商品描述1',
1002: '商品描述1',
1003: '商品描述1',
1004: '商品描述2',
1005: '商品描述2',
1006: '商品描述3',
1007: '商品描述3',
1008: '商品描述3',
1009: '商品描述3',
1010: '商品描述2',
1011: '商品描述4',
1012: '商品描述4'
}
function getProductDescription (id) {
return dataObj[id]
}

那如果采用索引访问表,会是怎么样的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 这里假设id 1001 1002 1003 商品的描述相同,1004 1005 1010的描述相同
// 剩下的 1006 1007 1008 1009 商品描述相同,1011 1012描述相同
const idObj = {
1001: 1,
1002: 1,
1003: 1,
1004: 2,
1005: 2,
1006: 3,
1007: 3,
1008: 3,
1009: 3,
1010: 2,
1011: 4,
1012: 4
}
const descriptionArray = [
'商品描述1',
'商品描述2',
'商品描述3',
'商品描述4'
]
function getProductDescription (id) {
return descriptionArray[idObj[id]]
}

阶梯访问表

第三种表驱动法是阶梯访问表,它适用于数据不是一个固定的值,而是一个范围的问题,比如将百分制成绩转成五级分制(我们用的优、良、中、合格、不合格,西方用的 A、B、C、D 和F),假定转换关系是当成绩在 90-100 区间,判为A,成绩在 80-89 区间,判为B,成绩在 70-79 区间,判为 C,成 绩在 60-69 区间,判为D,成绩在 60 以下,判为 F。现在的问题是,怎么用表格对付这个范围问题?一种笨笨的方法直接用 if else 判断,还有是申请一个 100 长度的表,然后在这个表中填充相应的等级就行了,但这样太浪费空间了,有没有更好的方法?

先看看 if else 的实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
getLevel (grade) {
if (grade <= 59) {
return 'F'
} else if (grade <= 69) {
return 'D'
} else if (grade <= 79) {
return 'C'
} else if (grade <= 89) {
return 'B'
} else if (grade <= 100) {
return 'A'
}
}

这里就不在再示例直接访问表和索引访问表的实现方法拉,虽然也可以实现,实现,但是直接访问表和索引访问表在这里的需求上都太浪费空间。

阶梯访问表的实现法:

1
2
3
4
5
6
7
8
9
const gradeArray = [59, 69, 79, 89, 100]
const levelArray = ['F', 'D', 'C', 'B', 'A']
function getLevel (grade) {
for (let i = 0; i < gradeArray.length; i++) {
if (grade <= gradeArray[i]) {
return levelArray[i]
}
}
}

这里童鞋们看到了结果,可能觉得代码量还是差不多,而且感觉阶梯访问表的方式逻辑都更负责了,还增加了遍历等增加时间复杂度的操作。

但是,这里的举例都是数据量小,假设第上面阶梯访问表的需求存在几百,几千个范围呢,你还是用 if else 的话会产生什么样的结果自然不必多说了吧?

以上就是表驱动法的总结,大家学会了么?快去改改项目中 if else 和 switch case 的代码先试试水吧,记住,数据量越大,表驱动法优势越大噢!