代码中的软件工程
最近上高级软件工程课,孟老师给我们展示了如何搭建并分析menu项目,以体现其中的软件工程思想。从最简单的hello world小程序,根据软件工程的一般性原理,逐步的添加完善,直到最终实现了一个比较通用的命令行menu程序,经过了这段时间的学习,使我受益匪浅,现将自己的一点体会和收获总结如下。
1 vs code编译及环境配置
为了能够进行menu程序的开发,需要在vscode中配置C/C++环境。首先,进入官网下载MinGW-w64,下载install安装程序,然后配置环境变量,进入cmd输入gcc -v进行确认,

安装完成之后,进入vsCode下载C/C++扩展插件。然后配置好c_cpp_properties.json、launch.json和task.json,至此,环境搭建完成,可以运行menu小程序了。
2 menu代码的软件工程原理分析
2.1 模块化
模块化是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离,把大问题分解成若干小问题,再逐个解决,本质上是分治的思想。一般我们使用耦合度与内聚度来衡量软件模块化的程度。
耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。一般在软件设计中我们追求松散耦合。
内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性(Feather)。
我们在模块化的程序中应该追求低耦合、高内聚,并且要遵守KISS原则(Keep It Simple & Stupid)。KISS原则指出:简单是软件设计的目标,简单的代码占用时间少,漏洞少,并且易于修改。
在menu小程序中,我们能够看到,实现链表数据结构的linktable.h和linktable.c,以及具体的业务逻辑的menu.c和menu.h,加上最后的测试程序test.c,它们之间是分离开来的。它们都有相对单一的功能目标,而且也相对的独立于另外的模块。

linktable.h中包含了对链表节点的数据结构以及对相关操作的声明,使用时只需包含该头文件即可使用所声明的操作。
#define SUCCESS 0 #define FAILURE (-1) /* * LinkTable Node Type */ typedef struct LinkTableNode { struct LinkTableNode * pNext; }tLinkTableNode; /* * LinkTable Type */ typedef struct LinkTable tLinkTable; /* * Create a LinkTable */ tLinkTable * CreateLinkTable(); /* * Delete a LinkTable */ int DeleteLinkTable(tLinkTable *pLinkTable); /* * Add a LinkTableNode to LinkTable */ int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode); /* * Delete a LinkTableNode from LinkTable */ int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode); /* * Search a LinkTableNode from LinkTable * int Conditon(tLinkTableNode * pNode,void * args); */ tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args); /* * get LinkTableHead */ tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable); /* * get next LinkTableNode */ tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
在linktable.c文件中实现头文件中的声明,下列代码是部分操作的实现,同时包含底层的链表数据结构。
struct LinkTable { tLinkTableNode *pHead; tLinkTableNode *pTail; int SumOfNode; pthread_mutex_t mutex; };
/* * Create a LinkTable */ tLinkTable * CreateLinkTable() { tLinkTable * pLinkTable = (tLinkTable *)malloc(sizeof(tLinkTable)); if(pLinkTable == NULL) { return NULL; } pLinkTable->pHead = NULL; pLinkTable->pTail = NULL; pLinkTable->SumOfNode = 0; pthread_mutex_init(&(pLinkTable->mutex), NULL); return pLinkTable; }
menu头文件和c文件也是类似的思想,头文件声明了一些操作,然后c文件提供具体实现。
//menu.h /* add cmd to menu */ int MenuConfig(char * cmd, char * desc, int (*handler)()); /* Menu Engine Execute */ int ExecuteMenu();
在menu.c文件中实现了一些头文件中并未声明的其他的函数和数据结构,DataNode结构用于保存各个命令的信息,它使用了在linktable模块中定以的链表节点。
//menu.c中部分细节展示 typedef struct DataNode { tLinkTableNode * pNext; char* cmd; char* desc; int (*handler)(int argc, char *argv[]); } tDataNode; int SearchConditon(tLinkTableNode * pLinkTableNode,void * arg) { char * cmd = (char*)arg; tDataNode * pNode = (tDataNode *)pLinkTableNode; if(strcmp(pNode->cmd, cmd) == 0) { return SUCCESS; } return FAILURE; } /* find a cmd in the linklist and return the datanode pointer */ tDataNode* FindCmd(tLinkTable * head, char * cmd) { tDataNode * pNode = (tDataNode*)GetLinkTableHead(head); while(pNode != NULL) { if(!strcmp(pNode->cmd, cmd)) { return pNode; } pNode = (tDataNode*)GetNextLinkTableNode(head,(tLinkTableNode *)pNode); } return NULL; } /* add cmd to menu */ int MenuConfig(char * cmd, char * desc, int (*handler)(int argc, char*argv[])) { tDataNode* pNode = NULL; if ( head == NULL) { head = CreateLinkTable(); pNode = (tDataNode*)malloc(sizeof(tDataNode)); pNode->cmd = "help"; pNode->desc = "Menu List:"; pNode->handler = Help; AddLinkTableNode(head,(tLinkTableNode *)pNode); } pNode = (tDataNode*)malloc(sizeof(tDataNode)); pNode->cmd = cmd; pNode->desc = desc; pNode->handler = handler; AddLinkTableNode(head,(tLinkTableNode *)pNode); return 0; }
2.2 可重用接口
可重用这个概念包括消费者重用和生产者重用,消费者重用是指软件开发者在项目中重用已有的一些软件模块代码,以加快项目工作进度。软件开发者在重用已有的软件模块代码时一般会重点考虑四个关键因素:
- 该软件模块是否有完善的文档说明
- 该软件模块是否有完整的测试及修订记录
- 采用该软件模块代码是否比从头构建一个需要更少的工作量,包括构建软件模块和集成软件模块等相关的工作
- 该软件模块是否能满足项目所要求的功能
消费者重用的关键因素同时也是生产者重用的关键因素。接下来介绍一下接口的概念:接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。这在menu小程序中的一个最主要的体现就是linktable接口,在linktable.h文件中,声明了数据结构以及操作该结构的一组函数。下面举个例子说明可重用接口。
//linktable.h中的链表节点结构 typedef struct LinkTableNode { struct LinkTableNode * pNext; }tLinkTableNode; //menu.c中的cmd结构 typedef struct DataNode { tLinkTableNode * pNext; char* cmd; char* desc; int (*handler)(int argc, char *argv[]); } tDataNode; //menu.c中用于打印所有cmd的函数 /* show all cmd in listlist */ int ShowAllCmd(tLinkTable * head) { tDataNode * pNode = (tDataNode*)GetLinkTableHead(head); while(pNode != NULL) { printf("%s - %s\n", pNode->cmd, pNode->desc); pNode = (tDataNode*)GetNextLinkTableNode(head,(tLinkTableNode *)pNode); } return 0; }
在linktable.h中,我们可以看到,链表节点的内容非常简单,只包含了指向下一个相同结构的指针。这样定义的结构具有很强的通用性,为什么这么说呢,因为任何需要使用到链表的结构都可以在定义结构的时候将其作为第一个元素进行声明,这样在使用用户自己的结构时,如果要用到通用的链表操作,一种做法是进行强制类型转换把它转换成指向链表节点的指针类型,再将其传给linktable.h中声明的函数。就像我们在这段代码的ShowAllCmd函数中看到的那样,GetNextLinkTableNode函数的功能是寻找当前链表节点的下一跳,属于linktable模块,这里pNode将自身传进函数时进行了强制类型转换,然后将函数返回值再转换回原类型,这就很好的体现了接口的可重用性。
另一个例子是给linktable增加Callback方式的接口,代码如下。
//linktable.c call-in /* * Search a LinkTableNode from LinkTable * int Conditon(tLinkTableNode * pNode); */ tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args) { if(pLinkTable == NULL || Conditon == NULL) { return NULL; } tLinkTableNode * pNode = pLinkTable->pHead; while(pNode != NULL) { if(Conditon(pNode,args) == SUCCESS) { return pNode; } pNode = pNode->pNext; } return NULL; }
//menu.c callback int SearchConditon(tLinkTableNode * pLinkTableNode,void * arg) { char * cmd = (char*)arg; tDataNode * pNode = (tDataNode *)pLinkTableNode; if(strcmp(pNode->cmd, cmd) == 0) { return SUCCESS; } return FAILURE; }
在call-in函数中有一个细节,就是传进去的参数类型是void*,这也很好的体现了代码的可重用性,降低了模块间的耦合程度,函数无需知道传进来的参数类型,这个类型由用户自己在callback函数中确定。
2.3 menu小程序中的线程安全
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。线程安全问题大多是由全局变量及静态变量引起的,局部变量逃逸也可能导致线程安全问题。
在menu小程序中,为了避免多个线程操作同一个链表导致线程安全问题,在一些链表操作的代码中加入了锁机制,举例如下。
1 //linktable.c 2 /* 3 * Delete a LinkTableNode from LinkTable 4 */ 5 int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode) 6 { 7 if(pLinkTable == NULL || pNode == NULL) 8 { 9 return FAILURE; 10 } 11 pthread_mutex_lock(&(pLinkTable->mutex)); 12 if(pLinkTable->pHead == pNode) 13 { 14 pLinkTable->pHead = pLinkTable->pHead->pNext; 15 pLinkTable->SumOfNode -= 1 ; 16 if(pLinkTable->SumOfNode == 0) 17 { 18 pLinkTable->pTail = NULL; 19 } 20 pthread_mutex_unlock(&(pLinkTable->mutex)); 21 return SUCCESS; 22 } 23 tLinkTableNode * pTempNode = pLinkTable->pHead; 24 while(pTempNode != NULL) 25 { 26 if(pTempNode->pNext == pNode) 27 { 28 pTempNode->pNext = pTempNode->pNext->pNext; 29 pLinkTable->SumOfNode -= 1 ; 30 if(pLinkTable->SumOfNode == 0) 31 { 32 pLinkTable->pTail = NULL; 33 } 34 pthread_mutex_unlock(&(pLinkTable->mutex)); 35 return SUCCESS; 36 } 37 pTempNode = pTempNode->pNext; 38 } 39 pthread_mutex_unlock(&(pLinkTable->mutex)); 40 return FAILURE; 41 }
该函数功能是删除一个链表节点,我们可以看到,代码第11行给链表结构进行加锁,第20、34和39行出现了解锁紧接着函数return语句。这样在进行修改链表状态的过程时保证没有其他线程对链表读写,这样就实现了线程安全。
参考资料
浙公网安备 33010602011771号