代码中的软件工程
# 前言
本篇文章主要基于孟宁老师上课的内容以及menu实验代码完成。由于我是跨考生,在本科期间并没有接触过软件工程这门课程,在上了孟老师这门高级软件工程课后,让我对软件工程有了更深刻的认识和理解,故借此机会总结一下自己的收获。
参考资料:
(https://www.jianshu.com/p/a6e0d1465491)
(https://code.visualstudio.com/docs/cpp/config-mingw)
#一、环境配置
1. 安装vscode以及vscode中的c/c++扩展
由于vscode在上次作业中以及配置,这里就不在赘述,安装vscode中的c/c++扩展如下
2. 安装MinGW
本机为windows系统,需要安装c++编译器调试器,这里选择使用MinGW,
下载地址为(https://osdn.net/projects/mingw/releases/), 选择合适的版本下载安装即可。安装完成后将MinGW/bin添加到环境变量,打开控制台,输入gcc -v,出现如下图所示的界面即为安装成功。
3. 配置编译器和调试器
在vscode中打开menu-master文件夹下的hello.c文件,点击左边的工具栏运行和调试

选择c++(GDB/LLDB)

点击生成gcc.exe

这里就生成了launch.json文件
launch.json文件,是用来配置VS Code以在按F5调试程序时启动LLDB调试器。
再点击调试,生成task.json文件
tasks.json的主要作用就是执行类似 gcc -g main.c -o main 的命令,创建一个tasks.json文件告诉VS代码如何构建(编译)程序。
这里注意tasks.json的"label"参数值和launch.json的"preLaunchTask"参数值需要保持一致
之后按F5,即可看到调试过程中成功输出“Hello,world”

而在项目目录中,多出了hello.c文件以及.vscode文件夹,文件夹下多出launch.json以及tasks.json两个json文件,如下图所示

至此,准备工作已经完成。
# 二、menu项目代码分析
1. 代码规范和代码风格
代码风格的原则:简明、易读、无二义性
同类编程语言或项目在代码规范和风格的一般要求:
1)排版
1-1:程序块要采用缩进风格编写,缩进的空格数为4个。
1-2:相对独立的程序块之间、变量说明之后必须加空行。
1-3:不允许把多个短语句写在一行中,即一行只写一条语句。
1-4:对齐只使用空格键,不使用TAB键。
1-5:函数或过程的开始、结构的定义及循环、判断等语句中的代码都要采用缩进风格,case语句下的情况处理语句也要遵从语句缩进要求。
以下四种代码风格,显而易见第四种的代码块满足排版要求,更清晰,更易于阅读

2)注释
2-1:一般情况下,源程序有效注释量必须在20%以上。
2-2:文件头部应进行注释,注释必须列出:版权说明、版本号、生成日期、作者、内容、功能、修改日志等。
2-3:函数头部应进行注释,列出:函数的目的/功能、输入参数、输出参数、返回值、调用关系(函数、表)等。
2-4:边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。
2-5:注释的内容要清楚、明了,含义准确,防止注释二义性。
这里我们以孟宁老师给的menu程序为例,看看老师是如何写出优秀的注释
下图是menu程序的头部注释,可以看到,该注释包含包名、文件名和描述信息以及copyright和author之类的信息,让人一目了然清晰易懂。

之后我们再来看看函数注释
3)命名
合适的命名会大大增加代码的可读性;
3-1:类名、函数名、变量名等的命名一定要与程序里的含义保持一致,以便于阅读理解。
3-2: 类型的成员变量通常用m_或者_来做前缀以示区别。
3-3:一般变量名、对象名等使用LowerCamel风格,即第一个单词首字母小写,之后的单词都首字母大写,第一个单词一般都表示变量类型,比如int型变量iCounter。
3-4:类型、类、函数名等一般都用Pascal风格,即所有单词首字母大写。
3-5:类型、类、变量一般用名词或者组合名词,如Member 函数名一般使用动词或者动宾短语,如get/set,RenderPage;
2.模块化设计
模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。
这个做法背后的基本原理是关注点的分离。关注点的分离在软件工程领域是最重要的原则,我们习惯上称为模块化,翻译成我们中文的表述其实就是“分而治之”的方法。关注点的分离的思想背后的根源是由于人脑处理复杂问题时容易出错,把复杂问题分解成一个个简单问题,从而减少出错的情形。
我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度。
耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。 一般在软件设计中我们追求松散耦合。
内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。 理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性(Feather)。
进行了模块化设计之后我们往往将设计的模块与实现的源代码文件有个映射对应关系,因此我们需要将数据结构和它的操作独立放到单独的源代码文件中,这时就需要设计合适的接口,以便于模块之间互相调用。
让我们来看看menu项目是如何实现模块化的
在menu项目中,linktable和menu是两个不同的模块,linktable是menu中用到的数据结构,linktable定义了管理链表数据结构和对其操作的方法,这个模块看不到调用它的上层结构做了什么。
而menu是菜单的功能的实现,menu也不知道linktable具体是怎么操作链表的,但是能通过linktable提供的功能并将他们组装起来实现自己的功能。这就做到了一个软件模块只做一件事。
最后实现如下图所示的模块化

这也遵循了KISS原则
- KISS(Keep It Simple & Stupid)原则
一行代码只做一件事
一个块代码只做一件事
一个函数只做一件事
一个软件模块只做一件事
3.可重用接口
接口的基本概念
接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。
在面向过程的编程中,接口一般定义了数据结构及操作这些数据结构的函数;而在面向对象的编程中,接口是对象对外开放(public)的一组属性和方法的集合。函数或方法具体包括名称、参数和返回值等。
一般来说,接口规格包含五个基本要素:
1.接口的目的;
2.接口使用前所需要满足的条件,一般称为前置条件或假定条件;
3.使用接口的双方遵守的协议规范;
4.接口使用之后的效果,一般称为后置条件;
5.接口所隐含的质量属性。
接口只描述模块做什么,但不会包含怎么做。完成接口所做出的声明的代码被称为实现,声明了接口之后我们需要实现它才能使用。
通过将模块的接口和实现分离,我们可以对系统的其它部分隐藏实现的复杂度。
以menu程序为例,linktable.h就是linktable模块的接口,里面声明了函数,对应的linktable.c是接口的实现,需要实现linktable.h里面所声明的东西。同样menu.h和menu.c也是。
接口的设计应足够的通用,不是和单一的项目紧密耦合的,而是在不同的项目都可以重复使用。
给Linktable增加Callback方式的接口
在linktable.h定义了一个接口
根据注释以及函数名我们可以知道这个接口目的是找到链表中的一个节点,而接口的参数里有一个返回值为int的Condition函数,而Condition函数的参数类型为tLinkTableNode *,这就是callback方法。
给Linktable增加Callback方式的接口,需要两个函数接口,一个是call-in方式函数,如SearchLinkTableNode函数,其中有一个函数作为参数,这个作为参数的函数就是callback函数,如代码中Conditon函数。
利用callback函数参数使Linktable的查询接口更加通用,有效地提高了接口的通用性。 我们还通过将linktable.h中不是在接口调用时必须内容转移到linktable.c中,这样可以有效地隐藏软件模块内部的实现细节,为外部调用接口的开发者提供更加简洁的接口信息,同时也减少外部调用接口的开发者有意或无意的破坏软件模块的内部数据。通过接口进行信息隐藏已经成为面向对象编程语言的标准做法,使用public和private来声明属性和方法对于外部调用接口的开发者是否可见。
4.可重入函数与线程安全
可重入函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用局部变量,要么在使用全局变量时保护自己的数据。
线程安全即如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。而如果多个线程同时运行,线程A修改了某个数据,切换到线程B执行,修改了同样的数据,但是再切换到线程A时,这个数据已经不是线程A修改完时的样子了,那么这个进程就不是线程安全的。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
函数的可重入性与线程安全之间的关系
1.可重入的函数不一定是线程安全的,可能是线程安全的也可能不是线程安全的;可重入的函数在多个线程中并发使用时是线程安全的,但不同的可重入函数(共享全局变量及静态变量)在多个线程中并发使用时会有线程安全问题。
2.不可重入的函数一定不是线程安全的。
让我们来分析一下menu项目
一般来说只读不写是不需要加锁的,对临界区变量的改变操作就需要加锁保证对该变量的顺序执行。
我们对linktable.c中的函数进行逐一分析
1)CreateLinkTable函数

CreateLinkTable函数的主要功能是创建链表结构,不同线程之间创建链表结构、并且申请空间是相互不冲突的,所以该函数是可重入函数。
2)DeleteLinkTable函数

该函数的作用是删除链表,可以看到,该函数中有加锁pthread_mutex_lock和解锁pthread_mutex_unlock的过程,这个过程可以保证一个线程在进行链表删除操作时,不会被其他线程所干扰,也即同时只能有一个线程来进行删除操作,所以这里是线程安全的。
3)AddLinkTableNode函数

该函数同样也使用了加锁解锁的操作,保证了同时只有一个线程能够添加结点,是线程安全的。同样,DelLinkTableNode函数也是线程安全的。
4)SearchLinkTableNode函数

该函数的作用是查找链表中的结点,显然这只有读操作而没有写操作,故不需要加锁也是线程安全的,类似的还有GetLinkTableHead函数和GetNextLinkTableNode函数,都是只读不写,所以无需加锁。
至此,我们已经分析完了linktable.c中的所有函数,孟宁老师写的十分严谨,对于写操作都进行了加锁,保证了线程安全。
浙公网安备 33010602011771号