资讯专栏INFORMATION COLUMN

表驱动法

LeoHsiun / 2686人阅读

摘要:表驱动法就是一种编程模式,从表里面查找信息而不使用逻辑语句。但随着逻辑链的越来越复杂,查表法也就愈发显得更具吸引力。前面已经说过,简单的是没什么问题的,表驱动只是为了优化复杂的逻辑判断,使其变得更灵活易扩展。

在我们平时的开发中,if else是最常用的条件判断语句。在一些简单的场景下,if else用起来很爽,但是在稍微复杂一点儿的逻辑中,大量的if else就会让别人看的一脸蒙逼。
如果别人要修改或者新增一个条件,那就要在这个上面继续增加条件。这样恶性循环下去,原本只有几个if else最后就有可能变成十几个,甚至几十个。
别说不可能,我就见过有人在React组件里面用了大量的if else,可读性和可维护性非常差。(当然,这个不算if else的锅,主要是组件设计的问题)

这篇文章主要参与自《代码大全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];
概念

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

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

使用表驱动法前需要思考两个问题,一个是如何从表中查询,毕竟不是所有场景都像上面那么简单的,如果if判断的是不同的范围,这该怎么查?
另一个则是你需要在表里面查询什么,是数据?还是动作?亦或是索引?
基于这两个问题,这里将查询分为以下三种:

直接访问

索引访问

阶梯访问

直接访问表

我们上面介绍的那个日历就是一个很好的直接访问表的例子,但是很多情况并没有这么简单。

统计保险费率

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

if (gender === "female") {
    if (hasMarried) {
        if (age < 18) {
            //
        } else if (age < 65) {
            //
        } else {
            // 
        }
    } else if (age < 18) {
        //
    } else if (age < 65) {
        //
    } else if {
        //
    }
} else {
    ...
}

但是从上面的日历例子来看,这个年龄却是个范围,不是个固定的值,没法用数组或者对象来做映射,那么该怎么办呢?这里涉及到了上面说的问题,如何从表中查询?
这个问题可以用阶梯访问表和直接访问表两种方法来解决,阶梯访问这个后续会介绍,这里只说直接访问表。
有两种解决方法:
1、复制信息从而能够直接使用键值

我们可以给1-17年龄范围的每个年龄都复制一份信息,然后直接用age来访问,同理对其他年龄段的也都一样。这种方法在于操作很简单,表的结构也很简单。但有个缺点就是会浪费空间,毕竟生成了很多冗余信息。

2、转换键值
我们不妨再换种思路,如果我们把年龄范围转换成键呢?这样就可以直接来访问了,唯一需要考虑的问题就是年龄如何转换为键值。
我们当然可以继续用if else完成这种转换。前面已经说过,简单的if else是没什么问题的,表驱动只是为了优化复杂的逻辑判断,使其变得更灵活、易扩展。

enum genders {
    lessThan18 = "<18",
    between18And56 = "18-65",
    moreThan56 = ">65"
}
enum genders {
    female = 0,
    male = 1
}
enum marry = {
    unmarried = 0,
    married = 1
}
const age2key = (age: number): string => {
    if (age < 18) {
        return genders.lessThan18
    }
    if (age < 65) {
        return genders.between18And56
    }
    return genders.moreThan56
}
const premiumRate: {
    [genders: string]: {
        [marry: string]: {
            rate: number
        }
    }
} = {
    [genders.lessThan18]: {
        [genders.female]: {
            [marry.unmarried]: {
                rate: 0.1
            },
            [marry.married]: {
                rate: 0.2
            }
        },
        [genders.male]: {
            [marry.unmarried]: {
                rate: 0.3
            },
            [marry.married]: {
                rate: 0.4
            }
        }
    },
    [genders.between18And56]: {
        [genders.female]: {
            [marry.unmarried]: {
                rate: 0.5
            },
            [marry.married]: {
                rate: 0.6
            }
        },
        [genders.male]: {
            [marry.unmarried]: {
                rate: 0.7
            },
            [marry.married]: {
                rate: 0.8
            }
        }
    },
    [genders.moreThan56]: {
        [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]
}
索引访问表

我们前面那个保险费率问题,在处理年龄范围的时候很头疼,这种范围往往不像上面那么容易得到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];
}

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

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

参考资料:

代码大全(第2版)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101251.html

相关文章

  • 前端如何优化if/else

    摘要:常见例子如何优雅地输出今天星期几今天星期日一二三四五六上面的例子可以看到表就是一个字符串参考资料表驱动法数据驱动编程 前端中的if/else 在编写业务代码的时候,经常会出现条件判断,如果判断条件众多,就会出现if/else天梯,如果新的业务场景出现,就需要再添加一个if/else,这样的代码维护起来,简直是灾难。 if (status === 0) { //do somethin...

    youkede 评论0 收藏0
  • 测试基本面试知识

    测试基本知识1、请你分别介绍一下单元测试、集成测试、系统测试、验收测试、回归测试:(1)单元测试:完成最小的软件设计单元(模块)的验证工作,目标是确保模块被正确的编码,使用过程设计描述作为指南,对重要的控制路径进行测试以发现模块内的错误,通常情况下是白盒的,对代码风格和规则、程序设计和结构、业务逻辑等进行静态测试,及早的发现和解决不易显现的错误。(2)集成测试:通过测试发现与模块接口有关的问题。目...

    不知名网友 评论0 收藏0
  • 测试面试基础知识

    摘要:静态测试包括对于代码测试,主要是测试代码是否符合相应的标准和规范。集成测试,是单元测试的下一阶段,是指将通过测试的单元模块组装成系统或子系统,再进行测试,重点测试不同模块的接口部门。 ...

    DDreach 评论0 收藏0
  • 史上最难PHP笔试题答案

    摘要:支持字符串哈希列表集合有序集合等数据结构,目前不支持事务。是多入口以下关于表驱动法的描述,错误的是表驱动法可以作为复杂继承结构的替代方案,难点在于一个经过深思熟虑的查询表。表驱动法查找无规则分布的数据采用阶梯访问的方法最佳。 1、有关PHP字符串的说法,不对的是: CA、如果一个脚本的编码是ISO-8859-1,则其中的字符串也会被编码为 ISO-8859-1。B、PHP的字符串在内部...

    yuxue 评论0 收藏0
  • 第6章:可维护性软件构建方 6.3可维护性构建技术

    摘要:遵循特定规则,利用操作符,终止节点和其他非终止节点,构造新的字符串非终结符是表示字符串的树的内部节点。语法中的生产具有这种形式非终结符终结,非终结符和运算符的表达式语法的非终结点之一被指定为根。 大纲 基于状态的构建 基于自动机的编程 设计模式:Memento提供了将对象恢复到之前状态的功能(撤消)。 设计模式:状态允许对象在其内部状态改变时改变其行为。 表驱动结构* 基于语法的构...

    young.li 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<