代码中的软件工程
这几个月在孟宁老师的课上学习了如何从0开始搭建一个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
一、编译和调试环境配置
在这个项目中,使用到的语言是C语言,所以需要下载GCC作为编译器,编辑器选用VS Code。
首先需要进入MinGW官网:http://www.mingw.org

点击左上角的Downloads

然后点击下载,下载结束后打开电脑的环境变量,在系统变量的Path变量中加入安装MinGW的安装路径
打开cmd,输入 gcc -v ,出现如下:

说明安装成功
然后打开VSCode,VSCode只是一个代码编辑器,为了让其支持C语言,需要安装插件:C/C++

然后需要配置相应的C语言文件。首先选择一个要写代码的文件,这里我在桌面新建一个文件menu,我所有的代码都要在这个文件夹下进行。
然后在menu文件夹下新建一个文件夹:.vscode,注意前面有一个. 然后在.vscode文件夹下新建两个文件:tasks.json和launch.json
在tasks.json中写如下代码:
{ "version": "2.0.0", "tasks": [ { "label": "build", "type": "shell", "group": { "kind": "build", "isDefault": true }, "presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "shared" }, "windows": { "command": "g++", "args": [ "-ggdb", "\"${file}\"", "--std=c++11", "-o", "\"${fileDirname}\\${fileBasenameNoExtension}.exe\"" ] } } ] }
在launch.json中写如下代码:
{ "version": "0.2.0", "configurations": [ { "name": "(gdb) Launch", "preLaunchTask": "build", "type": "cppdbg", "request": "launch", "program": "${fileDirname}/${fileBasenameNoExtension}.exe", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": true, "MIMode": "gdb", "miDebuggerPath": "C:/Program Files (x86)/Dev-Cpp/MinGW64/bin/gdb.exe", "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true } ] }] }
至此,整个工具就已经配置完成。
二、对Menu文件进行分析
(一)代码风格
代码风格的原则:简明、易读、无二义性
首先代码的风格要简明,在括号、排版、换行等问题上,尽量遵循一下规则:
缩进保持在4个空格,一行要求在一百个字符内,在操作符前后要适当留出一个空格,函数体内逻辑上不相关的语句之间要留出一些空行来区分,花括号要独占一行并且对齐,多条语句要换行分开,不要将多个语句定义放在同一行。
在变量和函数的命名上要注意与其代表的含义一致,一般的变量命名是要注意第一个单词首字母小写,之后的单词首字母大写。
在一个文件头部要注明文件的版权、作者、版本、描述等相关信息,主食要使用ASCII字符格式的英文,示例如下图所示:

(二)模块化设计
模块化设计是软件工程中一个非常重要的思想,在软件设计时让系统内的各个模块保持相对独立,让每一个部分被独立的开发。其背后的思想是让一个复杂的问题分解成一个个简单的问题,“分而治之”,减少出错的情况。
这里需要提到软件设计中的一个原则:KISS(Keep It Sample & Stupid)
一行代码只做一件事
一个代码块只做一件事
一个函数只做一件事
一个软件模块只做一件事
为了评价软件设计中的模块化程度,一般我们使用耦合度和内聚度来衡量。
耦合度:是指软件模块之间的依赖程度,一般可以分为紧密耦合、松散耦合和无耦合。

在软件设计中,我们要追求松散耦合
内聚度:是指一个软件模块内部各种元素之间相互依赖的紧密程度。
在软件设计中我们追求的内聚是功能内聚,让一个软件模块只完成一件事。
比如在menu文件中,我们在menu存储时需要使用到链表这个数据结构,就需要新建一个linktable.c文件,将所有关于链表的操作都定义到这个文件下,我们对链表的所有操作封装到一起,保证了一个模块只做一件事,当我们需要调用链表时,直接使用已经封装好的模块中的操作就行,不需要对链表的操作进行修改,保证了代码的关注点分离。

同时,我们可以通过使用本地化外部接口来提高代码的使用能力

将我们的代码接口分离出来,写成本地化的外部接口,能更好的帮助我们分离业务之间的关联性,使得代码开发更加高效。
(三)可重用接口
虽然我们将链表的操作封装到了一个文件中,但这时还需要注意一点的是,我们的链表操作中还是有很多关于菜单命令的痕迹,导致我们可能在以后想要用到链表操作时,调用该文件会产生错误,这就要求我们在写代码的过程中只去关注当前文件的内容,操作功能内聚,写链表操作就不要涉及其他文件的内容,同时我们也要尽力追求链表操作尽量与其他模块之间松散耦合,做到一个通用型的接口。
一般在做到通用型接口的同时,要考虑一下几个问题:
1、接口的定义是否清晰完善,有一致的命名规则;
2、对用到的数据结构和算法要给出清晰的文档描述;
3、与外部的参数传递及错误处理部分要单独存放易于修改
4、记录发现的缺陷及修订缺陷的情况。
tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode) { if(pLinkTable == NULL || pNode == NULL) { return NULL; } tLinkTableNode * pTempNode = pLinkTable->pHead; while(pTempNode != NULL) { if(pTempNode == pNode) { return pTempNode->pNext; } pTempNode = pTempNode->pNext; } return NULL; }
比如在上面这个函数中,我们定义了一个名为GetNextLinkTableNode的函数名,这个函数名表示我们要找到链表的下一个节点;传入的参数是当前链表的头结点和需要找到下一个节点的节点,返回值是一个节点指针。
这里我们需要首先判断一下传入的参数是否合法,我们需要在自己实现的代码过程中尽力保证运行不会出错,而不是将判断节点是否有错的任务交给调用者去思考。
当我们从链表头节点开始遍历直到链表尾任未发现节点时,需要返回NULL,保证函数能顺利返回一个值。
在这个函数中,调用者必须遵守链表文件定义节点tLinkTableNode和链表tLinkTable的相关规定,这也是函数使用默认规则。
再比如SearchLinkTableNode这个函数,我们调用这个函数寻找链表中是否出现过一个节点,我们在函数传参的时候传入Conditon函数作为参数,将SearchLinkTableNode函数与业务层的数据相分离,Conditon中比较的是什么,SearchLinkTableNode并不关心,这样写使得查询函数的接口更加通用,有效的提高了接口的通用性。
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args) { if(pLinkTable == NULL || Conditon == NULL) { return NULL; } tLinkTableNode * pNode = pLinkTable->pHead; while(pNode != NULL) { if(Conditon(pNode,args) == SUCCESS) { return pNode; } pNode = pNode->pNext; } return NULL; }
condition函数:
int Conditon(tLinkTableNode * pLinkTableNode,void * arg) { char * cmd = (char*)arg; tDataNode * pNode = (tDataNode *)pLinkTableNode; if(!strcmp(pNode->cmd, cmd)) { return SUCCESS; } return FAILURE; }
在以后的调用中,我们即使脱离了menu小程序,也能将这个链表操作的文件交给其他文件去使用,这就真正达到了代码的可重用。
通用接口定义的基本方法:
- 参数化上下文
- 移除前置条件
- 简化后置条件
(四)可重入函数与线程安全
可重入函数可以由多个任务并发的调用,而不必担心数据错误。当前计算机迅速发展,CPU核数变多,可能会遇到同一个函数并发执行的问题,这就要求我们考虑函数的线程安全问题。
可重入函数的基本要求:
1、不为连续的调用持有静态数据;
2、不返回指向静态数据的指针;
3、所有数据都由函数的调用者提供;
4、使用局部变量,或者通过制作全局数据的局部变量拷贝来保护全局数据;
5、使用静态数据或全局变量时要做周密的并行时序分析,通过临界区互斥避免临界区冲突;
6、绝不调用任何不可重入函数。
但要注意,可重入的函数不一定是线程安全的,不可重入的函数一定不是线程安全的。
/* * Delete a LinkTable */ int DeleteLinkTable(tLinkTable *pLinkTable) { if(pLinkTable == NULL) { return FAILURE; } while(pLinkTable->pHead != NULL) { tLinkTableNode * p = pLinkTable->pHead; pthread_mutex_lock(&(pLinkTable->mutex)); pLinkTable->pHead = pLinkTable->pHead->pNext; pLinkTable->SumOfNode -= 1 ; pthread_mutex_unlock(&(pLinkTable->mutex)); free(p); } pLinkTable->pHead = NULL; pLinkTable->pTail = NULL; pLinkTable->SumOfNode = 0; pthread_mutex_destroy(&(pLinkTable->mutex)); free(pLinkTable); return SUCCESS; }
比如DeleteLinkTable函数,我们在使用是为了保证我们删除节点的时候不会出现对于同一个节点删除两次的线程安全问题,需要加入一个读写锁,当我们要删除一个节点时,对当前节点加锁,在解锁以前,不允许其他线程对当前节点进行操作。
tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable) { if(pLinkTable == NULL) { return NULL; } return pLinkTable->pHead; }
对于GetLinkTableHead函数,得到链表的头结点不会发生线程安全问题,即使多个线程同时读入链表头节点也时被允许的。
三、实践总结
软件工程的研究目标是寻找开发高质量软件的策略,我们一般从三个角度来提高软件质量:从软件产品本身内在的质量特点;从用户的角度来看软件是否有良好的用户体验;从商业的角度来看,开发软件能够获得投资回报或其他驱动因素。
在以后写代码的时候,要思考自己的代码能否从这三个角度来提高,并且不断通过代码实战来提高自己的软件工程水平。

浙公网安备 33010602011771号