使用表驱动技术优化程序结构

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_MAP_BEGINDISPATCH_ITEM_END分别代表映射开始和结束。它们的作用就是建立一个静态的常量数组,然后在映射最后写入结束标志,并且将这个静态的数组的地址传递给数组指针,也就是thePointer代表的对象。

DISPATCH_ITEM_MAP就是负责命令字和执行操作的映射,它会把二者写入建立好的静态数组中。

具体的使用如下所示:

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.总结

表驱动技术是一种可以使你的代码更简洁,结构更加灵活的技术,最适用于多分支的函数当中。另外,我们可以配合驱动表编写一些可以灵活配置的宏,这样能让你的程序修改起来更加得心应手。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值