代码中的软件工程

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,至此,编译和调试都配置完成了。

    

    在调试时,顶部六个按钮分别代表

  1. 继续执行到下一个断点处
  2. 执行下一条语句,遇到函数直接执行完不会跳转进函数
  3. 执行下一条语句,遇到函数会跳转进函数继续单步执行
  4. 跳出当前所在的函数,如果是主函数会结束程序
  5. 重新启动调试
  6. 结束调试

   

  

 

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#代码中的软件工程

 

posted @ 2020-11-09 15:22  没有蛀牙才是永远的神  阅读(169)  评论(0)    收藏  举报