代码中的软件工程
孟老师课堂上通过一个生动形象的菜单小程序的演化过程来帮助我们学习软件工程的基本思想,这里做一次简单的课后复习。
准备部分
首先在VSCode下准备好C/C++的编译调试环境,编写最基础版本的菜单程序,仅包含两条命令,help输出一行字符串:"This is help cmd!",quit退出程序,显示结果:

准备部分包括1.1版本伪代码实现和2.1版本的最基础实现
3.1版本
在菜单3.1版本程序中, 引入了链表结构,将程序中业务部分与工具部分(数据结构)初步分离,用DataNode存储菜单中的一条命令,包括命令名、要输出的字符串、要进行的操作、和指向下一个命令结点的指针,将指令对应的操作也封装在函数中,进行抽象化和模块化设计。链表节点数据结构定义:
typedef struct DataNode { char* cmd; char* desc; int (*handler)(); struct DataNode *next; } tDataNode;
指令对应操作的初步模块化:
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); }
3.2版本
3.2版本中做了进一步的功能模块划分,将显示全部命令行和在链表中查找指定命令定义成函数ShowAllCmd和FindCmd,进一步的功能模块细分使得函数主体部分的实现愈发清晰,将具体的功能实现封装在函数中进一步提高程序的可读性和可维护性。
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; }
3.3版本
将数据结构和对应操作放到单独的源代码文件中,模块化的影响开始体现:单个代码文件内容变少,代码文件数量增多。将链表节点结构的定义和有关的操作放到linklist.c和linklist.h中,将业务功能和底层的数据结构以文件的形式分开。
4.1版本
引入链表数据结构的接口,将主程序中对数据结构的操作全部通过定义接口调用接口实现,在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); /* * get LinkTableHead */ tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable); /* * get next LinkTableNode */ tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
业务功能与数据结构分离,在linktable.h中定义只包含指向下一个节点的指针的链表节点,而在主文件中定义包含数据部分的数据节点:
typedef struct LinkTableNode { struct LinkTableNode * pNext; }tLinkTableNode; typedef struct DataNode { tLinkTableNode * pNext; char* cmd; char* desc; int (*handler)(); } tDataNode;
实际使用时初始化一个数据节点DataNode,再强制转化成链表节点插入到链表中,这样从数据结构层不再能看到链表中具体有什么数据,实现业务与工具的彻底分离。
int InitMenuData(tLinkTable ** ppLinktable) { *ppLinktable = CreateLinkTable(); tDataNode* pNode = (tDataNode*)malloc(sizeof(tDataNode)); pNode->cmd = "help"; pNode->desc = "Menu List:"; pNode->handler = Help; AddLinkTableNode(*ppLinktable,(tLinkTableNode *)pNode); pNode = (tDataNode*)malloc(sizeof(tDataNode)); pNode->cmd = "version"; pNode->desc = "Menu Program V1.0"; pNode->handler = NULL; AddLinkTableNode(*ppLinktable,(tLinkTableNode *)pNode); pNode = (tDataNode*)malloc(sizeof(tDataNode)); pNode->cmd = "quit"; pNode->desc = "Quit from Menu Program V1.0"; pNode->handler = Quit; AddLinkTableNode(*ppLinktable,(tLinkTableNode *)pNode); return 0; }
LinkTable结构中新增互斥量mutex,用以控制临界区访问,在涉及修改底层数据存放的操作前加pthread_mutex_lock()指令,在并发情况下可以阻止其他线程修改当前数据区,保证系统的线程安全。
5.1版本
给linktable增加callback方式的接口,将4.1版本中menu.c中FindCmd函数的遍历查找过程进一步细分成遍历过程和验证比较过程,遍历过程SearchLinkTableNode以接口方式定义在linktable.c中,而比较过程SearchCondition因为涉及数据节点的cmd成员,只能放在menu.c中,将整个SearchCondition函数以参数的方式传递给SearchLinkTableNode,底层数据结构层的SearchLinkTableNode函数调用业务层函数SearchCondition请求访问业务数据,保证业务层和工具层的分离。
//menu.c中
tDataNode* FindCmd(tLinkTable * head, char * cmd) { return (tDataNode*)SearchLinkTableNode(head,SearchCondition); } int SearchCondition(tLinkTableNode * pLinkTableNode) { tDataNode * pNode = (tDataNode *)pLinkTableNode; if(strcmp(pNode->cmd, cmd) == 0) { return SUCCESS; } return FAILURE; }
//linktable.c中 tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode)) { if(pLinkTable == NULL || Conditon == NULL) { return NULL; } tLinkTableNode * pNode = pLinkTable->pHead; while(pNode != pLinkTable->pTail) { if(Conditon(pNode) == SUCCESS) { return pNode; } pNode = pNode->pNext; } return NULL; }
5.2版本
5.1版本中数据结构层SearchLinkTableNode函数调用SearchCondition比较cmd时,用的是全局变量cmd[],与业务层模块属于公共耦合即共享变量名,因此在SearchCondition和SearchLinkTableNode函数中分别多传一个变量args,将cmd以函数参数形式传进去,将公告耦合降低到数据耦合,同时也使得该接口更加通用,可重用性更好。
int SearchCondition(tLinkTableNode * pLinkTableNode, void * args) { char * cmd = (char*) args; tDataNode * pNode = (tDataNode *)pLinkTableNode; if(strcmp(pNode->cmd, cmd) == 0) { return SUCCESS; } return FAILURE; } 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; }
同时5.1版本中链表LinkTable的结构定义放在linktable.h中,但实际menu.c中并没有用到,5.2版本将该部分内容转移到linktable.c中,有效隐藏软件模块内部的实现细节,为外部调用接口的开发者提供更简洁的接口信息,避免开发者破坏内部数据。
浙公网安备 33010602011771号