代码中的软件工程
作者:互联网
引言
孟老师通过一个简单的menu小程序,直观细致地给我们讲解了模块化设计、可重用接口以及线程安全等问题,令我受益匪浅。在此感谢孟老师的辛勤指导!!!
本文参考孟老师的文章:https://gitee.com/mengning997/se/blob/master/README.md
实验环境配置
安装Mingw-w64/GCC编译器
安装成功
VsCode环境搭建
生成的task.json文件
{ "version": "2.0.0", "tasks": [ { "type": "cppbuild", "label": "C/C++: gcc.exe build active file", "command": "D:\\mingw\\mingw32\\bin\\gcc.exe", "args": [ "-g", "${file}", "-o", "${fileDirname}\\${fileBasenameNoExtension}.exe" ], "options": { "cwd": "D:\\mingw\\mingw32\\bin" }, "problemMatcher": [ "$gcc" ], "group": { "kind": "build", "isDefault": true }, "detail": "compiler: D:\\mingw\\mingw32\\bin\\gcc.exe" } ] }
生成的launch.json文件
{ // 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。 // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "gcc.exe - 生成和调试活动文件", "type": "cppdbg", "request": "launch", "program": "${fileDirname}\\${fileBasenameNoExtension}.exe", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": false, "MIMode": "gdb", "miDebuggerPath": "D:\\mingw\\mingw32\\bin\\gdb.exe", "setupCommands": [ { "description": "为 gdb 启用整齐打印", "text": "-enable-pretty-printing", "ignoreFailures": true } ], "preLaunchTask": "C/C++: gcc.exe build active file" } ] }
模块化设计
模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离 (SoC, Separation of Concerns),是由软件工领域的奠基性人物Edsger Wybe Dijkstra(1930~2002)在1974年提出。 关注点的分离的思想背后的根源是由于人脑处理复杂问题时容易出错,把复杂问题分解成一个个简单问题,从而减少出错的情形。 模块化软件设计的方法如果应用的比较好,最终每一个软件模块都将只有一个单一的功能目标,并相对独立于其他软件模块,使得每一个软件模块都容易理解容易开发。从而整个软件系统也更容易定位软缺陷bug,因为每一个软件缺陷bug都局限在很少的一两个软件模块内。 而且整个系统的变更和维护也更容易,因为一个软件模块内的变更只影响很少的几个软件模块。因此,软件设计中的模块化程度便成为了软件设计有多好的一个重要指标。一般我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度。 耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。一般在软件设计中我们追求松散耦合。 内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性(Feather)。由于模块化设计的设计原则使得一个模块只做一件事,模块之间不需要知道具体实现的过程,在使用时只需要调用即可。因此,模块化设计的系统总体满足高内聚低耦合的特性。在孟老师所给出的menu项目中,我们不难看出主要由linktable模块、menu模块、test模块构成,这里我以linktable模块为例说明:
struct LinkTable { tLinkTableNode *pHead; tLinkTableNode *pTail; int SumOfNode; pthread_mutex_t mutex; }; int DeleteLinkTable(tLinkTable *pLinkTable) int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode) int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode) tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args)
LinkTable模块实现了对menu的各种操作,如增删改链表等功能,对链表操作的函数无需关注细节,直接调用就好,代码易于理解,并且如果新增功能(如加上查找链表的功能),直接调用函数即可,无需重写代码。
menu模块并不需要知道LinkTable内方法的具体实现过程,只需要关注自身方法的具体实现,需要时调用LinkTable模块即可,这就是模块化思想的好处。
可重用设计
有了模块化的代码结构之后,要让各个模块之间能很好地互相调用,就需要设计合适的接口,具体来说,要求简洁,清晰,明确。
接口,就是互相联系的双方共同遵守的一种协议规范,换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务,一般是通过定义一组 API 函数来约定沟通方式。
在面向过程的编程中,接口一般定义了数据结构及操作这些数据结构的函数;而在面向对象的编程中,接口是对象对外开放(public)的一组属性和方法的集合。函数或方法具体包括名称、参数和返回值等。
接口规格是软件系统的开发者正确使用一个软件模块需要知道的所有信息,那么这个软件模块的接口规格定义就必须清晰明确地说明正确使用本软件模块的信息。
一般来说,接口包含五个基本要素:接口的目的、接口使用所需满足的前置条件或假定条件、使用接口的双方遵守的协议规范、接口使用之后的效果、接口所隐含的质量属性。
仍然以linktable模块进行分析,在linktable.h中给用到的链表相关的接口下了定义:
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);
linktable.c中实现源代码,做到了两个模块的解耦:
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; }
这样的好处是在团队分工开发时,事先定义好公共的如果两个开发者要分工开发,头文件(.h),然后就可以各自按照头文件的规范进行开发,互不干扰,加快开发速度。同时这样可以有效地隐藏软件模块内部的实现细节,为外部调用接口的开发者提供更加简洁的接口信息,减少了外部调用接口的开发者有意或无意的破坏软件模块内部数据的风险。
可重入函数与线程安全
可重入函数:若一个程序或子程序可以“在任意时刻被中断,然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。也就是说,当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时,重新进入同一个子程序,仍然是安全的。
可重入函数应满足的条件:不能含有静态(全局)非常量数据、不能返回静态(全局)非常量数据的地址、只能处理由调用者提供的数据、不能依赖于单实例模式资源的锁、调用的函数也必需是可重入的。上述条件就是要求可重入函数使用的所有变量都保存在调用栈的当前函数帧(frame)上。因此,同一执行线程重入执行该函数时 加载了新的函数帧,与前一次执行该函数时使用的函数帧不冲突、不互相覆盖,从而保证了可重入执行安全。
线程安全:多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
需要考虑线程安全的情况:访问共享的变量或资源, 会有并发风险, 比如对象的属性, 静态变量, 共享缓存, 数据库等;所有依赖时序的操作, 即使每一步操作都是线程安全的, 还是存在并发的问题;不同的数据之间存在绑定关系的时候。例如IP与端口号. 只要修改了IP就要修改端口号, 否则IP也是无效的。 因此遇到这种操作的时候,要警醒原子的合并操作,要么全部修改成功, 要么全部修改失败。使用其他类的时候, 如果该类的注释声明了不是线程安全的,那么就不应该在多线程的场景中使用, 而应该考虑其对应的线程安全的类,或者对其做一定处理保证线程安全。仍以linktable块做分析,可以看出linktable.h文件通过给链表的增删操作加了一个互斥锁实现线程安全,在结构体LinkTable中定义可互斥锁pthread_mutex_t:
struct LinkTable { tLinkTableNode *pHead; tLinkTableNode *pTail; int SumOfNode; pthread_mutex_t mutex; };
CreateLinkTable函数中使用 pthread_mutex_init对互斥锁进行初始化:
tLinkTable * CreateLinkTable() { tLinkTable * pLinkTable = (tLinkTable *)malloc(sizeof(tLinkTable)); if(pLinkTable == NULL) { return NULL; } pLinkTable->pHead = NULL; pLinkTable->pTail = NULL; pLinkTable->SumOfNode = 0; pthread_mutex_init(&(pLinkTable->mutex), NULL); return pLinkTable; }
在DeleteLinkTable函数、AddLinkTableNode函数、DelLinkTableNode函数中对链表的操作都分别进行加锁和解锁。以DeleteLinkTable函数为例,使用pthread_mutex_lock和pthread_mutex_unlock在删除链表结点前后分别进行加锁和解锁,使用pthread_mutex_destroy销毁互斥锁:
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; }
我们可以看出,通过加锁和解锁操作确保在同一时间最多只有一个线程访问添加或删除结点的代码,可以避免数据错误,即使用互斥锁实现了线程安全。
总结
通过学习孟老师的范例程序,我学到了很多,对比较抽象的软件工程的一些理论和方法有了现实的认识,并且能在实践中得到应用。再次感谢孟老师!!!
标签:pLinkTable,代码,软件工程,mutex,模块,线程,tLinkTableNode,tLinkTable 来源: https://www.cnblogs.com/codeingtest/p/13941966.html