代码中的软件工程
1.引言
本篇文章主要基于孟宁老师上课的内容以及实验代码完成。是软件工程系列的第二篇文章。
本篇首先对老师给的源代码进行阅读分析,并且结合代码分析其中的软件工程方法、规范或软件工程思想。
2.win10下的环境配置
该部分主要关于vscode中c语言单文件的编译和调试。
step1 在vscode中安装C/C++扩展

step2 安装编译器调试器
win10下比较主流的跨平台编译器调试器就是mingw,而其他博客的mingw安装方法都过于复杂,且速度较慢,这边提出一种新的方法。
首先下载一个带mingw的codeblocks,推荐红线版本

然后进行安装,记住mingw安装位置,安装按检查gcc和gdb,类似下图的结果就ok

step3 vscode配置
首先,在新文件夹中cmd下输入 code . 表示用vscode打开该文件夹。可见文件夹中空空如也。

其次,创建一个新的cpp文件。

再次,开始配置编译环境。点击terminal->congfigue default build task,原则如图所示的build选项,可以看到左边出现了task.json。

最后,开始配置调试环境。依次点击三个圆点所示按钮。再点击g++.exe


可以看见,自动生成了launch.json,至此,编译和调试都配置完成了。

在调试时,顶部六个按钮分别代表
- 继续执行到下一个断点处
- 执行下一条语句,遇到函数直接执行完不会跳转进函数
- 执行下一条语句,遇到函数会跳转进函数继续单步执行
- 跳出当前所在的函数,如果是主函数会结束程序
- 重新启动调试
- 结束调试

3.项目代码分析
最终项目编译结果如下图,下面是对于代码的分析。

3.1 模块化设计
模块化(Modularity)是在软件系统设计时保持系统内部各部分相对独立,以便每一个部分都可以被独立地进行设计和开发。模块化软件设计的方法如果应用的比较好,最终每一个软件模块都将只有一个单一的功能模块,并相对独立于其他软件模块,使得每一个软件模块都容易理解容易开发。
说到模块化,就不得不提耦合和内聚。耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。一般在软件设计中我们追求松散耦合。内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性(Feather)。我们在模块化的程序中应该追求高耦合、低内聚,并且要遵守KISS原则(Keep It Simple & Stupid)。KISS原则指出:简单是软件设计的目标,简单的代码占用时间少,漏洞少,并且易于修改。
接下来结合代码,看看案例中的“模块化”体现在哪。
menu项目完成了一个类似于命令行终端的程序,我们可以通过命令行来输入命令,得到结果。test.c、test_exec.c、test_fork.c与test_reply.c为menu的四个版本,很显然,基础的存储结构和一些util可以作为一个模块进行复用。每一个版本的menu,都可以调用linklist的接口,使用链表的数据结构,从而使逻辑更清晰、代码量减少。在模块间低耦合,模块内高内聚,这是一个很好的案例。
这段代码在menu.c 中,对linklist.c中实现的链表结构进行了调用。
1 /* add cmd to menu */ 2 int MenuConfig(char * cmd, char * desc, int (*handler)()) 3 { 4 tDataNode* pNode = NULL; 5 if ( head == NULL) 6 { 7 head = CreateLinkTable(); 8 pNode = (tDataNode*)malloc(sizeof(tDataNode)); 9 pNode->cmd = "help"; 10 pNode->desc = "Menu List"; 11 pNode->handler = Help; 12 AddLinkTableNode(head,(tLinkTableNode *)pNode); 13 } 14 pNode = (tDataNode*)malloc(sizeof(tDataNode)); 15 pNode->cmd = cmd; 16 pNode->desc = desc; 17 pNode->handler = handler; 18 AddLinkTableNode(head,(tLinkTableNode *)pNode); 19 return 0; 20 }
3.2 可重用接口
重用分为消费者重用和生产者重用,消费者重用是指软件开发者在项目中重用已有的一些软件模块代码,以加快项目工作进度。生产者重用需要重点考虑设计通用的模块,通用的接口等因素。接口,就像“天王盖地虎”这样的暗号,是重用中最重要的部分。
良好的接口还包含了五大要素,分别是:1)接口的目的;2)接口使用所需满足的前置条件或假定条件;3)使用接口的双方遵守的协议规范;4)接口使用之后的效果,一般称为后置条件;5)接口所隐含的质量属性。接下来将对两种接口形式——Call-in和Callback方式结合代码来进行分析。
同样是和上一小节相同的案例,menu.c和linklist.c中的复用、调用问题。比如一个增加节点的接口
1 /* 2 * Add a LinkTableNode to LinkTable 3 */ 4 int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
按照五大要素进行分析:
1)接口目的是在链表中增加新节点,通过函数名就可以看出
2)该接口的前置条件是链表必须存在使用该接口才有意义,也就是链表pLinkTable != NULL
3)使用该接口的双方遵守的协议规范是通过数据结构tLinkTableNode和tLinkTable定义的
4)接口的效果是返回success状态
5) 该接口没有特别要求接口的质量属性
上面声明的函数其实是Call-in函数,与此相对,Callback函数就是回调函数。通俗理解,就是触发什么事件之后,通过指针来调用回调函数。
下面就是一个callback类型的接口,当出发一定条件,就会调用Condition函数。此外,利用c的#define语法来定义返回类型success和failure也是很好的用法。
1 /* 2 * Search a LinkTableNode from LinkTable 3 * int Conditon(tLinkTableNode * pNode,void * args); 4 */ 5 tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args);
3.3 线程安全
说到线程安全,一定需要提到可重入函数。可重入函数指可以被多个任务并发使用,而不必担心数据发生错误。与之相反的不可重入函数则要求不能超过一个任务来共享,除非能保证函数的互斥。而线程安全又与可重入密切相关,为了保证线程安全,我们在必要时需要通过“上锁”来保护临界资源。
线程安全主要还是体现在修改和读取的时序问题上,一般来说通过锁的机制来保证。线程安全问题大多是由全局变量及静态变量引起的,局部变量逃逸也可能导致线程安全问题。
为了测试线程安全的具体实现问题,在test.c的基础上还增加了另外三个c文件。
1)test_fork.c提供了fork操作,可以在父进程中起一个子进程;
2)test_exec.c在test_fork.c的基础上提供了exec操作,在子进程中执行hello;
3)test_reply.c中通过起不同的子进程模拟客户端与服务器进行TCP通信。
比如说如下的代码:
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 }
这是一个删除表节点的函数,涉及到修改,当然有可能会有线程安全的问题。 pthread_mutex_lock 这个函数其实就是对于资源加锁,当资源没有解锁使,别的进程和线程就不能访问该段临界区。这其实和操作系统中的互斥问题相同,lock其实就是信号量。11行枷锁,然后进入if分支,20、34、39行根据业务逻辑对锁进行释放,锁持有时间太长会导致程序运行速度变慢。
4. 参考
[1] https://gitee.com/mengning997/se/blob/master/README.md#代码中的软件工程

浙公网安备 33010602011771号