通过menu小工具初探代码中的软件工程
一个合格的软件项目应该具备什么样的代码风格?恐怕很多人都有各自的标准,其中最重要的几个方面,一定是大家都能接受的共识。比如代码注释规范,变量和函数的命名风格,代码结构清晰易懂,通过模块化设计做到降低耦合度,代码可重用,另外还有涉及到多线程时的线程安全等。
为了让大家有一个更加直观的感受,接下来我们就以中科大孟宁老师所写的一个menu(命令行)小工具为例,初探软件工程的魅力所在。项目链接地址如下:
参考资料:https://gitee.com/mengning997/se/blob/master/README.md
项目链接:https://github.com/mengning/menu
对该项目的运行和分析,我们会基于Windows10,vscode以及c/c++环境,下面来简单介绍一下环境的搭建。
一.项目环境配置
由于vscode本身只是一个编辑器,并不具备编译的功能,所以为了能运行代码,我们需要现在本地安装c/c++编译工具,此处选择的是Mingw-w64。该编译工具的下载地址如下:
https://sourceforge.net/projects/mingw-w64/files/?source=navbar
选择安装目录并且解压缩后,我们需要将解压缩的文件夹下的bin目录添加到环境变量PATH里面,点击“确定”。
之后我们打开命令行界面,输入gcc -v
,如果看到以下界面,说明环境配置成功。
关于更多和Mingw-w64相关的知识,请见以下链接:
接下来,我们选择用File——Open Folder的方式,打开项目文件夹,列表如下:
可以看到项目下有多个文件,包括.c文件和.h文件。由于需要将多个文件放在一起编译运行,我们需要对.vscode文件夹下的launch.json和tasks.json文件进行配置。其中tasks.json文件主要用于项目的编译,而launch.json文件主要用于项目的运行和调试。此处运行所需的文件有linktable.c,linktable.h,menu.c,menu.h,test.c。
配置如下:
tasks.json
launch.json
之后我们运行项目,得到以下命令行界面:
项目运行成功。
二.代码中的软件工程
1.合理的代码注释习惯
最理想的情况是代码中没有注释,仅依靠函数和变量的命名,就能理解某块代码的作用以及实现思路。而现实中很少能有代码能做到,除非特别简单。因此对于函数或者文件的注释还是必要的。
那么对于来说,什么才是合格的注释呢?首先,应该用一句简短的话来描述该函数的功能。其次,将函数功能、各参数的含义和输入/输出用途等一一列举,这往往是模块的对外接口,以方便自动生成开发者文档。
对于代码中变量和函数的命名规则,同样有要求:
(1)一般变量名、对象名等使用LowerCamel风格,即第一个单词首字母小写,之后的单词都首字母大写,第一个单词一般都表示变量类型,比如int型变量iCounter;
(2)类型、类、函数名等一般都用Pascal风格,即所有单词首字母大写;
(3)类型、类、变量一般用名词或者组合名词,如Member;
(4)函数名一般使用动词或者动宾短语,如get/set,RenderPage
2.模块化软件设计
模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离 (SoC, Separation of Concerns),如果用中文来表述,就是分而治之。采用软件模块化的方法,可以将某个单一的功能封装为某个模块,当出现bug时,便只需要修改对应的模块。而且系统的变更和维护也会更加方便,只需要针对几个模块进行操作。
因此,软件设计中的模块化程度便成为了软件设计有多好的一个重要指标,一般我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度。
•内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性(feature)。
以孟宁老师的代码为例,整个项目可以分为三块:
test模块:用于测试已经整个系统的各个模块功能,为核心代码。
该模块下定义了一些特殊的函数。如果用户在命令行中输入了正确的命令字符串,则main函数中会调用对应的函数
linktable模块:该模块使用一个链表将各个命令行中的函数封装成节点,彼此连接,方便查找调用
menu模块:该模块中的函数,主要是利用linktable模块中的函数实现的,同时menu模块中定义的函数,供test模块来调用。
3.可重用设计
重用分为消费者重用和生产者重用。
消费者重用是指软件开发者在项目中重用已有的一些软件模块代码,以加快项目工作进度。需要考虑如下因素:
•该软件模块是否能满足项目所要求的功能;
•采用该软件模块代码是否比从头构建一个需要更少的工作量,包括构建软件模块和集成软件模块等相关的工作;
•该软件模块是否有完善的文档说明;
•该软件模块是否有完整的测试及修订记录
而作为开发者,对应的更多是生产者重用。
生产者重用:编写代码的时候,或设计接口的时候,编写所写的代码不仅仅满足于当前项目(建立、获取或者重新设计可复用构件的活动)。消费者关心的问题,就是生产者要设计的东西。
耦合关系取决于接口的定义,内聚关系取决于功能是否单一。如果我们想将写好的代码,用于其他业务中,所以需要通用的接口设计。需要底层模块,与当前业务需求解耦。
好比我们将链表相关的接口定义在linktable.h文件中,在linktable.c文件中实现源代码,做到与menu模块分离。同时,linktable的相关操作可以被其他模块调用。如果想替换掉linktable模块,只需要更换该接口即可。
对链表数据结构和链表的操作,不应该涉及菜单业务上的功能。做到功能内聚。接口作为调用方和被调用方联系的窗口,决定了两者之间的耦合程度。一个.h文件就是一个接口。接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问提供的服务。
一般来说,接口规格包含五个基本要素:
•接口的目的;
•接口使用前所需要满足的条件,一般称为前置条件或假定条件;
•使用接口的双方遵守的协议规范;
•接口使用之后的效果,一般称为后置条件;
接口所隐含的质量属性。
在函数接口中,有一类特殊的接口,称为call-back函数接口,如下代码所示:
给Linktable增加Callback方式的接口,需要两个函数接口,一个是call-in方式函数,如SearchLinkTableNode函数,其中有一个函数作为参数,这个作为参数的函数就是callback函数,如代码中Conditon函数。
利用callback函数参数使Linktable的查询接口更加通用,有效地提高了接口的通用性。
4.线程安全
要了解线程安全,首先要知道什么是可重入函数。
可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用局部变量,要么在使用全局变量时保护自己的数据。
一般来说, 不可重入的函数一定不是线程安全的,但可重入的函数不一定是线程安全的,比如不同的可重入函数(共享全局变量及静态变量)在多个线程中并发使用时会有线程安全问题。
比如linktable.c中实现函数 DeleteLinkTableNode()时,使用了线程锁来保证线程安全。
通过对信号量mutex加锁和解锁,保证了在删除节点的过程中的线程安全。
三.总结
合格的代码必须添加足够清晰的注释,表明它的作用和使用方法。通过模块化设计,使得系统能够方便地进行功能修改和增删,避免多个功能耦合在一起。可重用设计通过通用接口,使得某块代码可以重复地被其他地方调用,减小了软件开发的成本。通过对特定的操作加锁和解锁,可以保证多线程条件下的线程安全问题。