代码中的软件工程——Menu项目实操
前言
本篇博文基于孟宁老师上课内容和所提供资料
通过一个menu项目,浅谈一些我对于软件工程的理解。感谢孟宁老师在我此过程中所给予的帮助!
编译和调试环境配置
1.在写C程序前,需要准备MinGW-64用于编译和调试
将MinGW64离线安装包解压后,配置环境变量,将目录下的bin添加到系统变量的Path路径中,在命令行中输入gcc -v
查看gcc的版本,查看是否能够运行gcc命令
2.在VSCode中安装c/c++插件
3.配置C++环境,在VSCode内点击运行栏,点击创建launch.json文件
使用简单的.cpp文件配置C++环境
(1)新建空文件夹Code
(2)打开VScode --> 打开文件夹 --> 选择刚刚创建的文件夹Code
(3)新建test.cpp文件(以最简单的 HelloWorld.cpp 为例)
(4)进入调试界面添加配置环境,选择 C++(GDB/LLDB),再选择 g++.exe,之后会自动生成 launch.json 配置文件,并且进行修改
(6)返回.cpp文件,按F5进行调试,会弹出找不到任务"task g++",选择 "配置任务",会自动生成 tasks.json 文件
(7)编辑 tasks.json 文件
【注】: launch.json 文件中 "preLaunchTask" 的值 必须与 tasks.json 文件中 "label"的值一致。值的设置看个人喜好,保持默认也是OK的。
8.调试成功
代码分析
1.模块化
通过孟宁老师的MOOC课程的项目——实现一个命令行的菜单小程序
最终目标是完成一个通用的命令行的菜单子系统便于在不同项目中重用——来分析其中的软件工程方法、规范或软件工程思想
将项目导入到VSCode中阅读
由MENU程序的设计,可以体会到模块程序设计和耦合度以及内聚度的概念。孟宁老师课上提到代码中模块化的主要目的是为了包容变化,当我们的需求发生变化时,我们可以尽可能的减少对代码的修改。
因此需要我们代码追求低内聚和高耦合。
模块化
是指在进行程序设计时将一个大程序按照功能划分为若干小程序模块,每个小程序模块完成一个确定的功能,并在这些模块之间建立必要的联系,通过模块的互相协作完成整个功能的程序设计方法。
在设计较复杂的程序时,一般采用自顶向下的方法,将问题划分为几个部分,各个部分再进行细化,直到分解为较好解决问题为止。模块化设计,简单地说就是程序的编写不是一开始就逐条录入计算机语句和指令,而是首先用主程序、子程序、子过程等框架把软件的主要结构和流程描述出来,并定义和调试好各个框架之间的输入、输出链接关系逐步求精的结果是得到一系列以功能块为单位的算法描述。以功能块为单位进行程序设计,实现其求解算法的方法称为模块化。模块化的目的是为了降低程序复杂度,使程序设计、调试和维护等操作简单化。
耦合度和内聚度
此外,为了衡量模块化程度,我们引入了耦合度和内聚度的概念。其中,耦合度是指软件模块之间的依赖程度;而内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。
模块化具体实现
在.h头文件中,放入相关操作需要的数据结构和函数声明
在.c文件中放入函数的具体实现
可以清晰地看到,随着项目的开发,项目所实现的功能在逐渐增多。而功能扩展需要尽可能的减少对原有功能的改变,并且需要在开发流程中清晰。即对扩展开放,对修改关闭。
我们发现上面的代码在功能性需求出现变化时,我们可以很好的进行维护,但当一些非功能性需求发生改变(例如数据结构发生变化等情况时),我们就需要对上述代码进行大量的修改。因此,我们可以把整段代码划分为业务逻辑层和数据存储层。我们把与功能相关的代码放在menu.c文件中,把与数据结构及其操作相关的代码放在linklist.h文件中在,并且在linklist.c文件中实现所需函数,于是便有了lab3.3中的代码。
2.可重用接口
在C语言中的数组、序列、线程、集合、位向量、字符串等都可以用接口和实现这样的组合来形成一个可重用的模块。
我们要把模块变成可重用的库函数,就是要理解接口和实现的上述规范,定义好接口的声明和标识符,以最简单的方式实现,打包成库函数,并且通过最严格的测试。
除此之外,我们还要考虑这些库函数的通用性、简单性和有效性的问题。这些问题才是比将一个模块打包成库函数更难解决的问题。
实现软件可重用,模块打包库函数
在menu项目中,我们用到了链表的数据结构,而在一个比较大的项目中,我们可能会多次用到链表的结构。因此,我们希望能写一个链表的通用模块,方便以后的重用。于是便有lab4中linktable.c和linktable.h文件
#ifndef _LINK_TABLE_H_ \#define _LINK_TABLE_H_ \#include <pthread.h> \#define SUCCESS 0 \#define FAILURE (-1) /* \* LinkTable Node Type */ typedef struct LinkTableNode { struct LinkTableNode * pNext; }tLinkTableNode; /* \* LinkTable Type */ typedef struct LinkTable tLinkTable; /* \* Create a LinkTable */ tLinkTable * CreateLinkTable(); /* \* Delete a LinkTable */ int DeleteLinkTable(tLinkTable *pLinkTable); /* \* Add a LinkTableNode to LinkTable */ int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode); /* \* Delete a LinkTableNode from LinkTable */ int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode); /* \* Search a LinkTableNode from LinkTable \* int Conditon(tLinkTableNode * pNode,void * args); */ tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args); /* \* get LinkTableHead */ tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable); /* \* get next LinkTableNode */ tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode); \#endif /* _LINK_TABLE_H_ */
linktable.c 的 SearchLinkTableNode 函数,int Conditon(tLinkTableNode * pNode, void * args)
依赖注入,是因为要提高重用性。设想不同人都用Compare()函数比较不同的东西,比如字符串大小、数组大小、数值大小,加入直接限制了查找条件,将不能重用。
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; }
线程安全
若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入的(reentrant)。可重入和线程安全密切相关。可重入函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。操作系统说,信号量用来保护临界资源,而临界资源是一段可共享的代码。在 linktable.c 里可以看到很多类似的代码:
/* * Add a LinkTableNode to LinkTable */ int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode) { if(pLinkTable == NULL || pNode == NULL) { return FAILURE; } pNode->pNext = NULL; pthread_mutex_lock(&(pLinkTable->mutex)); //加锁 if(pLinkTable->pHead == NULL) { pLinkTable->pHead = pNode; } if(pLinkTable->pTail == NULL) { pLinkTable->pTail = pNode; } else { pLinkTable->pTail->pNext = pNode; pLinkTable->pTail = pNode; } pLinkTable->SumOfNode += 1 ; pthread_mutex_unlock(&(pLinkTable->mutex)); //释放锁 return SUCCESS; }
我们可以看到,这个函数主要是用来添加一个结点,因为可能有多个线程同时在运行,所以为了保证数据不被破坏,在对数据修改的时候使用了锁。这样就可以保证数据的成功添加。
当开发过程中,我们遇到并发问题。怎么解决?一种解决方式,简单粗暴:上锁。将千军万马都给拦下来,只允许一个人过独木桥。书面意思就是将并行的程序变成串行的程序。现实的锁有门锁、挂锁和抽屉锁等等。在Java学习中,我们的锁就是synchronized关键字和Lock接口。
总结
以上便是我通过阅读menu项目代码,结合孟宁老师上课所讲的内容以及所提供的学习资料,对软件工程的一个初步的理解。希望可以将所学到的内容应用在以后所写的代码中,使代码更简洁、更易读、更便于重用。再次感谢孟宁老师在学习过程中所给予的帮助!