表驱动法

第十八章 表驱动法

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

18.1 表驱动法使用总则

在适当的环境下,采用表驱动法,所生成的代码回避复杂的逻辑代码更简单、更容易修改,而且效率更高。假设你希望把字符划分成字母、标点和数字三类,那么你也许会用到下面这种复杂的逻辑链:
if( ( ( 'a' <= inputChar) && ( inputChar <= 'z') ) ||
	( ( 'A' <= inputChar) && ( inputChar <= 'Z') ) ) {
		charType = CharacterType.letter;
}
else if ( ( inputChar == ' ') || ( inputChar == ',') ||
( inputChar == '.') || ( inputChar == '!') || ( inputChar == '(') ||
( inputChar == '|') || ( inputChar == ';') || ( inputChar == ':') ||
( inputChar == '?') || ( inputChar == '-') ) {
	charType = CharacterType.Punetuation;
}
else if ( ( '0' <= inputChar ) && ( inputChar <= '9' ) ) {
	charType = CharacterType.Digit;
}
另一方面,如果用一个查询表(lookup table)。就可以把每一个字符的类型保存在一个用字符编码访问的数组里。那么上述的复杂代码片段就可以替换为:
charType = charTypeTable[ inputChar ];
使用表驱动法的两个问题
在使用表驱动法的时候,必须要解决两个问题。首先,你必须要回答怎 样从表中查询条目的问题。你可以用一些数据来直接访问表。比如说,如果你希望把数据按月份进行分类,那么创建一个月份表示非常直截了当的。你可以用一个下标从1到12的数组实现它。
另一些数据可能很难直接用于查表。例如,假设你希望按照社会安全号码做数据分类,那么除非你可以承受在表里面存放 999-999-9999 条记录。否则就不能用社会安全号码直接查表。你会被迫采用一种更为复杂的方法。下面是从表里面查询记录的方法列表:
直接访问(Direct access)
索引访问(Indexed access)
阶梯访问(Stair-up access)

在使用表驱动法的时候,需要解决的第二个问题是, 你应该在表里面存些什么。有的时候,表查询出来的结果是数据。如果你遇到的是这种情况,那么就可以把这些数据保存到表里面。在另外一些情况下,表查询出来的结果是动作(action)。在这种情况下,你可以保存一个描述该动作的代码,或者,在有些语言里,你可以保存着对实现该动作的子程序的引用。无论是哪一种情况,表都会变得更为复杂。

18.2 直接访问表

和所有的查询表一样,直接访问表代替了更为复杂的逻辑控制结构。之所以说它们是“直接访问”的,是因为你无须绕很多复杂的圈子就能够在表里面找到你想要的信息。
示例:一个月中的天数
假设你需要计算每个月中的天数(为了说明期间,此处不考虑闰年)。本做法就是写一个大的if语句:
if ( month == 1 )
	days = 31;
else if ( month == 2 )
	days = 28;
else if ( month == 3 )
	days = 31;
else if ( month == 4 )
	days = 30;
else if ( month == 5 )
	days = 31;
else if ( month == 6 )
	days = 30;
else if ( month == 7 )
	days = 31;
else if ( month == 8 )
	days = 31;
else if ( month == 9 )
	days = 30;
else if ( month == 10 )
	days = 31;
else if ( month == 11 )
	days = 30;
else if ( month == 12 )
	days = 31;
实现同样功能的一种更为简单、更容易修改的方法是把这些数据保存到一张表里面。首先创建出这张表:
// Initialize Table of "Days Per Month" Data
int daysPerMonth[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
现在,你无需再写那条长的if语句,只需要一条简单的数组访问语句就可以得出每个月中的天数了:
days = daysPerMonth[ month - 1 ];
示例:灵活的消息格式
假设你编写了一个程序,打印存储在一份文件中的消息。通常该文件会存储大约500条消息,而每份文件中会存有大约20种不同的消息。每一条消息都有若干字段,并且每条消息都有一个消息头,其中有一个ID,告诉你该消息属于这20多种消息中的哪一种。
这些消息的格式并不是固定不变的,它们由你的客户来确定,而你也无法要求你的客户去把格式稳定住。
基于逻辑的方法
如果你采用基于逻辑的方法,那么你可能会读取每一条消息,检查其ID,然后调用一个用来阅读、解释以及打印一种消息的子程序。如果你有20中消息,那么就要有20个则程序。你还要写出不知道多少底层子程序去支持它们。
每次有任何一种消息的格式变了,你就不得不修改负责处理该消息的子程序或者类的逻辑。
在基于逻辑的方法中,其消息阅读子程序包含一个循环,用来读入消息、解释其ID,以及根据ID调用20个子程序中的某一个。下面就是基于逻辑方法所用的伪代码:
While more messages to read
	Read message header
	Decode the message ID from the message header
	If the message header is type 1 then
		Print a type 1 message
	Else if the message header is type 2 then
		Print a type 2 message 
	...
	Else if the message header is type 19 then
		Print a type 19 message 
	Else if the message header is type 20 then 
		Print a type 20 message 
End While
面向对象的方法
如果你采用某种面向对象的方法,那么为题的逻辑将被隐藏在对象继承结构里,但是基本结构还是同样的复杂:
While more messages to read
	Read message header
	Decode the message ID from the message header
	If the message header is type 1 then
		Instantiate a type 1 message object
	Else if the message header is type 2 then
		Instantiate a type 2 message object 
	...
	Else if the message header is type 19 then
		Instantiate a type 19 message object 
	Else if the message header is type 20 then 
		Instantiate a type 20 message object 
End While
无论是直接写逻辑,还是把它包含在特定的类里面,这20中消息中的每一种都要有自己的消息打印子程序。子程序也可以用伪代码来标识,下面就是读取和打印浮标温度消息子程序的伪代码:
Print "Buoy Temperature Message"


Read a floating-point value
Print "Average Temperature"
Print the floating-point value


Read a floating-point value
Print "Temperature Range"
Print the floating-point value


Read an integer value
Print "Number of Samples"
Print the integer value


Read a character string
Print "Location"
Print the character string


Read a time of day
Print "Time of Measurement"
Print the time of day
这只是针对一种消息的伪代码。其他19中消息也都需要有相似的代码。而且如果增加了第21种消息,那要么增加第21个子程序,要么增加增加第21个类——无论如何,新增加一种消息类型都要求修改代码。
表驱动法
表驱动法要比前几种方法都经济。其中的消息阅读子程序由一个循环组成,该循环负责读入每一个消息头,对其ID解码,在Message数组中查询其消息描述,然后每次都调用同一个子程序来解释该消息。永乐表驱动法之后,你可以用一张表来描述没一种消息的格式,而不用再把它们硬编码进程序逻辑里。这样会降低初期编码的难度,生成更少的代码,并且无需修改代码就可以很轻松地进行维护。
为了使用该方法,你需要先列出消息种类和字段类型。在C++中,你可以按照下面的方法来定义所有可能的字段类型:
//C++示例:定义消息数据类型
enum FieldType {
	FieldType_FloatingPoint,
	FieldType_Integer,
	FieldType_String,
	FieldType_TimeOfDay,
	FieldType_Boolean,
	FieldType_BitField,
	FieldType_Last = FieldType_BitField
};
不用再为20种消息中的每一种硬编码打印子程序,你可以只创建少数几个子程序,分别负责打印每一种基本数据类型——浮点、整型、字符串等。你可以把每种消息的内容描述放在一张表里(包括每个字段的名称),然后再根据该表中的描述来分别解释没一种消息。下面是用于描述一种消息的表记录的示例:
示例:定义消息表中的一项
Message Begin
	NumFields 5
	MessageName "Bouy Temperature Message"
	Field 1, FloatingPoint, "Average Temperature"
	Field 2, FloatingPoint, "Temperature Range"
	Field 3, Integer, "Number of Samples"
	Field 4, String, "Location"
	Field 5, TimeOfDay, "Time of Measurement"
Message End
这张表既可以硬编码在程序里(在这种情况下,所示的每一元素都将被赋给一个变量),也可以在程序启动时或者随后从文件中读出。
一旦把消息定义读入程序,那么你就能把所有的信息嵌入在数据里面,而不必嵌入在程序的逻辑里面了。数据要比逻辑更为灵活。当消息格式改变的时候,修改数据是很容易的。如果必须要新增一种消息类型,那么只须网数据表里再增加一项元素即可。
下面就是表驱动法中最上层循环的伪代码:
While more messages to read
	Read message header
	Decode the message ID from the message heaeder
	Look up the message description in the message-description table
	Read the messag fields and print them based on the message description
End While
这与基于逻辑的伪代码相比,实在是太简单了。在这一层下面的逻辑里面,你会发现一个子程序就可以解释消息描述表里面的消息描述、读入消息数据并且打印消息。这个子程序比任何一个基于逻辑的消息打印子程序都要通用,它不算太复杂,而且它只是一个子程序,而不是20个:
While more fields to print
	Get the field type from the message description
	Case ( fileld type )
		of ( floating point )
			read a floating-point value
			print the field label
			print the floating-point value
		of ( integer )
			read an integer value
			print the field label
			print the integer value
		of ( character string )
			read a character string
			print the field label
			print the character string
		of ( boolean )
			read a single flag
			print the field label
			print the single flag
		of ( bit field )
			read a bit field
			print the field label
			print the bit field
	End Case
End While
诚然,这个有着6种情况的子程序要比只负责打印浮点温度消息的子程序长一些。但它是你要使用的唯一的打印子程序。你不必为了其他那19中消息再写19个子程序。这个子程序可以处理6中字段类型,负责处理所有的消息类型。
这个子程序也显示出了实现这类表查询操作的最复杂的一种方法,因为它用到了一个case语句。另外一种方法是创建一个抽象的AbstractField类,然后为每一中字段类型派生一个子类。这样你就无需使用case语句,只需调用适当类型对象的成员函数即可。
你可以按照如下方法用C++来创建这些对象类型:
//C++示例:建立对象类型
class AbstractField {
	public:
	virtual void ReadAndPrint ( string, FileStatus &) = 0;
};
class FloatingPointField : public AbstractField {
	public:
	virtual void ReadAndPrint ( string, FileStatus &) {
		...
	}	
};


class IntegerField ...
class StringField ...
...
这段代码片段为每个类声明了一个成员函数,它具有一个字符串参数和一个FileStatus参数。
下一步是声明一个数组以存放着一组对象。该数组就是查询表,如下所示:
C++示例:创建一个用于持有个类型的对象的表
AbstractField * field[ FieldType_Last_Last ];
建立对象表的最后一步是把具体对象的名称赋给这个Field数组:
//C++示例:建立对象清单
field[ FieldType_FloatingPoint ] = new FloatingPoint();
field[ FieldType_Integer ] = new IntegerField();
field[ FieldType_String ] = new StringField();
field[ FieldType_TimeOfDay ] = new TimeOfDayField();
field[ FieldType_Boolean ] = new BooleanField();
field[ FieldType_BitField ] = new BitFlagField();
这段代码片段假定,位于赋值语句右边的FloatingPointField等标识符是类型为AbstractField的对象的名称。把这些对象赋给数组总的数组元素意味着,为了调用正确的 ReadAndPrint() 子程序,你只需要引用一个数组元素而不是直接用某个具体类型的对象。
一旦建立了这个子程序表,要处理的消息里的一个字段,你只需要方位对象表并且调用表中的一个成员函数就可以了。代码如下:
//C++示例:在表中查询对象及其成员函数
fieldIdx = 1;
while ( (fieldIdx <= numFieldsInMessage ) && ( fileStatus == OK ) ) {
	fieldType = fieldDescription[ fieldIdx ].FieldType;
	fieldName = fieldDescription[ fieldIdx ].FieldName;
	field[ fieldType ].ReadAndPrint( fieldName, fileStatus );
}
构造查询键值
我们总是希望能够直接访问到表的键值,然而,很多时候数据并不会那么合作。那么可以按照下面的方法来构造这些键值。
复制信息从而能够直接使用键值 就是将那些能够直接访问的键值的信息复制给那些不能访问的数据上。这样做的优点在于表自身的结构非常简单,访问表的操作也很简单。这样做的缺点在于,复制生成的冗余信息会浪费空间,并且表中存在错误的可能性也增加了。
转换键值以使其能够直接使用 创建一个用来转换键值的函数,将你想要的键值转换成可以直接访问的键值。
把键值转换提取成独立的子程序 这样做可以避免在不同位置执行了不同的转换,也使得转换操作修改起来更加容易。

18.3 索引访问表

有时候,无法通过一个简单的数学运算将一个数据转换成键值。这类情况中的一部分可以通过使用索引访问的方法加以解决。
当你使用索引的时候,先用一个基本类型的数据从一张表中查出一个键值,然后再用这一键值查出你感兴趣的主数据。
假设现在有100种商品,每种商品都有一个4位数字编号,其范围是0000到9999.在这种情况下,用这个编号作为键值直接查询一张描述商品信息的表,那么就要生成一个具有10000条记录的索引数组(从0到9999)。该数组中除了与商店中的货物的标志相对应的100条记录意外,其余都是空的。
索引访问技术有两个主要优点:首先,如果主查询表中的每条记录都很大,那么创建一个浪费了很多空间的索引数组所用的空间,就要比创建一个浪费了很多空间的主查询表所用的空间小得多。举例来说,如果主表中的每条记录需要占用100字节,而索引表中每条记录只需要占用2字节。假设主表有100条记录,而用来访问它的数据有10000种可能取值。这样一来,你面临的就是在10000条索引记录和10000条主数据成员记录之间做出选择。如果你用的是一套索引,那么用掉的总内存量是20000自己,如果你放弃了索引结构,而把孔家耗费在主表里面,那么用掉的总内存量就会是1000000字节。
第二项有点是,即使你用了索引以后没有节省内存空间,操作位于索引中的记录有时也要比操作位于主表中的记录更方便更廉价。比如说,有一张含有员工姓名、雇佣日期和薪水的表,你可以生成一个索引来按章员工姓名访问该表,生成另一个索引表按照雇佣时间来访问该表,以及生成第三个索引按照薪水来访问该表。
索引访问技术的最后一个优点就是表查询技术在可维护性上具有的普遍优点。编写到表里面的数据比嵌入代码中的数据更容易维护。为了使这种灵活性最大化,可以把借助索引访问数据的代码提取成单独的子程序,然后再希望通过物品编号获得表键值的时候调用该子程序。当需要修改表的时候,你可以考虑更换索引访问技术,或者换用另一种表查询的技术。如果你不把索引访问代码随笔啊写到应用程序中各个地方,那么这种访问技术更改起来是非常容易的。
18.4 阶梯访问表
阶梯访问表方法不像索引结构那样直接,但是它要比索引访问方法节省空间。阶梯方法通过确定没想命中的阶梯层次确定其归类,它命中的“台阶”确定其类属。
举例来说,如果你正在开发一个等级评定的应用程序,其中“B”记录所对应的范围是75%到90%。下面是你某一天可能会写到的等级区间:
>= 90.0% A
< 90.0% B
< 75.0% C
< 65.0% D
< 50.0% E
这种划分范围用在表查询中是很糟糕的,因为你不能用简单的数据转换函数来把表键值转换为A至E字母所代表的等级。用索引也不合适,因为这里用的是浮点数。你可能想到把浮点数转换成整数,从而使应用程序变成可能。但是,为了掩饰期间,这个例子还会继续使用浮点数。
为了使用解题方法,你要把每一区间的上限写入到一张表里然后写一个循环,按照个区间的上限来检查分数。当分数第一次超过某个区间的上限时,你就知道相应的等级了。在使用解题方法的时候,你必须要谨慎地处理范围的端点。下面就是根据这个例子写的为一组学生成绩评判等级的代码:
// set up data for grading table
double rangeLimit[] = { 50.0, 65.0, 75.0, 90.0, 100.0 };
string grade[] = { "A", "B", "C", "D", "E" };
maxGradeLevel = grade.Length - 1;
...
// assign a grade to a student based on the student's score
int gradeLevel = 0;
string studentGrade = "A";
while ( ( studentGrade == "A" ) && ( gradeLevel < maxGradeLevel ) ) {
	if ( studentScore == rangeLimit[ gradeLevel ] )
		studentGrade = grade[ gradeLevel ];
	gradeLevel++;
}
尽管这个例子很简单,但却可以很容把它推广到处理多个学生、多种等级,以及等级发生变化的情况。
与其他表驱动法相比,这种方法的优点在于它很适合处理那些无规则的数据。使用这种方法同样也可以享受到表驱动法所具有的一般性有点:它非常灵活并容易修改。
下面是在使用阶梯技术的时候需要注意的一些细节。
留心端点 需要考虑每一个阶梯的上界。为最高一级区间的最高点假拟出一个值。
考虑用二分查找取代顺序查找 如果列表很大,那么顺序查找的成为效率的一种制约。用二分查找法确定某一个数值的正确分类。
考虑用索引访问来取代阶梯技术 阶梯方法中的查找可能很费时,如果执行速度很重要,你也许会愿意用索引访问发来取代阶梯法,即以牺牲存储空间来换取速度。
把阶梯表查询操作提取成单独的子程序
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值