C语言设计模式——命令模式
好处:让代码清晰明了,容易添加和删除,易维护。
哪些地方会用到命令模式?(列出几个常见的例子)
1、按键处理,每个按键按下得到一个索引(指的就是命令),一个按键对应一个处理函数。按键处理命令模式
2、协议解析(串口,网口,CAN,等等);以串口为例简单说明一下,比如有如下协议:http类型解析(html,jpg,jpeg...)
帧头 | 命令 | 数据长度 | 数据内容 | 校验 | 帧尾 |
1字节 | 1字节 | 2字节 | n字节 | 2字节 | 1字节 |
命令1:0x01 温度
命令2:0x02 湿度
命令3:0x03 光照强度
传统的实现方式如下:(伪代码)
static uint8_t parse(char *buffer, uint16_t length)
{
uint8_t head = buffer[0];
uint8_t cmd = buffer[1];
uint16_t len = (buffer[2] << 8) | buffer[3];
uint16_t crc = CRCCheck(buffer, length - 3);
uint8_t tail = buffer[length - 1];
if((head != xxx) && (tail != xxx) && (crc != ((buffer[length - 3]) << 8) | buffer[length - 2]))
{
return 0;
}
switch(cmd)
{
case 0x01:
int temperatue = *(int *)&buffer[4];
printf("temperatue = %d\n", temperatue);
break;
case 0x02:
int humidity = *(int *)&buffer[4];
printf("humidity = %d\n", humidity);
break;
case 0x03:
int illumination= *(int *)&buffer[4];
printf("illumination = %d\n", illumination);
break;
default:
printf("parse error\n");
break;
}
return 1;
}
通过这段伪代码可以看出代码结构的一些问题,如果要添加更多的命令,势必需要向switch case语句中加入更多的case语句。使得解析函数越来越臃肿。当然我们可以使用如下方式规避一些问题:(伪代码)
// 当心字节对齐的问题
typedef struct
{
uint8_t head;
uint8_t cmd;
uint16_t length;
uint8_t data[1];
} package_t;
static int parse_temperature(char *buffer)
{
int value = *(int *)buffer;
printf("temperature = %d\n", value);
}
static int parse_humidity(char *buffer)
{
int value = *(int *)buffer;
printf("humidity = %d\n", value);
}
static int parse_illumination(char *buffer)
{
int value = *(int *)buffer;
printf("illumination = %d\n", value);
}
static uint8_t parse(char *buffer, uint16_t length)
{
package_t *frame = (package_t *)buffer;
uint16_t crc = CRCCheck(buffer, length - 3);
uint8_t tail = buffer[length - 1];
if((frame->head != xxx) && (tail != xxx) && (crc != (buffer[length - 3]) << 8 | buffer[length - 2]))
{
return 0;
}
switch(frame->cmd)
{
case 0x01:
parse_temperature(frame->data);
break;
case 0x02:
parse_humidity(frame->data);
break;
case 0x03:
parse_illumination(frame->data);
break;
default:
printf("parse error\n");
}
return 1;
}
相比于第一段代码,已经有了很大的改善,扩展性也得到了很大的提升。随着项目的进行,解析函数还是可能会越来越大。接下来就开始介绍命令模式。在命令模式里面,我们只需要维护一个命令列表就行了,而不需要关注解析函数本身。(伪代码)
// 当心字节对齐的问题
typedef struct
{
uint8_t head;
uint8_t cmd;
uint16_t length;
uint8_t data[1];
} package_t;
static int parse_temperature(char *buffer)
{
int value = *(int *)buffer;
printf("temperature = %d\n", value);
}
static int parse_humidity(char *buffer)
{
int value = *(int *)buffer;
printf("humidity = %d\n", value);
}
static int parse_illumination(char *buffer)
{
int value = *(int *)buffer;
printf("illumination = %d\n", value);
}
typedef struct
{
uint8_t cmd;
void (* handle)(char *buffer);
} package_entry_t;
static const package_entry_t package_items[] =
{
{0x01, parse_temperature},
{0x02, parse_humidity},
{0x03, parse_illumination},
{0xFF, NULL},
};
static uint8_t parse(char *buffer, uint16_t length)
{
package_t *frame = (package_t *)buffer;
uint16_t crc = CRCCheck(buffer, length - 3);
uint8_t tail = buffer[length - 1];
const package_entry_t *entry;
if((frame->head != xxx) && (tail != xxx) && (crc != (buffer[length - 3]) << 8 | buffer[length - 2]))
{
return 0;
}
for(entry = package_items; entry->handle != NULL; ++entry)
{
if(frame->cmd == entry->cmd)
{
entry->handle(frame->data);
break;
}
}
return 1;
}
我们可以看到,解析函数写好之后就不用动了,需要变化的只是一个表。这样写能让代码看起来干净整洁清晰,命令也可以使用宏定义或者枚举,看自己的喜好吧。一个命令对应一个处理函数,尽量使用此类方式去取代swicth case的方式,始终让代码保持整洁易扩展易维护的特性。
上面使用了命令模式作用于串口协议,同样的方式可以适用于各种协议,网口协议的话,格式都不用改。如果是can协议的话,将can的id用作命令,就ok了。其他的,类似。