代码中的软件工程
本文结合menu项目案例对软件工程中的一些思想进行分析与总结,参考资料见:https://gitee.com/mengning997/se/blob/master/README.md#代码中的软件工程
一、windows下VSCode的C/C++环境配置
1.安装VSCode
2.安装VSCode的C/C++扩展
3.通过SourceForge网站安装Mingw-w64
4.将Mingw-w64/bin文件夹的路径添加到Windows的PATH环境变量
要检查Mingw-w64是否正确安装并可用,打开终端并键入:
g++ --version
gdb --version
5.配置json文件
- tasks.json
从主菜单中,选择Terminal > Configure Default Build Task,在下拉列表中,选择g++.exe build active file.
- launch.json
从主菜单中,选择Run > Add Configuration然后选择C++ (GDB/LLDB),接着在下拉列表中选择g++.exe build and debug active file.
- c_cpp_properties.json
打开命令面板(Ctrl + Shift + P),选择C/C++:Edit configurations(UI).
至此,环境接配置完成,可进行编译(Ctrl + Shift + B)、调试(F5)和运行(Ctrl + F5)。
二、模块化设计
模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离,一般在软件设计中我们追求松散耦合。
-
KISS原则
- 一行代码只做一件事
- 一个块代码只做一件事
- 一个函数只做一件事
- 一个软件模块只做一件事
在menu项目中初步的模块化设计的就是将将数据结构和它的操作与菜单业务进行分离处理,也就是说我们需要将数据结构和它的操作独立放到单独的源代码文件中,而业务代码放到另一个源代码文件中。在lab3.3中,我们可以看到,业务部分位于menu.c(包含main函数)中,而将数据结构的定义,如DataNode,和一些操作,如FindCmd和ShowAllCmd,在linklist.h中声明,它们的对应实现则位于linklist.c中。
三、可重用接口
接口就是互相联系的双方共同遵守的一种协议规范。接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。在C语言中,接口可以理解为定义了数据结构以及操作这些数据结构的函数。
要定义简洁、清晰、明确的接口,接口规格应包含五个基本要素:
- 接口的目的;
- 接口使用前所需要满足的条件,一般称为前置条件或假定条件;
- 使用接口的双方遵守的协议规范;
- 接口使用之后的效果,一般称为后置条件;
- 接口所隐含的质量属性.
结合menu项目中既有Call-in方式的函数接口,也有Callback方式的函数接口,我们重点分析一下Callback方式的函数接口,lib5.1的linktable.h中有如下接口:
/*
* Search a LinkTableNode from LinkTable
* int Conditon(tLinkTableNode * pNode);
*/
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode));
可以看到,SearchLinkTableNode接收两个参数,一个是链表指针,一个是Condition函数(即Callback函数)参数,其功能是对于给定的链表,查找该链表中满足条件的结点,如果存在满足条件的结点则返回该结点,否则返回NULL。而这个条件由调用者提供,也就是说业务层的代码在调用该接口时需要提供一个函数,该函数定义了什么是满足条件什么是不满足条件。接口在linktable.h中定义,而Condition函数在menu.c(SearchCondition函数)中实现,在menu.c中调用SearchLinkTableNode函数,就会回过头来调用自身的SearchCondition函数,也即是Callback。
查看menu.c中SearchCondition函数的实现,我们会发现其中使用了一个全局变量cmd,所以这两个模块是公共耦合的。这里引入几种耦合的概念:
- 公共耦合:当软件模块之间共享数据区或变量名即是公共耦合;
- 数据耦合:在软件模块之间仅通过显式的调用传递基本数据类型即为数据耦合;
- 标记耦合:在软件模块之间仅通过显式的调用传递复杂的数据结构(结构化数据)即为标记耦合;
耦合度:公共耦合 > 标记耦合 > 数据耦合
为解决公共耦合的问题,在call-in方式的函数接口SearchLinkTableNode中增加了一个参数args,callback函数Conditon也增加了一个参数args,如下所示:
/*
* Search a LinkTableNode from LinkTable
* int Conditon(tLinkTableNode * pNode,void * args);
*/
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args);
四、可重入函数与线程安全
1.可重入函数
可重入函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用局部变量,要么在使用全局变量时保护自己的数据。
2.线程安全
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
3.函数的可重入性与线程安全之间的关系
- 可重入的函数不一定是线程安全的
- 不可重入的函数一定不是线程安全的