软件构建(八)——表驱动法与一般控制问题

表驱动法是一种编程模式——从表里面查找信息而不使用逻辑语句(if和case)。事实上,凡是能通过逻辑语句来选择的事物,都可以通过查表来选择。本文将介绍直接访问表、索引访问表和阶梯访问表三种常见的表驱动法。最后,本文将记录一些控制问题(如布尔表达式的使用、空语句、深层嵌套问题等等)上的使用原则,这一部分内容比较简单而且与前面的章节有一些重叠,因此将快速带过。

1. 表驱动法

例如当使用复杂的if-elseif逻辑对字符分类时,可以用一个查询表(数组或字典)来代替查询,当然,查询表要事先创建好。

1.1 直接访问表

使用直接访问法的例子:

  • 一个月中的天数:建立含12个整数的数组,把月份当下标查询
  • 保险费率:费率随年龄、性别、婚姻状况等变化,可以以这几个维度建立多维数组,从外部读入数据
  • 灵活的消息格式:假定一份文件中有几百条消息,消息种类约20种,每种消息都有若干字段。可以把消息种类构造为查询表,并把每种字段对应的行为用多态实现,这样可以大幅简化对消息种类和字段进行判断

使用直接访问法查表时,关键是能直接得到查询的键值。有时像保险费率中的年龄,可能小于18岁,18-65岁,超过65岁是三种不一样的费率,这时可以使用以下这些方法将其转换为可查询的键值:

  • 复制信息:查询表填充18个18岁以下的费率,47个18-65岁的费率,以此类推,缺点是复制的冗余会浪费空间,而且表中存在错误的可能性也会增加
  • 转换键值:将一个区间通过某个函数转换为一个值,例如max(min(66, age), 17)可以生成一个位于17到66之间的键值,这种方法要精心设计转换函数
  • 把键值转换提取成独立子程序:上面的转换键值方法其实不太适用于年龄转换这种复杂情况,编写一个KeyFromAge()方法里面写几个if判断将年龄转换为键值更加清晰

1.2 索引访问表

使用索引的时候,先用一个基本类型的数据从一张索引表中查出一个键值,然后再用这一键值查出你感兴趣的主数据。下图解释了这种技术的具体原理:

索引访问表的原理

索引访问技术有几个主要优点:首先,如果主查询表中的每一条记录都很大,那么创建一个浪费了很多空间的索引数组所用的空间,就要比创建一个浪费了很多空间的主查询表所用的空间小得多;其次,即使用了索引以后没有节省内存空间,操作位于索引中的记录有时也要比操作位于主表中的记录更方便廉价;最后,索引访问技术在可维护性上所具有的普遍优点,编写到表里面的数据比嵌入代码中的数据更容易维护。

1.3 阶梯访问表

阶梯结构的基本想法是,表中的记录对于不同的数据范围有效,而不是对不同的数据点有效。最常见的例子是按分数段(浮点数)评定ABCDF等级。由于是浮点数划分范围,用数据转换函数或索引都不适合。为了使用阶梯方法,要把每一区间的上限写入一张表里,然后写一个循环,按照各区间的上限来检查分数,当分数第一次超过某个区间的上限时,就知道相应的等级了。除此之外,还可以将这种方法应用在概率分布的统计(这在游戏中的抽奖相当常见),这种无规则分布的数据是不可能用一个函数把它们整齐地转换成表键值的。使用阶梯访问表需要注意一些细节:

  • 留心端点:充分考虑每一个阶梯区间的上界,不要把<误用为<=
  • 考虑用“准”二分查找取代顺序查找
  • 考虑用索引访问采取代阶梯技术:如果执行速度很重要,应考虑用空间换时间的索引表技术
  • 把阶梯表查询操作提取成单独的子程序

2. 一般控制问题

2.1 布尔表达式的使用

  • 用true和false做布尔判断,而不要用0和1等数值
  • 隐式地比较布尔值:即使用while (!done)而不要写成while (done == false)
  • 简化复杂的表达式:可以通过拆分复杂的判断并引入新的布尔变量,把复杂的表达式提取成布尔函数,用决策表代替复杂的条件等方法
  • 编写肯定形式的布尔表达式:在变量命名上尽量采用肯定形式,if语句的布尔表达式尽量不用not形式
  • 德摩根定理简化否定的布尔判断
  • 用括号便布尔表达式更清晰
  • 理解布尔表达式是如何求值的:主要是要充分理解所用编程语言中“短路求值”的用法
  • 按照数轴的顺序编写数值比较表达式
  • 与0比较时应该:隐式地比较逻辑变量,显式地把数字和0相比较,显式地把指针与NULL相比较,在C语言中显示地比较字符和\0终止符

2.2 空语句

  • 万非得以使用空语句时,要突出这种用法:空语句并不多见,应该让其独占一行,加以缩进,用成对的花括号括住空语句以表强调
  • 为主语句创建一个DoNothing()预处理宏或者内联函数
  • 考虑如果换用一个非空的循环体,是否会让代码更清晰

2.3 深层嵌套问题

过分深层的缩进(嵌套)是产生混乱代码的罪魁祸首之一。有研究表明,应避免使用超过3到4层的嵌套。下面给出一些用于避免深层嵌套的方法。

  • 通过重复检测条件中的某一部分来简化嵌套的if语句
  • 在循环中用break块来简化嵌套if
  • 把嵌套if转换成一组if-else-if语句或case语句
  • 把深层嵌套的代码抽取出来放进单独的子程序
  • 对于复杂的case语句,可以考虑面向对象的手段来简化
  • 重新设计深层嵌套的代码

2.4 控制结构与复杂度

降低软件复杂度首先要知道如何度量复杂度,其中最著名的方法是计算子程序中“决策点”的数量:

  1. 从1开始,一直往下筒骨哦程序
  2. 一旦遇到ifwhilerepeatforandor这些关键字或同类的词,就加1
  3. 给case语句中每一种情况都加1

通过下面的评分来分析子程序的复杂度:

  • 0-5:子程序可能还不错
  • 6-10:得想办法简化子程序了
  • 10+:把子程序的某一部分拆分成另一个子程序并调用它

把子程序的一部分提取成另一个子程序,不会降低整个程序的复杂度,只是把决策点移到其他地方。但是这样做可以降低你在同一时间必须关注的复杂度水平。由于重点是要降低你需要在头脑中同时考虑的项目的数量,所以降低一个给定子程序的复杂度是有价值的。10个决策点的上限并不是绝对的。应该把决策点的数量当作一个警示,该警示说明某个子程序可能需要重新设计了,不要死守决策点上限这个规则。

参考文献:电子工业出版社《代码大全(第2版)》第18、19章

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器