前言
对于目前公司的命令/报文解析方式不爽有段时间了,因为一个函数竟然写了百行左右,那些switch…case 的case 直接用数字表示,而且那个数字真是毫无顺序可言(至少我是找不到),每次找命令都是从头看到尾,一遍没看到,再看一遍,还是没有,才能确认确实没有相应的指令。对于修改和维护来说,我一直有种自己在“添乱”的感觉。但是更不爽的是我没能力解决这个问题,直到看到了那篇文章:【命令解析器 :C语言对象化设计实例】
如果没有面向对象编程经验的话那篇文章可以先跳过,看看我的描述能不能解决实际问题,如果不能的话,起码会产生种意识:这个问题很小,看一篇文章就能解决,但博主语言组织能力太差,害我不得不多看一篇文章。
自定义协议的命令解析器: 是在接收到自定义协议的报文的情况下用于对命令进行解析。本质上就是发生相应的事件,执行相应功能。
Switch…case类型命令解析的弊端
那篇文章提出了switch…case 那种方式更客观清晰的弊端描述,像“添乱”这个词太主观了,大概只有我自己才知道这个具体意味着什么。
-
大量的case分支对程序的复杂度时明显增加的,非常不便于查找,排错和维护。
-
命令增加引起跨模块修改。只是增加了GPIO相关功能,命令处理逻辑没变(依然只是判断字符串相等),为什么却要改动cmd.c(命令解析源文件)的命令处理逻辑。
-
大量的外部函数,模块间高度耦合。在cmd.c (命令解析源文件)中直接通过函数名调用,两个文件(命令解析文件 和 被调用函数所在文件)像缠绵的情侣般高度耦合,这种紧密的联系破坏了C程序设计的一个基本原则—模块的独立性。采用 了模块化编程,然而每个模块却不能独立使用,这是会破坏模块化的,若不加维护和避免,可能会拖垮优良的架构。
程序的改进目标
- 命令的处理函数要去耦合,高内聚低耦合的的代码是易于复用和维护的
- 增加或减少命令不影响cmd.c(命令解析源文件)
对命令解析器的分析
【命令】本身可以封装为 包含 【命令名】和【对应操作】两个成员的结构体
-
【命令名】:属性,可以用字符数组存储
-
【对应操作】:行为/函数,由于C语言结构体不支持函数,可用函数指针存储。
【命令解析器】的处理过程:命令注册,查找匹配 和 执行指令
通过调用【命令解析器】主动注册命令,而不是通过代码写死,从而**避免了跨模块修改**
【命令解析器】需要从一对命令中匹配一个,因此需要一种能存储命令集合的数据结构:线性表(列表内容;列表长度)。线性表的基本操作与命令处理过程结合(命令的注册和匹配 其实就是 插入和查找过程)
命令解析器的代码示例
【数据结构】:结构体数组(线性表)+ 存储命令个数的变量
typedef void (*handler)(void); // 命令操作函数指针类型
/* 命令结构体类型 */
typedef struct cmd
{
char cmd_name[MAX_CMD_NAME_LENGTH + 1]; // 命令名
handler cmd_operate; // 命令操作函数
} CMD;
// 文件名称:cmd.h
/* 命令列表结构体类型 */
typedef struct cmds
{
CMD cmds[MAX_CMDS_COUNT]; // 列表内容
int num; // 列表长度
} CMDS;
【注册命令函数】:接收一个命令类型数组,插入到命令解析器的命令列表中。
// 文件名称:cmd.c
void register_cmds(CMD reg_cmds[], int length)
{
int i;
if (length > MAX_CMDS_COUNT)
{
return;
}
for (i = 0; i < length; i++)
{
if (commands.num < MAX_CMDS_COUNT) // 命令列表未满
{
strcpy(commands.cmds[commands.num].cmd_name, reg_cmds[i].cmd_name);
commands.cmds[commands.num].cmd_operate = reg_cmds[i].cmd_operate;
commands.num++;
}
}
}