软件项目各有不同,开发语言多种多样,但软件开发这种行为过程,有其规律性,很多前辈从各个角度对软件开发这种行为做了总结。我们这里来介绍GOF从工程实现的角度总结的23种设计模式(最近实践),这将会是一个系列。
软件开发是一种智力活动,沟通交流时多有障碍,从设计模式的角度来沟通功能的实现,也能大大提高沟通效率。
在我个人的软件开发经历中,感受最深的是命令模式和观察者模式。我们先从命令模式开始。
先来看一个通讯协议相关的例子,数据帧定义如下:
帧长度 | 传感器类型 | 数据 |
---|---|---|
1字节 | 1字节 | n字节 |
传感器数据格式定义如下
传感器 | 类型号 | 数据格式 |
---|---|---|
温度传感器 | 1 | 整型数据(4字节) |
湿度传感器 | 2 | 整型数据(4字节) |
解析实现可如下实现:
static int parse(const char *data,size_t n)
{
int len = data[0];
int type = data[2];
int value;
switch(type){
case 1:
value = *(int *)&data[3];
printf("temperatue = %d\n",value);
case 2:
value = *(int *)&data[3];
printf("humidity = %d\n",value);
default:
printf("parse error\n");
}
}
相信你已经嗅到了这段代码的坏味道,如果我们系统中要添加更多的传感器类型,势必需要向switch case
语句中增加更多的case
语句。使得parse
函数越来越臃肿。
当然,我们可以采用,如下方式规避一些问题
static int parse_temperature(const char *data,size_t len)
{
assert(len == 4);
int value = *(int *)data;
printf("temperature = %d\n",value);
}
static int parse_humidity(const char *data,size_t len)
{
assert(len==4);
int value = *(int *)data;
printf("humidity = %d\n",value);
}
static void parse(const char *data,size_t len)
{
int data_len = data[0];
int data_type = data[1];
switch(type){
case 1:
parse_temperature(data+2,data_len-2);
break;
case 2:
parse_humidity(data+2,data_len-2);
break;
default:
break;
}
}
上面的代码相比与第一次实现有所改善,但是我们采用命令模式实现后,会看到,无论是可读性,可扩展性,都会得到相当成都的提高。
typedef int (*parse_func)(const char *data,size_t len);
struct parse_handler{
int type;
parse_func func;
}
static int parse_temperature(const char *data,size_t len)
{
assert(len == 4);
int value = *(int *)data;
printf("temperature = %d\n",value);
}
static int parse_humidity(const char *data,size_t len)
{
assert(len==4);
int value = *(int *)data;
printf("humidity = %d\n",value);
}
static struct parse_handler handlers = {
{1,parse_tmepeature},
{2,parse_humidity},
};
#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))
static parse_func parse_func_find(int type)
{
int i;
for(i=0;i<ARRAY_SIZE(handlers);i++){
if(handlers[i].type == type)
return handlers[i].func;
}
return NULL;
}
static int parse(const char *data,size_t len)
{
int data_len = data[0];
int data_type = data[1];
parse_func func = parse_func_find(data_type);
if(func != NULL)
func(data+2,data_len-2);
}
如上,需要增加或者修改传感器的时候,我们只需要关注handlers
这个数组。
static struct parse_handler handlers = {
{1,parse_tmepeature},
{2,parse_humidity},
};
这样写的优势
- 传感器类型和解析实现直接写在了同一行,非常直观,也没有必要将传感器类型数据定义为宏。
- 添加传感器的时候,只要在结构体数组中添加一行即可,如果是第二种实现,则需要在
parse
函数中,添加一个case
语句,若是遗忘了break
很容易导致错误。
这种实现方式,基本是此类问题的最优解。被称为命令模式。一个type被称为一个命令,解析函数对应于命令的实现。
很多命令行工具都是采用的此类实现方式。伪代码如下:
char buffer[1024];
while(true){
char *p = fgets(buffer,sizeof(buffer),stdin);
execute(p) //执行某个命令,先find再执行。
}