代码中的软件工程---menu项目
写在前面:
孟宁老师在高软课堂上向我们展示了如何一步一步的搭建一个项目,并且将软件工程的思维运用在其中。以一个menu项目为例从最开始的一个helloworld简单程序到后面一步一步添加功能完善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.vscode中c/c++插件安装
直接在vscode扩展插件中搜索,安装vscode中的c/c++模块。
2.GCC编译器安装
安装好vscode中的插件后,由于插件中并不包含c/c++的编译器,所以需要我们自行安装编译器。
继续在自己的电脑上安装gcc编译器或者windows版gcc编译器MinGW,下载地址:Mingw-w64 for Windows 64 & 32 bits
点击下载好的mingw可执行.exe文件,如“mingw-get-setup.exe”,点击install。
下载安装后记得在系统设置中添加环境变量,右键我的电脑->属性->高级系统设置->高级->环境变量,在系统变量的path下追加MinGW的bin地址:
Ctrl+R,输入cmd打开控制台,在控制台中输入gcc -v 查看gcc的版本:
表示gcc已经安装完成。
打开vscode运行hello.c,已经可以正常输出hello world。
二.模块化设计
模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。
这个做法背后的基本原理是关注点的分离 (SoC, Separation of Concerns),是由软件工程领域的奠基性人物Edsger Wybe Dijkstra(1930~2002)在1974年提出,没错就是Dijkstra最短路径算法的作者。
关注点的分离在软件工程领域是最重要的原则,我们习惯上称为模块化,翻译成我们中文的表述其实就是“分而治之”的方法。关注点的分离的思想背后的根源是由于人脑处理复杂问题时容易出错,把复杂问题分解成一个个简单问题,从而减少出错的情形。
模块化软件设计的方法如果应用的比较好,最终每一个软件模块都将只有一个单一的功能目标,并相对独立于其他软件模块,使得每一个软件模块都容易理解容易开发。
从而整个软件系统也更容易定位软件缺陷bug,因为每一个软件缺陷bug都局限在很少的一两个软件模块内。 而且整个系统的变更和维护也更容易,因为一个软件模块内的变更只影响很少的几个软件模块。
因此,软件设计中的模块化程度便成为了软件设计有多好的一个重要指标,一般我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度。
耦合度 是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。 一般在软件设计中我们追求松散耦合。
内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。 理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性(Feather)。
在lab3.2的menu文件中,整个项目的代码全部放在menu.c一个文件夹中,所有功能的实现都在这些代码里面,没有进行任何模块化设计,显得杂乱无章。
lab3.2 menu代码:
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中,各个功能的实现被分开了,函数的实现放在了linklist.c中,而在linklist.h文件中定义了数据结构和linklist.c中的函数调用,最后再在menu.c中将linklist.h作为头文件引入,这样就可以清晰明了的只调用功能而不将具体的实现都体现在同一个文件中,这种方法可以更好的隐藏模块的代码实现细节,只留给调用者简洁的接口信息。
进行了模块化设计之后我们往往将设计的模块与实现的源代码文件有个映射对应关系,因此我们需要将数据结构和它的操作独立放到单独的源代码文件中,这时就需要设计合适的接口,以便于模块之间互相调用。
三.可重用接口设计
我们要求这一个软件模块只做一件事,也就是功能内聚,那就要让它做好链表数据结构和对链表的操作,不应该涉及菜单业务功能上的东西;同样我们希望这一个软件模块与其他软件模块之间松散耦合,就需要定义简洁、清晰、明确的接口。
接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。
接口规格是软件系统的开发者正确使用一个软件模块需要知道的所有信息,那么这个软件模块的接口规格定义就必须清晰明确地说明正确使用本软件模块的信息。一般来说,接口规格包含五个基本要素:
- 接口的目的;
- 接口使用前所需要满足的条件,一般称为前置条件或假定条件;
- 使用接口的双方遵守的协议规范;
- 接口使用之后的效果,一般称为后置条件;
- 接口所隐含的质量属性。
例如:
tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);
此函数接口代码是从链表中取出链表的头节点的函数声明,使用该接口之后的效果是找到了链表的头节点,这里是通过tLinkTableNode类型的指针作为返回值来作为后置条件,C语言中也可以使用指针类型的参数作为后置条件;该接口没有特别要求接口的质量属性,如果搜索一个节点可能需要在可以接受的延时时间范围内完成搜索;
给Linktable增加Callback方式的接口,需要两个函数接口,一个是call-in方式函数,如SearchLinkTableNode函数,其中有一个函数作为参数,这个作为参数的函数就是callback函数,如代码中Conditon函数。
其中callback函数condition作为参数传入SearchLinkTableNode函数,Conditon负责收集的情报范围tLinkTableNode * pNode,一旦发现目标情报卧底就被激活return pNode。
开始时使用全局变量cmd,风险较大,为了降低风险增加了args参数,只有在搜集到目标情报args时才向基地报道完成任务。
四.线程安全
什么是线程安全?
- 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
- 线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
函数的可重用性:
可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用局部变量,要么在使用全局变量时保护自己的数据。
函数的可重用性和线程安全之间的关系:
- 可重入的函数不一定是线程安全的,可能是线程安全的也可能不是线程安全的;可重入的函数在多个线程中并发使用时是线程安全的,但不同的可重入函数(共享全局变量及静态变量)在多个线程中并发使用时会有线程安全问题;
- 不可重入的函数一定不是线程安全的。
对于linktable软件模块的线程安全分析,我们要考虑:所有的函数是不是都是可重入函数,不同的可重入函数有没有可能同时进入临界区,写互斥,读写互斥。
体现在linktable.c的代码中就是无论是在创建node时还是删除node时都先对线程加锁然后进行操作,操作完成后再对线程解锁。
增加节点:
删除节点:
总结:通过这次作业和老师的课上讲解我了解到了软件工程中的一些新的概念,也明白了软件开发过程中处处都存在着规范,对体现在代码中的软件工程思想有了更深刻的理解,不论是模块化设计还是对接口的实现尽可能的封装隐藏细节,还有在软件开发过程中线程安全是很重要的一个点。