背景
其实在《设计模式——可利用面向对象软件的基础》一书中,提及的23种设计模式里并没有表驱动这种模式,因为《设计模式》一书更多的是根据面向对象的应用提取出来的设计方法。而表驱动模式本身是强烈依赖于数组这种数据结构的,跟对象扯不上关系,所以没有被收录在此书中。但由于它在C语言中的影响力之大,适用面之广,所以被收录在了《代码大全》(这可是另一本经典著作呀)一书中。
名词释义
表驱动本身是强烈依赖于数组结构,可以是一维数组,也可以是多维数组,然后根据该数据的分布式结构进行数据索引。即使是使用一维数组,也是通过数组下标索引到对应的数据,从索引这个角度来看,是Key-Value这种键-键值的对应关系,像极了在表格中查找数据(通过行和列找到对应的格子),所以称之为表驱动。
例子
跑马灯的实现
#include <stdint.h>
/* LED灯1亮 */
extern void LED1_On(void);
/* LED灯1灭 */
extern void LED1_Off(void);
/* LED灯2亮 */
extern void LED2_On(void);
/* LED灯2灭 */
extern void LED2_Off(void);
void LED_Ctrl(void)
{
static uint32_t sta = 0;
if (0 == sta)
{
LED1_On();
}
else
{
LED1_Off();
}
if (1 == sta)
{
LED2_On();
}
else
{
LED2_Off();
}
/* 两个灯,最大不超过2 */
sta = (sta + 1) % 2;
}
/* 主函数运行 */
int main(void)
{
while(1)
{
LED_Ctrl();
os_delay(200);
}
}
上面的实现有个问题,就比如现在要加一个灯,那首先需要实现LEDx_On和LEDx_Off,另外需要把这两个添加到LED_Ctrl里。如下所示:
#include <stdint.h>
/* LED灯1亮 */
extern void LED1_On(void);
/* LED灯1灭 */
extern void LED1_Off(void);
/* LED灯2亮 */
extern void LED2_On(void);
/* LED灯2灭 */
extern void LED2_Off(void);
/* LED灯3亮 */
extern void LED3_On(void); //新增代码
/* LED灯3灭 */
extern void LED3_Off(void); //新增代码
void LED_Ctrl(void)
{
static uint32_t sta = 0;
if (0 == sta)
{
LED1_On();
}
else
{
LED1_Off();
}
if (1 == sta)
{
LED2_On();
}
else
{
LED2_Off();
}
/* 加了一个灯 */
if (2 == sta)
{
LED2_On();
}
else
{
LED2_Off();
}
/* 三个灯,最大不超过3 */
sta = (sta + 1) % 3;
}
int main(void)
{
while(1)
{
LED_Ctrl();
os_delay(200);
}
}
可以看到,加个灯,需要改三个地方,缺一不可,这对于软件维护是个大忌,很容易出问题。那应该怎么让修改点尽量的少呢?接下来就是重点了,怎么使用表驱动对上面的例子进行优化。
- 直接访问
这个很好理解,序号所指即数据所在。比如表格中,第一行为A数据,第二行为B数据,这种跟表格固有属性强相关的,或者跟顺序无关的,就可以使用此模式。对应到数组中,就是使用一维数组下标查找数据。像上面的例子,跑马灯轮流亮的,都是相邻的下一个灯,即灯的动作跟序号强相关,那就可以使用直接访问的方式。修改如下:
#include <stdint.h>
/* LED灯1亮 */
extern void LED1_On(void);
/* LED灯1灭 */
extern void LED1_Off(void);
/* LED灯2亮 */
extern void LED2_On(void);
/* LED灯2灭 */
extern void LED2_Off(void);
/* 把同一个灯的操作封装起来 */
struct tagLEDFuncCB
{
void (*LedOn)(void);
void (*LedOff)(void);
};
/* 定义需要操作到的灯的表 */
const static struct tagLEDFuncCB LedOpTable[] =
{
{
LED1_On, LED1_Off},
{
LED2_On, LED2_Off},
};
void LED_Ctrl(void)
{
static uint32_t sta = 0;
uint8_t i;
for (i = 0; i < sizeof(LedOpTable)/sizeof(LedOpTable[0]); i++)
{
(sta == i)?(LedOpTable[i].LED_On()):(LedOpTable[i].LED_Off());
}
/* 跑下个灯 */
sta = (sta + 1) % (sizeof(LedOpTable) / sizeof(LedOpTable[0]));
}
int main(void)
{
while(1)
{
LED_Ctrl();
os_delay(200);
}
}
这种写法,如果要加一个灯,那只需要在表里添加相应的操作即可,保证了只修改一个地方。
#include <stdint.h>
/* LED灯1亮 */
extern void LED1_On(void);
/* LED灯1灭 */
extern void LED1_Off(void);
/* LED灯2亮 */
extern void LED2_On(void);
/* LED灯2灭 */
extern void LED2_Off(void);
/* LED灯3亮 */
extern void LED3_On(void);
/* LED灯3灭 */
extern void LED3_Off(void);
/* 把同一个灯的操作封装起来 */
struct tagLEDFuncCB
{
void (*LedOn)(void);
void (*LedOff)(void);
};
const static struct tagLEDFuncCB LedOpTable[] =
{
{
LED1_On, LED1_Off},
{
LED2_On, LED2_Off},
{
LED3_On, LED3_Off}, //只需要添加绑定一个灯的操作
};
void LED_Ctrl(void)
{
static uint32_t sta = 0;
uint8_t i = 0;
for (i = 0; i < sizeof(LedOpTable)/sizeof(LedOpTable[0]); i++)
{
(sta == i)?(LedOpTable[i].LED_On()):(LedOpTable[i].LED_Off());
}
/* 跑下个灯 */
sta = (sta + 1) % (sizeof(LedOpTable) / sizeof(LedOpTable[0]));
}
int main(void)
{
while(1)
{
LED_Ctrl();
os_delay(200);
}
}
- 索引访问
现实可能不会有像直接访问那么理想的数据形式,有可能现在有10个数据,但并不是从第一行开始算起,比如第10行对应A数据,第20行对应B数据,这种就需要添加一个索引号来进行间接访问。如现在要获取A数据,那需要先找到10这个行号,再找到对应的A数据。对应到数组中,就是使用二维数据,一维作为索引,一维作为数据。
还是以上面的跑马灯为例,如果现在硬件工程师跟你说:哎呀,LED1和LED2的PCB线拉反了,PCB定稿了,你这边改一下应该很快吧。行吧,说改就改,这里可以有两种做法,一种是直接把表里面LED1和LED2的操作调换一下。还有一种就是增加索引信息,实现如下:
#include <stdint.h>
/* LED灯1亮 */
extern void LED1_On(void);
/* LED灯1灭 */
extern void LED1_Off(void);
/* LED灯2亮 */
extern void LED2_On(void);
/* LED灯2灭 */
extern void LED2_Off(void);
/* LED灯3亮 */
extern void LED3_On(void);
/* LED灯3灭 */
extern void LED3_Off(void);
/* 把同一个灯的操作封装起来 */
struct tagLEDFuncCB
{
uint8_t Index; /* 增加的索引 */
void (*LedOn)(void);
void (*LedOff)(void);
};
const static struct tagLEDFuncCB LedOpTable[] =
{
/* 小序号优先执行--别问我为什么是LED2比LED1先跑,问硬件的去 */
{
1, LED1_On, LED1_Off},
{
0, LED2_On, LED2_Off},
{
2, LED3_On, LED3_Off},
};
void LED_Ctrl(void)
{
static uint32_t sta = 0;
uint8_t i = 0;
for (i = 0; i < sizeof(LedOpTable) / sizeof(LedOpTable[0]); i++)
{
/* 跟直接访问的区别在于,是通过索引号来决定调用顺序,而不是表的顺序 */
(sta == LedOpTable[i].Index)?(LedOpTable[i].LED_On()):(LedOpTable[i].LED_Off());
}
/* 跑下个灯 */
sta = (sta + 1) % (sizeof(LedOpTable) / sizeof(LedOpTable[0]));
}
int main(void)
{
while(1)
{
LED_Ctrl();
os_delay(200);
}
}
- 阶梯访问
有时候单纯的依赖数据结构还无法实现的,就比如表格里,前10行是A数据,11~20行是B数据,此时如果直接使用行号对应数据,那这个表会变得很大。但我们从变化角度来看,实际变的,只有头尾的行号。那同样使用索引访问里的数组结构,一维作为索引,一维作为数据,只是在引用的时候加点变化,把索引值作为范围判断值使用。
再在上面的例子再加亿点点变化。这时候产品经理走过来说,小菜呀,这跑马灯一直以一个速度跑,一点都不好玩,能不能加点变化进去,就比如前面的灯跑快点,后面的灯跑慢点之类的。
那就再修一修,把前面索引的含义改一下,改成时间间隔的形式。实现如下:
#include <stdint.h>
/* LED灯1亮 */
extern void LED1_On(void);
/* LED灯1灭 */
extern void LED1_Off(void);
/* LED灯2亮 */
extern void LED2_On(void);
/* LED灯2灭 */
extern void LED2_Off(void);
/* LED灯3亮 */
extern void LED3_On(void);
/* LED灯3灭 */
extern void LED3_Off(void);
/* 把同一个灯的操作封装起来 */
struct tagLEDFuncCB
{
uint8_t Index; /* 增加的索引 */
uint32_t Time; /* 增加时间间隔 */
void (*LedOn)(void);
void (*LedOff)(void);
};
const static struct tagLEDFuncCB LedOpTable[] =
{
/* 索引号小的优先执行--别问我为什么是LED2比LED1先跑,问硬件的去 */
/* 时间间隔短的跑得越快--为什么前后跑的速度不一样?产品经理说的,我也不造啊~~~ */
{
1, 10, LED1_On, LED1_Off},
{
0, 1, LED2_On, LED2_Off},
{
2, 100, LED3_On, LED3_Off},
};
void LED_Ctrl(void)
{
static uint32_t sta = 0;
uint8_t i = 0;
/* 增加个时间计数 */
static uint32_t cnt = 0;
/* 时间到了就切换灯 */
if (cnt == LedOpTable[LedOpTable[sta].Index].Time)
{
for (i = 0; i < sizeof(LedOpTable)/sizeof(LedOpTable[0]); i++)
{
/* 跟直接访问的区别在于,是通过索引号来决定调用顺序,而不是表的顺序 */
(sta == LedOpTable[i].Index)?(LedOpTable[i].LED_On()):(LedOpTable[i].LED_Off());
}
}
else
{
//
}
/* 跑下个灯 */
sta++;
cnt++;
if (sta >= (sizeof(LedOpTable) / sizeof(LedOpTable[0])))
{
sta = 0;
cnt = 0;
}
}
int main(void)
{
while(1)
{
LED_Ctrl();
os_delay(200);
}
}
看到了吧,无论下面的屎山怎么堆积,需要变化的部分都只在表中,即使后面有变更,也不会改到原有代码里,这样即保证了代码的稳定性,也可以保持代码的优雅(仅限表的实现)。
适用范围
大量用到"if/case"的地方,或者大量重复性操作,基本都可以使用表驱动,把变化部分抽象出来放到表中。
优势
- 在某些应用场合下,可以节省代码空间。
- 数据可变部分结构化,方便使用文件或其他形式,对数据可变部分进行替换。
- 代码简洁优雅。
劣势
执行效率比起直接使用switch-case要慢一些。