关注点分离在软件工程领域是最重要的原则,是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。
我们习惯从功能上划分模块,保持“功能独立”是模块化设计的基本原则。因为,“功能独立”的模块可以降低开发、测试、维护等阶段的代价。
评价模块化设计优劣的三个特征因素:“信息隐藏”、“内聚与耦合”和“封闭--开放性”
模块的信息隐藏可以通过接口设计来实现。一个模块仅提供有限个接口,执行模块的功能或与模块进行信息交流必须且只需通过公有接口来实现。
内聚是一个模块内部各成分之间相关程度的度量。耦合是模块之间依赖程度的度量。理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性。模块设计追求高类聚,低耦合。
如果一个模块可以作为一个独立体被其他程序引用,则称模块具有封闭性。如果一个模块可以被扩充,则称模块具有开放性。封闭---开放性实际上对应软件质量因素中的可复用性和可扩充性。模块设计推崇的是对修改封闭,对扩展开放。
要完成一个项目不是一蹴而就的,往往需要多个版本的迭代。孟宁老师的菜单小程序是从最简单的helloword一步一步修改完善而成的。
在lab3.1差不多已经构建了菜单小程序的雏形,因为要研究这个程序所体现出来的模块化思想,就姑且称这个版本为初始版本吧。
{
char* cmd;
char* desc;
int (*handler)();
struct DataNode *next;
} tDataNode;
static tDataNode head[] =
{
{"help", "this is help cmd!", Help,&head[1]},
{"version", "menu program v1.0", NULL, &head[2]},
{"quit", "Quit from menu", Quit, NULL}
};
int main()
{
/* cmd line begins */
while(1)
{
char cmd[CMD_MAX_LEN];
printf("Input a cmd number > ");
scanf("%s", cmd);
tDataNode *p = head;
while(p != NULL)
{
if(strcmp(p->cmd, cmd) == 0)
{
printf("%s - %s\n", p->cmd, p->desc);
if(p->handler != NULL)
{
p->handler();
}
break;
}
p = p->next;
}
if(p == NULL)
{
printf("This is a wrong cmd!\n ");
}
}
}
int Help()
{
printf("Menu List:\n");
tDataNode *p = head;
while(p != NULL)
{
printf("%s - %s\n", p->cmd, p->desc);
p = p->next;
}
return 0;
}
int Quit()
{
exit(0);
}
可以看到,在这个初始版本中,对数据结构的操作和菜单处理(业务)集中到了一起,这个时候并未体现任何模块化思想。
为了进行模块化设计,我们首先要做的就是将数据结构与业务进行关注点分离。
/* data struct and its operations */
typedef struct DataNode
{
char* cmd;
char* desc;
int (*handler)();
struct DataNode *next;
} tDataNode;
tDataNode* FindCmd(tDataNode * head, char * cmd)
{
if(head == NULL || cmd == NULL)
{
return NULL;
}
tDataNode *p = head;
while(p != NULL)
{
if(!strcmp(p->cmd, cmd))
{
return p;
}
p = p->next;
}
return NULL;
}
int ShowAllCmd(tDataNode * head)
{
printf("Menu List:\n");
tDataNode *p = head;
while(p != NULL)
{
printf("%s - %s\n", p->cmd, p->desc);
p = p->next;
}
return 0;
}
/* menu program */
static tDataNode head[] =
{
{"help", "this is help cmd!", Help,&head[1]},
{"version", "menu program v1.0", NULL, NULL}
};
int main()
{
/* cmd line begins */
while(1)
{
char cmd[CMD_MAX_LEN];
printf("Input a cmd number > ");
scanf("%s", cmd);
tDataNode *p = FindCmd(head, cmd);
if( p == NULL)
{
printf("This is a wrong cmd!\n ");
continue;
}
printf("%s - %s\n", p->cmd, p->desc);
if(p->handler != NULL)
{
p->handler();
}
}
}
int Help()
{
ShowAllCmd(head);
return 0;
}
这个版本把数据结构和它的操作与菜单业务处理进行分离处理,有点模块化的思想了,但是两个模块还是在同一个源代码之中。
接下来就是将数据结构及其操作和菜单业务的代码存在到不同的源代码文件中,这个时候两模块之间进行信息交流只能依靠接口。
在这个版本我认为不仅仅是将模块存在不同的源代码中,还体现了模块化信息隐藏的特点。
数据模块的接口放在了.h中,对应的实现代码放在了.c中。
这种办法可以有效地隐藏软件模块内部的实现细节,为外部调用接口的开发者提供更加简洁的接口信息,同时也减少外部调用接口的开发者有意或无意的破坏软件模块的内部数据。通过接口进行信息隐藏已经成为面向对象编程语言的标准做法。
这个时候模块化设计已经初步完成了,但是还不够好,模块之间的耦合度还很高,由于数据结构是用户定义的,如果某天需要对数据结构进行修改,势必影响业务模块。
通用的模块才有更多重用的机会,同时通用的模块也可以降低模块之间的耦合度。所以下一版本中选择将数据结构换为更加通用的linktable。
到了接口这里,我一开始还有些疑问,所谓面向接口编程和面向对象到底有何不同。
“面向对象”与“面向接口”并非两种不同的方法学,“面向接口”其实是“面向对象”的内在要求,是其一部分内涵的集中表述。我们对于理想软件的期待常被概括为“高内聚,低耦合”,这也是整个现代软件开发方法学所追求的目标。面向对象方法学作为现代软件开发方法学的代表,本身就蕴含着“高内聚,低耦合”的思想精髓,从这个意义上来说,“面向对象”这个表述更加侧重于“高内聚”,“面向接口”的表述则更加侧重于“低耦合”——不过是同一事物的不同侧面罢了。
紧接着为linktable设置更加通用的接口:
然后又利用callback函数参数使得Linktable的查询接口更加通用:
又为了避免使用全局变量,降低耦合度,给callback函数增加了一个参数args,相应地给SearchLinkTableNode接口增加了args参数。
至此,模块化设计差不多已经完成。
原文:https://www.cnblogs.com/oywr/p/13927849.html