代码中的软件工程
前言
本篇博客基于孟宁老师的高级软件工程课程,以VS Code + 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,点击侧边栏的扩展(Ctrl+Shift+X),搜索c++,点击安装C/C++
C/C++扩展不包含C++编译器或调试器
2、安装C++编译器和调试器
本机使用Windows系统,因此下载Mingw-w64(Windows中的GCC,http://mingw-w64.org)
安装过程中有几个选项需要说明:
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构建版本号,选择最大即可
安装完成后,需要将安装目录下的bin目录加入环境变量
添加环境变量后,打开CMD,执行gcc -v看看是否安装成功
3、运行和调试
使用vscode打开一个C文件,点击侧边栏的运行(Ctrl+Shift+D),点击运行和调试,选择环境C++(GBD/LLDB)
#注意文件夹路径不能有中文
选择配置gcc.exe
生成了.vscode文件夹,其中有launch.json和tasks.json文件
tasks.json告诉vscode如何生成(编译)程序的文件,将调用gcc编译器基于源代码创建可执行文件
launch.json用于调试器设置
二、软件工程方法
一个菜单程序的生长
初始代码lab2(menu.c):
#include <stdio.h> #include <stdlib.h> int main() { char cmd[128]; while(1) { scanf("%s", cmd); if(strcmp(cmd, "help") == 0) { printf("This is help cmd!\n"); } else if(strcmp(cmd, "quit") == 0) { exit(0); } else { printf("Wrong cmd!\n"); } } }
1、模块化设计
模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发,这个做法背后的基本原理是关注点的分离 (SoC, Separation of Concerns),是由软件工程领域的奠基性人物Edsger Wybe Dijkstra(1930~2002)在1974年提出。关注点的分离在软件工程领域是最重要的原则,我们习惯上称为模块化,翻译成我们中文的表述其实就是“分而治之”的方法
模块化程度是软件设计有多好的一个重要指标,一般我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度
耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。一般在软件设计中我们追求松散耦合
内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性(Feather)
引入链表结构,DataNode储存了命令名、命令描述、函数和下一结点的指针,将数据结构和它的操作从菜单业务中分离出来
/* find a cmd in the linklist and return the datanode pointer */ tDataNode* FindCmd(tDataNode * head, char * cmd); /* show all cmd in listlist */ int ShowAllCmd(tDataNode * head);
并将链表结构定义和函数操作声明放在linklist.h中,对链表的操作的具体实现放在linklist.c文件中
#include <stdio.h> #include <stdlib.h> #include "linklist.h" int Help(); #define CMD_MAX_LEN 128 #define DESC_LEN 1024 #define CMD_NUM 10 /* menu program */ static tDataNode head[] = { {"help", "this is help cmd!", Help,&head[1]}, {"version", "menu program v1.0", NULL, NULL} }; main() { /* cmd line begins */ while(1) { char cmd[CMD_MAX_LEN]; printf("Input a cmd number > "); scanf("%s", cmd); tDataNode *p = FindCmd(head, cmd); if( p == NULL) { printf("This is a wrong cmd!\n "); continue; } printf("%s - %s\n", p->cmd, p->desc); if(p->handler != NULL) { p->handler(); } } } int Help() { ShowAllCmd(head); return 0; }
主程序menu.c主体并没有很大变化,将复杂的问题抽离出去,调用其接口。每一个软件模块都将只有一个单一的功能目标,并相对独立于其他软件模块,使得每一个软件模块都容易理解容易开发。也使得后期维护变得简单一些,整个软件系统更容易定位软件缺陷bug,因为每一个软件缺陷bug都局限在很少的一两个软件模块内。而且整个系统的变更和维护也更容易,因为一个软件模块内的变更只影响很少的几个软件模块。
使用本地化外部接口来提高代码的适应能力:
2、可重用接口
接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。在面向过程的编程中,接口一般定义了数据结构及操作这些数据结构的函数;而在面向对象的编程中,接口是对象对外开放(public)的一组属性和方法的集合。函数或方法具体包括名称、参数和返回值等。
接口规格包含五个基本要素:
接口的目的;
接口使用前所需要满足的条件,一般称为前置条件或假定条件;
使用接口的双方遵守的协议规范;
接口使用之后的效果,一般称为后置条件;
接口所隐含的质量属性。
lab4代码(linktalbe.c):
该接口的目标是从链表中下一个链表节点,函数名GetNextLinkTableNode清晰明确地表明了接口的目标;
该接口的前置条件是链表必须存在,同时当前结点不为空,使用该接口才有意义,也就是链表(pLinkTable != NULL)&&(pNode != NULL);
使用该接口的双方遵守的协议规范是通过数据结构tLinkTableNode和tLinkTable定义的;
使用该接口之后的效果是找到了链表的下一个节点,这里是通过tLinkTableNode类型的指针作为返回值来作为后置条件,C语言中也可以使用指针类型的参数作为后置条件;
该接口没有特别要求接口的质量属性,如果搜索一个节点可能需要在可以接受的延时时间范围内完成搜索;
3、线程安全
线程(thread)是操作系统能够进行运算调度的最小单位。它包含在进程之中,是进程中的实际运作单位。一个线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。一般默认一个进程中只包含一个线程。
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
添加互斥锁:
/* * LinkTable Type */ typedef struct LinkTable { tLinkTableNode *pHead; tLinkTableNode *pTail; int SumOfNode; pthread_mutex_t mutex; }tLinkTable;
在创建链表函数中初始化互斥锁:
pthread_mutex_init(&(pLinkTable->mutex), NULL);
在修改链表时需要加锁解锁:
pthread_mutex_lock(&(pLinkTable->mutex)); pLinkTable->pHead = pLinkTable->pHead->pNext; pLinkTable->SumOfNode -= 1 ; pthread_mutex_unlock(&(pLinkTable->mutex));
删除链表时销毁互斥锁:
pthread_mutex_destroy(&(pLinkTable->mutex));
三、小结
通过孟老师课堂上的耐心讲解,以及课后阅读孟老师menu项目的迭代过程,结合代码分析其中的软件工程方法,这使得我们对软件的开发有了更深刻的认识,同时意识到了以前自己开发软件的混杂,在此深表谢意。