代码中的软件工程

前言

  本文是对软件工程的学习已及参考了孟老师提供的课程项目案例所写。对vscode 和gcc编译器的应用进行了学习,对软件工程中的模块化设计、可重用接口和线程安全进行了理解。

参考资料: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.vscode中c/c++插件安装

 

直接在vscode扩展插件中搜索,安装vscode中的c/c++模块。如图所示:

 

 

 2.安装gcc编译器

安装好c/c++插件之后,由于该插件并不包含c/c++的编译器,所以需要我们另外安装编译器。

 

 

 

安装过程中有几个选项需要说明:

      Version制定版本号,从4.9.1-8.x.0,按需选择,没有特殊要求就用最新版吧;

      Architecture跟操作系统有关,64位系统选择x86_64,32位系统选择i686;

      Threads设置线程标准可选posix或win32;

      Exception设置异常处理系统,x86_64可选为seh和sjlj,i686为dwarf和sjlj;

      Build revision构建版本号,选择最大即可

在命令行输入 gcc -v验证gcc编译器是否安装成功

 

 

 显示已经安装成功。

 

用vscode打开hello.c文件,进行运行和调试,生成了相应的文件夹

 

 

 在终端可以看到输出

 

 

 

 

模块化设计

1.基本原理

  模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离 (SoC, Separation of Concerns),是由软件工程领域的奠基性人物Edsger Wybe Dijkstra(1930~2002)在1974年提出,没错就是Dijkstra最短路径算法的作者。

  关注点的分离在软件工程领域是最重要的原则,我们习惯上称为模块化,翻译成我们中文的表述其实就是“分而治之”的方法。关注点的分离的思想背后的根源是由于人脑处理复杂问题时容易出错,把复杂问题分解成一个个简单问题,从而减少出错的情形。

 

  模块化软件设计的方法如果应用的比较好,最终每一个软件模块都将只有一个单一的功能目标,并相对独立于其他软件模块,使得每一个软件模块都容易理解容易开发。

  从而整个软件系统也更容易定位软件缺陷bug,因为每一个软件缺陷bug都局限在很少的一两个软件模块内。 而且整个系统的变更和维护也更容易,因为一个软件模块内的变更只影响很少的几个软件模块。

  因此,软件设计中的模块化程度便成为了软件设计有多好的一个重要指标,一般我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度。

2.耦合度(Coupling)

  耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。一般在软件设计中我们追求松散耦合

    

3.内聚度(Cohesion)

  内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性(Feather)

定义了链表结构:

这是模块化之前的代码(lab3.2),功能虽然齐全,但是所有的代码都堆在一个文件里面,阅读体验不佳:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 
 4 int Help();
 5 
 6 #define CMD_MAX_LEN 128
 7 #define DESC_LEN    1024
 8 #define CMD_NUM     10
 9 
10 /* data struct and its operations */
11 
12 typedef struct DataNode
13 {
14     char*   cmd;
15     char*   desc;
16     int     (*handler)();
17     struct  DataNode *next;
18 } tDataNode;
19 
20 tDataNode* FindCmd(tDataNode * head, char * cmd)
21 {
22     if(head == NULL || cmd == NULL)
23     {
24         return NULL;        
25     }
26     tDataNode *p = head;
27     while(p != NULL)
28     {
29         if(!strcmp(p->cmd, cmd))
30         {
31             return p;
32         }
33         p = p->next;
34     }
35     return NULL;
36 }
37 
38 int ShowAllCmd(tDataNode * head)
39 {
40     printf("Menu List:\n");
41     tDataNode *p = head;
42     while(p != NULL)
43     {
44         printf("%s - %s\n", p->cmd, p->desc);
45         p = p->next;
46     }
47     return 0; 
48 }
49 
50 /* menu program */
51 
52 static tDataNode head[] = 
53 {
54     {"help", "this is help cmd!", Help,&head[1]},
55     {"version", "menu program v1.0", NULL, NULL}
56 };
57 
58 int main()
59 {
60     /* cmd line begins */
61     while(1)
62     {
63         char cmd[CMD_MAX_LEN];
64         printf("Input a cmd number > ");
65         scanf("%s", cmd);
66         tDataNode *p = FindCmd(head, cmd);
67         if( p == NULL)
68         {
69             printf("This is a wrong cmd!\n ");
70             continue;
71         }
72         printf("%s - %s\n", p->cmd, p->desc); 
73         if(p->handler != NULL) 
74         { 
75             p->handler();
76         }
77    
78     }
79 }
80 
81 int Help()
82 {
83     ShowAllCmd(head);
84     return 0; 
85 }

而在lab3.3中,对上面代码进行了模块化,将对应功能的代码写在分别的文件中,再在主文件中进行引入,阅读体验大大提升:

 

 利用头文件进行引入。对应的结构图:

每一个代码模块都有 独立的功能,与其他模块互不干扰,一个大程序按照功能划分为若干小程序模块,每个小程序模块完成一个确定的功能,并在这些模块之间建立必要的联系,通过模块的互相协作完成整个功能。使得程序设计更加简单和直观,从而提高了程序的易读性和可维护性,而且还可以把程序中经常用到的一些计算或操作编写成通用函数,以供随时调用。

 

 

 可重用接口

  再进行了初步的模块化设计之后,我们要求一个软件模块只用做好一件事,也就是功能内聚,那就要让它做好链表数据结构和对链表的操作,不应该涉及菜单业务功能上的东西;同样我们希望这一个软件模块与其他软件模块之间松散耦合,就需要定义简洁、清晰、明确的接口。  

 

 

  这时进一步优化这个初步的模块化代码,就需要设计合适的接口,接口规格是软件系统的开发者正确使用一个软件模块需要知道的所有信息,那么这个软件模块的接口规格定义就必须清晰明确地说明正确使用本软件模块的信息。一般来说,接口规格包含五个基本要素:

  1. 接口的目的;
  2. 接口使用前所需要满足的条件,一般称为前置条件或假定条件;
  3. 使用接口的双方遵守的协议规范;
  4. 接口使用之后的效果,一般称为后置条件;
  5. 接口所隐含的质量属性。

例如:

tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);

该接口的目标是从链表中下一个链表节点,函数名GetNextLinkTableNode清晰明确地表明了接口的目标;

    该接口的前置条件是链表必须存在,同时当前结点不为空,使用该接口才有意义,也就是链表(pLinkTable != NULL)&&(pNode != NULL);

    使用该接口的双方遵守的协议规范是通过数据结构tLinkTableNode和tLinkTable定义的;

    使用该接口之后的效果是找到了链表的下一个节点,这里是通过tLinkTableNode类型的指针作为返回值来作为后置条件,C语言中也可以使用指针类型的参数作为后置条件;

    该接口没有特别要求接口的质量属性,如果搜索一个节点可能需要在可以接受的延时时间范围内完成搜索;

通用的linktalbe接口模块在linktalbe.h中:

 1 /*
 2  * Create a LinkTable
 3  */
 4 tLinkTable * CreateLinkTable();
 5 /*
 6  * Delete a LinkTable
 7  */
 8 int DeleteLinkTable(tLinkTable *pLinkTable);
 9 /*
10  * Add a LinkTableNode to LinkTable
11  */
12 int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
13 /*
14  * Delete a LinkTableNode from LinkTable
15  */
16 int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
17 /*
18  * Search a LinkTableNode from LinkTable
19  * int Conditon(tLinkTableNode * pNode,void * args);
20  */
21 tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args);
22 /*
23  * get LinkTableHead
24  */
25 tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);
26 /*
27  * get next LinkTableNode
28  */
29 tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);

  对应的实现代码位于linktalbe.c中,我们还通过将linktable.h 中不是在接口调用时必须内容转移到linktable.c中,
这样可以有效地隐藏软件模块内部的实现细节, 为外部调用接口的开发者提供更加简洁的接口信息, 同时也减少夕卜部调用接口的开发者有意或无意的破坏软件模块的内部数据。通过接口进行信息隐藏已经成为面向对象编程语言的标准做法,使用private和 public来声明属性和和方法,对于外部调用接口的开发者是否可见。

线程安全

什么时线程安全

  如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

  可重入函数可以由多于一个任务并发使用,而不必担心数据错误。相反地,不可重入函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用本地变量,要么在使用全局变量时保护自己的数据。

定义链表时引入 mutex:

1 struct LinkTable
2 {
3     tLinkTableNode *pHead;
4     tLinkTableNode *pTail;
5     int            SumOfNode;
6     pthread_mutex_t mutex; //引入mutex
7 
8 };

对链表进行操作时都要进行加锁/解锁:

 1 int DeleteLinkTable(tLinkTable *pLinkTable)
 2 {
 3     if(pLinkTable == NULL)
 4     {
 5         return FAILURE;
 6     }
 7     while(pLinkTable->pHead != NULL)
 8     {
 9         tLinkTableNode * p = pLinkTable->pHead;
10         pthread_mutex_lock(&(pLinkTable->mutex));
11         pLinkTable->pHead = pLinkTable->pHead->pNext;
12         pLinkTable->SumOfNode -= 1 ;
13         pthread_mutex_unlock(&(pLinkTable->mutex));
14         free(p);
15     }
16     pLinkTable->pHead = NULL;
17     pLinkTable->pTail = NULL;
18     pLinkTable->SumOfNode = 0;
19     pthread_mutex_destroy(&(pLinkTable->mutex));
20     free(pLinkTable);
21     return SUCCESS;        
22 }

总结

   通过这段时间的学习和实践,让我逐步明白了软件工程是一门研究用工程化方法构建和维护有效的、实用的和高质量的软件的学科。学好软件工程对于我们以后从事的工作有非常大的益处。通过对这个例子的学习,看着代码一点点的增多,我们如何好更好的管理这些代码,就离不开我们的软件工程思想,要将软件工程思想运用于我们实际的开发过程中。

 

posted @ 2020-11-09 20:14  罗星  阅读(179)  评论(0)    收藏  举报