【知识分享】C语言中的设计模式——表驱动模式

背景

    其实在《设计模式——可利用面向对象软件的基础》一书中,提及的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)
	{
		LED3_On();
	}
	else
	{
		LED3_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"的地方,或者大量重复性操作,基本都可以使用表驱动,把变化部分抽象出来放到表中。

优势

  1. 在某些应用场合下,可以节省代码空间。
  2. 数据可变部分结构化,方便使用文件或其他形式,对数据可变部分进行替换。
  3. 代码简洁优雅。

劣势

    执行效率比起直接使用switch-case要慢一些。

  • 6
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知识噬元兽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值