-----------------------------------------------------------------------------------------------------------------------------
使用表驱动技术优化程序结构
转自 http://blog.youkuaiyun.com/vector03/article/details/6444050
1.前言
所谓的表驱动技术实际上是一种回调函数(callback function),它能够使调用者在完全不知道细节的情况下完成复杂的操作。使用这种技术不但可以使你的代码更加紧凑,还能够降低各个模块的耦合程度,优化程序结构。
表驱动的适用范围很广,无论是底层驱动开发,还是上层通讯都可以用到,甚至Microsoft著名的MFC中的消息映射也使用了这一技术。本文结合一个例子介绍了如何在实际工程中使用该技术。
2.简化你的switch-case
试想一下,如果你的程序需要处理这么一种情况:你会不停的接收到各种各样的外部请求,然后需要在自己的函数中针对不同请求作出相应的反应,你该怎么做?
最直接的想法无非是使用分支语句来调用不同的函数,比如下面的代码:
BOOL DispatchFunction(COMMANDMSG CommandID)
{
switch(CommandID)
{
case COMMAND1:
{
Func1();
break;
}
case COMMAND2:
{
Func2();
break;
}
……
default:
{
return FALSE;
}
}
return TRUE;
}
如果你是个有一定经验的程序员,你可能会考虑到使用宏来将每个case分支简化成一行。的确,这也是不错的方法,但是治标不治本。能不能找到一种方法将一个上百行的函数简化到区区不到十行呢?怎么样?很简单是吧。当然,如果处理的规模小,这么做是正确的方法。然而如果你面对的是几十种甚至上百种请求呢?设想一个有着上百个分支的程序,天呐,光是想头都快炸了……
答案是肯定的。我们可以构建一个数组,数组中每个元素存放着外部请求和对应操作的映射关系。现在,问题就被简化成在一个循环当中遍历一个数组,抽取请求,然后执行操作。像刚才的程序就可以这么写:
BOOL DispatchFunction(COMMANDMSG CommandID)
{
for (int i = 0; i < nItemCount; i++)
{
if (DispatchTable[i].KeyVal == CommandID)
{
*DispatchTable[i].HandleFunc();
return TRUE;
}
}
return FALSE
}
在上面的程序中,DispatchTable是一个结构体数组,也就是我们这个函数的驱动表。KeyVal是键值,HandleFunc是指向函数的指针,它们被包含在一个结构体中,并存放在驱动表里。函数通过访问表中的每个元素与外部请求CommandID进行比对,如果一致,则通过函数指针来调用相应的函数。这样,我们所作的工作就与刚才那个上百行的分支函数是一样的了。如何,我们刚刚将一个繁冗不堪的函数变成了一个干净整洁,区区只有不到十行代码的程序!这只是一个开始,使用表驱动你还可以看到更多好处。
另一方面,你可能还注意到这种写法会带来额外的好处。如果增加了新的请求方式,或者必须改写旧有的请求关系,我们根本不需要修改函数DispatchFunction,只要维护这个驱动表DispatchTable就可以完成这个工作了。因为这个函数甚至不清楚它自己干了些什么。它就像一个盒子,将驱动表装载到里面,然后执行一些简单的操作。你甚至能够在运行时替换掉里面的驱动表,从而达到动态装载的目的。
接下来我们来看一个例子,这种技术可以帮助我们完成什么样的任务。
3.例子:使用表驱动编写你的命令解释器
在通讯程序中,命令解释器是一个必不可少的组件。我们将底层收上来的报文加以拆分,提取中间的命令字段交给命令解释器来解释,之后执行各种操作。由此可见,命令解释器本身就是一个具有多分支的程序,正好适合采用表驱动。
这里举的例子是在笔者参与的实际工程中使用的命令解释器。首先必须定义函数指针和驱动表中元素的结构体,代码如下:
typedef void(CCmdTarget::*HANDLEFUNC)(PBYTE lpBuffer);
typedef struct _DISPATCHITEM
{
BYTE m_byKeyVal;
HANDLEFUNC m_HandleFunc;
}DISPATCHITEM, *PDISPATCHITEM;
由于相应的执行函数位于窗体中,因此函数指针类型前面带有CCmdTarget前缀。m_byKeyVal代表键值,也就是报文中提取出的命令段。m_HandleFunc是对应的执行函数指针。
命令解释器的代码如下:
BOOL CSCIComm::CommandInterpreter(BYTE byCommandID, PBYTE lpBuffer)
{
for (int i = 0; ; i++)
{
if (m_DispatchTbl[i].m_byKeyVal == COMMPTL_RESERVED_DISPATCHTBLEND)
{
return FALSE;
}
else if (m_DispatchTbl[i].m_byKeyVal == byCommandID && m_DispatchTbl[i].m_HandleFunc)
{
(m_pPortOwner->*m_DispatchTbl[i].m_HandleFunc)(lpBuffer);
return TRUE;
}
}
}
通过使用表驱动来编写命令解释器,可以得到很多好处:这部分代码跟上一节最后提到的那个函数非常类似,其中m_DispatchTbl是命令解释器CommandInterpreter的驱动表。所不同的是,为了提升程序的灵活度,在for循环中没有边界检查。这样,为了避免程序死循环,我们在驱动表的最后还要加上终止标识,也就是COMMPTL_RESERVED_DISPATCHTBLEND。另外在C++中,由于成员函数的调用实际上前面会存在隐含的this指针,使用函数指针调用同样如此。所以在这里增加了一个成员变量m_pPortOwner用来代替this指针调用这些成员函数。
首先是代码的简洁性,这个优点不言自明。
其次,你可以将底层代码和协议层分离。命令解释器属于下层,它会反上来很多命令,然后加以派送,但是它根本不需要关心该如何处理这些问题,因为只要加载驱动表然后“按图索骥”就好了。
最后,你甚至可以在程序运行中动态装载驱动表,从而达到不停止运行就能改变通信协议的目的,这一点提供了极高的灵活性。
你看,一个基本的表驱动结构优点多多而且实现起来也很简单。我们需要的额外操作只是构建这个驱动表并且对其加以维护而已。接下来,我会另外介绍一些小技巧来使这部分程序获得更大的方便性和灵活性,尽管这部分内容并不直接属于表驱动的范畴。
4.还可以更加灵活
前面提到,表驱动本身需要驱动表的构建。关于构建驱动表,你可以在类的构造函数中或者某个窗体的OnCreate或是OnInitalUpdate中,构建一个静态的数组,然后顺带进行初始化赋值。
但是,如果你考虑到代码的可读性,以及你的代码和其他代码的交互性。比如解释层和底层是由你来做,但是协议层是别人来做。你就不得不考虑如何能让别人更方便的构建想要的驱动表。这里提供一些小技巧可以作为参考。
首先,你可以定义一些宏,采用宏的方式将命令段和执行函数映射到这张表里。当然它们会标识有起始和结束的标志。在例子中定义如下:
#define DISPATCH_MAP_BEGIN(thePointer) /
if (!*thePointer){ /
static const DISPATCHITEM _DispatchTbl[] = /
{
#define DISPATCH_ITEM_MAP(byKeyVal, pHandleFunc) /
{ (BYTE)byKeyVal, (HANDLEFUNC)pHandleFunc },
#define DISPATCH_MAP_END(thePointer) /
{ COMMPTL_RESERVED_DISPATCHTBLEND, NULL } /
}; /
*thePointer = (PDISPATCHITEM)&_DispatchTbl[0]; /
}
而DISPATCH_ITEM_MAP就是负责命令字和执行操作的映射,它会把二者写入建立好的静态数组中。其中,DISPATCH_MAP_BEGIN和DISPATCH_ITEM_END分别代表映射开始和结束。它们的作用就是建立一个静态的常量数组,然后在映射最后写入结束标志,并且将这个静态的数组的地址传递给数组指针,也就是thePointer代表的对象。
具体的使用如下所示:
PDISPATCHITEM* pointer = m_SCIComm.GetDispatchTbl();
DISPATCH_MAP_BEGIN(pointer)
DISPATCH_ITEM_MAP(COMMPTL_REQ_INPUT, &CMainFrame::TrackParamHandler)
DISPATCH_ITEM_MAP(COMMPTL_REQ_OUTPUT, &CMainFrame::TrackParamHandler)
DISPATCH_ITEM_MAP(COMMPTL_MDY_INPUT, &CMainFrame::ModifyInputHandler)
DISPATCH_ITEM_MAP(COMMPTL_MDY_INPUT_E2, &CMainFrame::ModifyInputHandler)
DISPATCH_ITEM_MAP(COMMPTL_IDLECYC_MAX, &CMainFrame::SaveIdleCycleMaxHandler)
……
DISPATCH_MAP_END(pointer)
怎么样,是不是清楚多了?而且你会否有似曾相识的感觉,很像MFC中消息映射的格式?没错,实际上MFC中消息映射也是一个典型的表驱动应用。
另外,介绍的例子中命令解释器接收的参数byCommandID使用的是BYTE类型。为了提高可扩展性,你完全可以向其中传入一个结构体指针,这样就可以容纳更多的信息。但是,相对的,关于比较操作也必须传入一个回调函数,利用其进行比对操作。对于执行函数也是同样的道理。由于执行函数进行的操作千差万别,参数和返回值很有可能不一致。我们可以用一个结构体指针来替代固定的类型,这样就具有更大的灵活性了。
5.总结
表驱动技术是一种可以使你的代码更简洁,结构更加灵活的技术,最适用于多分支的函数当中。另外,我们可以配合驱动表编写一些可以灵活配置的宏,这样能让你的程序修改起来更加得心应手。
-----------------------------------------------------------------------------------------------------------------------------------
表驱动方法
转自 http://www.cnblogs.com/kerrycode/archive/2009/08/01/1536440.html
表驱动法是一种编程模式(Scheme),从表里面查找信息而不使用逻辑语句(if 和case) 它的好处是消除代码里面到处出现的if、else、swith语句,让凌乱代码变得简明和清晰。对简单情况而言,表驱动方法可能仅仅使逻辑语句更容易和直白,但随着逻辑的越来越复杂,表驱动法就愈发有吸引力。
2:表驱动法的例子演示
假设有段程序要计算某年某月的天数
通常的做法如下:

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

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

大家可能会看到这里会出现大量的switch、case语句,其实这只是个简单的逻辑,如果在业务逻辑复杂的情况下,代码里这些if 、else,switch,case语句必将是铺天盖地的出现。好了,我们来看看,表驱动的方法的简单应用吧

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

对比这两段代码,你会发现如果用表驱动法,你的代码将会更简洁,明了。
3:表驱动法查询数据的方式
直接访问(Direct Access)
索引访问(Index Access)
阶梯访问(Stair-Step Access)
4:表驱动法优点
前面一直在强调表驱动法的优点,下面我们来总结一下:
在适当环境下,使用它能够使代码简单、明了。
修改容易(易维护)、效率更高。
表驱动法的一个好处就是能够大量消除代码中if else, swith 判断。
---------------------------------------------------------------------------------------------------------------------------------
表驱动浅议
转自 http://www.jsjgprjyyy.com/Html/?1260_5.html
摘要:本文深入数据结构中“表”的使用,探讨以表设计为中心来驱动各种复杂信息的处理,也就是表驱动方法;并进一步探讨表中数据的结构封装和表驱动方法的功能扩展。
关键词:数据结构;表驱动;接口封装;功能扩展
中图分类号:tp311.12 文献标识码:a 文章编号:1007-9599 (2010) 06-0000-02
一、表驱动方法简介
表是一种常见的数据结构,但是一般的程序员却几乎很少使用,或者是不会有意识地进行使用。本文探讨的“表驱动”方法是对表这种数据结构的一种理论提炼,通过理论上的把握,使得程序员能够在实践中以表设计为中心来驱动各种复杂信息的处理。《代码大全》提到了表驱动的概念:表驱动方法是一种使你可以在表中查找信息,而不必用逻辑语句来把它们找出来的方法。这里的提到的信息可能是数据,也可能是动作。事实上,任何信息都可以通过表来挑选。
在简单的情况下,逻辑语句往往简单、直接(参见下面的示例代码)。但是,如果逻辑比较复杂,通常就是查找信息的输入、输出数据或动作很多,那么使用表驱动方法就是一个很好的选择了。
对于c语言初学者常常写出以下类似代码:
int demo_tab_driver(int index,void*in,void *out)
{
int rc;
switch(index)
{
case index_a:
rc=processa(in,out);
break;
case index_b:
rc=processb(in,out);
break;
case index_c:
rc=processc(in,out);
break;
..........
default:
rc=not_support;
break;
}
return rc;
}
以上的代码直接使用了逻辑语句(switch-case),由于输出信息比较少,整个代码可读性还是比较好的,也比较简单、直接。但是,如果输出信息成倍增多,那么随着switch-case分支的增多、嵌套,代码可读性将迅速下降,代码的维护也会越来越困难。而且,如果预计输出信息可能会大量增加,这种代码也存在代码扩展问题。
在设计这种信息查找模块时候,应该考虑逻辑的复杂性,输入输出信息的数据量,以及可能的扩展。一般只有在逻辑简单、信息量少,而且基本不存在扩展性问题时,才使用逻辑语句(if-else或switch-case)直接处理。
二、表驱动的简单使用
表驱动方法示例如下:#define dim_tab(x)(sizeof(x)/sizeof(x[0]))//求结构体数组的个数
//表结构定义
typedef struct
{
int index;//表索引
int (*ptrfunc)(void *in, void *out);
} dispatch_tab;
int demo_tab_driver(int index , void *in, void *out)
{
// 表初始化
dispatch_tab tab[] =
{
{index_a, processa},
{index_b, processb},
{index_c, processc},
......
};
int i;
int rc=not_support;
// 查表,并完成相关操作
for( i=0; i<dim_tab(tab); i++ )
{
if( index == tab[i].index )
{
rc=(*tab[i].ptrfunc)(in,out);
break;
}
}
return rc;
}
使用表驱动的好处就是对表的操作部分的代码简短且基本上无需维护。如果需要添加、修改新功能,只需要维护驱动表tab[]就可以了,这就摆脱了冗长乏味的switch-case。
三、表驱动的数据结构封装
上一节例子中各个case分支中的动作,其参数都比较简单,而且类型相同,即都使用了相同的参数in和out,如果各个分支使用的参数类型各不相同,那该怎么办呢?那就需要进行必要的封装,一般是封装一个比较复杂的struct类型,里面嵌套各种union对应不同的接口参数,通过不同的type,运行时从union中动态提取对应类型的数据。
具体方法如下:
typedef struct strpara_in
{
…//此处声明所有的分支公用的数据
int type;//数据类型
union
{
struct {
…
} a_in; //processa分支使用的数据
struct {
…
} b_in; //processb分支使用的数据
struct {
…
} c_in; //processc分支使用的数据
......
}para_in;
}
typedef struct strpara_out
{
…//此处声明所有的分支公用的数据
int type; //数据类型
union
{
struct {
…
} a_out; //processa分支使用的数据
struct {
…
} b_out; //processb分支使用的数据
struct {
…
} c_out; //processc分支使用的数据
......
}para_out;
// 表结构定义
typedef struct
{
int index;
int (*ptrfunc)(para_in *in, para_out *out);
} dispatch_tab;
int demo_tab_driver(int index , para_in *in, para_out *out)
{
… // 此处代码同上节示例
}
}
四、表驱动的功能扩展
有时候case分支有如下特点:多个分支都有许多共同的处理。这就有必要进行进一步的改造,进行功能处理分级:第一级:处理共通部分
第二级:处理该分支特有的功能
示例如下:
// 表结构定义
typedef struct
{
int index;
int (*ptrcommonfunc)(para_in *in, para_out *out); //第一级共通部分
int (*ptrfunc)(para_in *in, para_out *out); //特有功能部分
} dispatch_tab;
int demo_tab_driver(int index , para_in *in, para_out *out)
{
// 表初始化
dispatch_tab tab[] =
{
{ index_a, commonprocess1, processa },
{ index_b, commonprocess1, processb },
{ index_c, commonprocess2, processc },
{ index_d, commonprocess2, processd },
{ index_e, commonprocess2, processe },
{ index_f, commonprocess2, processf },
{ index_g, commonprocess2, processg },
......
};
int i;
int rc = not_support;
// 查表,并完成相关操作
for( i=0; i<dim_tab(tab); i++ )
{
if( index == tab[i].index )
{
(*tab[i].ptrcommonfunc)(in,out); //共通功能处理
rc = (*tab[i].ptrfunc)(in,out);
break;
}
}
return rc;
}
实际运用中,表的构造多种多样,在设计阶段费点时间设计表结构,是很有必要的。
五、表驱动方法实例
下面以一个“菜单操作”例子来介绍表驱动方法的实际使用。软件需求:使用键盘上的"←→"键切换菜单焦点,当焦点处于某菜单时,若敲击键盘上的ok、cancel键则调用该焦点菜单对应之处理函数。
注:以下代码关注于需求的后半部。
/*将菜单的属性和操作"封装"在一起*/
typedef struct strsysmenu
{
char*text; /* 菜单的文本 */
int xpos; /* 菜单在lcd上的x坐标 */
int ypos; /* 菜单在lcd上的y坐标 */
void (*onokfun)(void); /* 在该菜单上按下ok键的处理函数指针 */
void (*oncancelfun)(void); /* 在该菜单上按下cancel键的处理函数指针 */
} sysmenu, *ptrsysmenu;
// (菜单)表初始化
static sysmenu menu[menu_num] =
{
{"menu1", 0, 48, menu1onok, menu1oncancel },
{"menu2", 9, 48, menu2onok, menu2oncancel },
{"menu3", 18, 48, menu3onok, menu3oncancel },
{"menu4", 27, 48, menu4onok, menu4oncancel },
......
};
int currentfocusmenu; //当前光标聚焦于哪个菜单
/* 按下ok键 */
void onokkey(void)
{
menu[currentfocusmenu].onokfun();
}
/* 按下cancel键 */
void oncancelkey(void)
{
menu[currentfocusmenu].oncancelfun();
}
void menu1onok()
{
...
}
void menu1oncancel()
{
...
}
......
void menu4onok()
{
...
}
void menu4oncancel()
{
...
}
以上代码使用了表结构驱动和面向对象中的封装思想,可以看出程序结构相当清晰,维护也相当容易。
如果需要在系统中添加更多的菜单,只需修改表中的数据,也就是说:维护代码简化为维护一张表。
参考文献:
[1]steve mcconnell.代码大全[m].天奥.熊可宜.学苑出版社,1993,1
[2]严蔚敏,吴伟民.数据结构[m].清华大学出版社,1997,4,1
[3]arman danesh.linux从入门到精通[m].邱仲潘.电子工业出版社,1999,3,
---------------------------------------------------------------------------------------------------------------------------------------
善用表驱动法
转自 http://dennis-zane.iteye.com/blog/183886
最近碰到个需求,计算游戏得分的规则,类似这样:
游戏人数 | 第一名获得赌注 | 第二名获得赌注 | 第三名获得赌注 | 第四名获得赌注 |
二人 | 100% | 0% | — | — |
二人 (出现 2 个第 1 名时) | 50% | 50% |
|
|
三人 | 70% | 30% | 0% | — |
三人 ( 出现 3 个第 1 名时 ) | 33.3333% | 33.3333% | 33.3333% |
|
三人 ( 出现 2 个第1 名时 ) | 50% × 2 | 0% |
|
|
......
......
这些奖励规则没有什么规律,随着人数增多,就越发复杂了,并且业务人员可能随时改变这些规则。
显然,奖励规则可以采用策略模式,定义策略接口,根据游戏人数定义不同的规则,本质上就是利用动态的多态调用。可以想见,还是少不了复杂的case... when语句,以及繁多的代码。恰好最近读《unix编程艺术》和《代码大全2》,两者都提到一个结论:人类阅读复杂数据结构远比复杂的控制流程容易,或 者说数据驱动开发是非常有价值的。《代码大全2》声称这个是表驱动法。因此,这个奖励系数的计算,能否转化成一个查表过程呢?注意到,在游戏中,名次是根 据个人的积分在所有玩家中的排位来决定,大概会有这么个排序的玩家积分数组[100,50,3],这个数组表示3个玩家,第一名100分,第二名50分, 第三名才3分。依据规则,第一名的奖励系数就是0.7,第二名就是0.3。我想到类似这样的数组其实都有个形式表示它们的内置结构,比如 [100,50,3]数组的“结构”是"111",代表3个位置都有一个人。将"111"作为关键码去查表不就OK了?
将每个排好序的积分数组解码为这样的关键码,然后去查预先写好的奖励系数表,这个奖励系数表大概类似:
- @@award_rate_hash={
- :"2"=>{
- :"11"=>{:"1"=>1,:"2"=>0},
- :"20"=>{:"1"=>0.5,:"2"=>0.5}
- },
- :"3"=>{
- :"111"=>{:"1"=>0.7,:"2"=>0.3,:"3"=>0},
- :"300"=>{:"1"=>0.33},
- :"201"=>{:"1"=>0.5,:"3"=>0},
- :"120"=>{:"1"=>1,:"2"=>0}
- },
- :"4"=>{
- :"1111"=>{:"1"=>0.65,:"2"=>0.30,:"3"=>0.05,:"4"=>0},
- :"4000"=>{:"1"=>0.25},
- :"3001"=>{:"1"=>0.33,:"4"=>0},
- :"1300"=>{:"1"=>1,:"2"=>0},
- :"2020"=>{:"1"=>0.5,:"3"=>0},
- :"1201"=>{:"1"=>0.7,:"2"=>0.15,:"4"=>0},
- :"1120"=>{:"1"=>0.7,:"2"=>0.3,:"3"=>0},
- :"2011"=>{:"1"=>0.35,:"3"=>0.3,:"4"=>0}
- }
- }
一个三级hash表,首先根据玩家人数查到特定的系数表,比如要查3个玩家、积分数组是[100,50,3]的奖励系数表就是 @@award_rate_hash [:"3"],然后积分数组 [100,50,3]解码为:"111",继续查,如此规则的奖励系数表就是 @@award_rate_hash [:"3"][ :"111"]——也就是 {: " 1 " => 0.7 ,: " 2 " => 0.3 ,: " 3 " => 0},那么查积分是100的玩家系数就是 @@award_rate_hash [:"3"][ :"111"][ ([100,50,3].index(100)+1).to_s.to_sym],也就是 : " 1 " =>0.7 。 [100,50,3].index(100)+1就是积分100的玩家在数组中的名次(即1),也就是:"1"指向的结果0.7 。其他玩家的查表过程与此类似,不细说了。
这样,我们所有的奖励规则就是维护这么一张hash表,这个表看起来复杂,其实完全可以自动生成,让业务人员来提供样例数据,解码样例数据并生成这个表是很简单的事情。积分数组的“解码”,我给一个Ruby版本:
- #解码数组为字符串关键码
- def decode_array(array)
- len=array.size
- trace_list=[]
- result=[]
- len.times do |time|
- result[time]=0
- trace_list[time]=false
- end
- array.each_with_index do |item,index|
- result[index]=count_times(array,trace_list,index,len)
- end
- return result.join('').to_sym
- end
- def count_times(array,trace_list,index,len)
- item=array[index]
- result=0
- (index..len).each do |i|
- if array[i]==item and !trace_list[i]
- result+=1
- trace_list[i]=true
- end
- end
- return result
- end