代码中的软件工程-menu菜单程序剖析
前言
一个软件的生命周期有问题定义、可行性分析、总体描述、系统设计、编码、调试和测试、验收与运行、维护升级到废弃等阶段。我们评判一个人是否是一个好的开发者的指标就是看他在进行前序步骤的时候是否能够为后续步骤有所考虑。简单来说,就是在编码的时候能否考虑到后期维护的成本。学习软件工程,就是在学习一种思维,一种前人总结的习惯,进而帮助我们写出优良风格的代码。本文基于孟宁老师的课程资料,以menu菜单程序为例进行软件工程思想的分析。
参考资料:https://gitee.com/mengning997/se/blob/master/README.md#%E4%BB%A3%E7%A0%81%E4%B8%AD%E7%9A%84%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B
环境配置
1、下载安装c/c++环境,测试环境为Ubuntu
sudo apt-get install gcc
sudo apt-get install g++
sudo apt-get install gdb
2、下载vscode和c/c++插件
c/c++编译和运行插件
3、编写helloworld.cpp并使用F6启动c++编译器使用默认参数编译并运行
可以看到成功输出helloworld
分析menu程序
代码见:https://gitee.com/mengning997/se/blob/master/ppt/menu_code.zip
1、代码风格
/********************************************************************/ /* Copyright (C) SSE-USTC, 2012-2013 */ /* */ /* FILE NAME : linktabe.h */ /* PRINCIPAL AUTHOR : Mengning */ /* SUBSYSTEM NAME : LinkTable */ /* MODULE NAME : LinkTable */ /* LANGUAGE : C */ /* TARGET ENVIRONMENT : ANY */ /* DATE OF FIRST RELEASE : 2012/12/30 */ /* DESCRIPTION : interface of Link Table */ /********************************************************************/
程序块头部注释:示例程序中注释的头部和尾部均进行了对齐操作,如果觉得麻烦可以只对齐头部
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; }
代码块:实例程序中使用的大括号分布为函数签名的第二行以及最后一条语句的下一行进行对齐,此处还可以使用其他分布方式,但是要注意的是所有的函数应当遵循同一个标准
2、模块化设计
- 模块化设计指的是将程序执行流程设计变成一个个模块的组合,每个模块专注于一项功能。以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离。这样就能帮助更好地优化代码,减少bug产生,同时也利于扩展。
- 软件设计中的模块化程度是衡量软件设计好坏的一个重要指标,一般我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度。
- 耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。 一般在软件设计中我们追求松散耦合。
/* * LinkTable Node Type */ typedef struct LinkTableNode { struct LinkTableNode * pNext; }tLinkTableNode; /* * LinkTable Type */ typedef struct LinkTable { tLinkTableNode *pHead; tLinkTableNode *pTail; int SumOfNode; pthread_mutex_t mutex; }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); /* * get LinkTableHead */ tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable); /* * get next LinkTableNode */ tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
从版本3.3开始加入了linklist.h头文件,逐渐显露出模块化设计的思想。版本4中的头文件更加完整,如上图所示:其中用函数名标识了每个函数的用途,上方用注释的形式写出了具体函数能够完成的功能,以此形成一个个的模块,分别负责完成不同的部分。例如CreateLinkTable()用于创建一个链表、DeleteLinkTable()用于删除一个链表等。此外,头文件只提供接口,具体实现放在linklist.c文件中,这样层次更加清晰。
3、可重用接口
- 接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。
- 接口规格是软件系统的开发者正确使用一个软件模块需要知道的所有信息,那么这个软件模块的接口规格定义就必须清晰明确地说明正确使用本软件模块的信息。一般来说,接口规格包含五个基本要素:
---接口的目的
---接口使用前所需要满足的条件,一般称为前置条件或假定条件
---使用接口的双方遵守的协议规范
---接口使用之后的效果,一般称为后置条件
---接口所隐含的质量属性
/* * 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; }
如上图以查找链表节点的SearchLinkTableNode函数为例进行分析,此接口的目标是从链表中查找给定的链表节点;该接口的前置条件是链表必须存在使用该接口才有意义;使用该接口的双方遵守的协议规范是通过数据结构tLinkTableNode和tLinkTable定义的;使用该接口之后的效果是找到了链表的指定节点或者没找到返回null,用数据类型为tLinkTableNode的指针返回数据;该接口没有特别要求接口的质量属性。遵守了接口开发规范后任何外部进程或者函数在需要时就可以提供合适的参数来调用此函数并拿到返回值,以此完成可重用设计。
SearchLinkTableNode函数还用到了callback接口设计,将condition函数作为参数传入,函数调用者可以根据自身需要定制此函数作为自己程序的判断逻辑条件,而函数实现者并不知道真正在运行时被调用的是什么条件函数,这样一来大大提高了接口的通用性。此外,使用一个args指针传入函数,避免访问全局变量cmd。
int SearchConditon(tLinkTableNode * pLinkTableNode,void * arg) { char * cmd = (char*)arg; tDataNode * pNode = (tDataNode *)pLinkTableNode; if(!strcmp(pNode->cmd, cmd)) { return SUCCESS; } return FAILURE; }
上图为实际运行时所使用到的条件函数,其功能为找到命令所对应的指针。其中tDataNode * pNode = (tDataNode *)pLinkTableNode;这一语句将数据类型pLinkTableNode强制转换为tDataNode类型,因为是向上转换,所以肯定会成功。这里使用到了多态的思想,使得程序设计更加灵活。
4、线程安全
- 可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用局部变量,要么在使用全局变量时保护自己的数据。(PS:
可重入的函数不一定是线程安全的;不可重入的函数一定不是线程安全的。
) -
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
- 线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
struct LinkTable { tLinkTableNode *pHead; tLinkTableNode *pTail; int SumOfNode; pthread_mutex_t mutex; };
/* * Delete a LinkTableNode from LinkTable */ int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode) { if(pLinkTable == NULL || pNode == NULL) { return FAILURE; } pthread_mutex_lock(&(pLinkTable->mutex)); if(pLinkTable->pHead == pNode) { pLinkTable->pHead = pLinkTable->pHead->pNext; pLinkTable->SumOfNode -= 1 ; if(pLinkTable->SumOfNode == 0) { pLinkTable->pTail = NULL; } pthread_mutex_unlock(&(pLinkTable->mutex)); return SUCCESS; } tLinkTableNode * pTempNode = pLinkTable->pHead; while(pTempNode != NULL) { if(pTempNode->pNext == pNode) { pTempNode->pNext = pTempNode->pNext->pNext; pLinkTable->SumOfNode -= 1 ; if(pLinkTable->SumOfNode == 0) { pLinkTable->pTail = NULL; } pthread_mutex_unlock(&(pLinkTable->mutex)); return SUCCESS; } pTempNode = pTempNode->pNext; } pthread_mutex_unlock(&(pLinkTable->mutex)); return FAILURE; }
如上图所示,确保线程安全一种很有效的方法就是加锁,在本例中,对链表的修改操作在某些时刻需要切断链表,那么如果此时如果有读链表的进程的话就可能会出错,所以方法是使用信号量mutex,在链表删除前加锁不让其他进程访问,删除操作结束后解锁。
小结
以前在编写程序的时候都是按照从上到下的流程进行,很多功能都挤在一起。通过本次实践,我对好的编程风格有了一定的了解,比如模块化设计、可重用接口设计等,也了解了callback接口设计理念以及线程安全等较为高级的概念,相信对我以后的学习和工作都有很大的帮助。