代码中的软件工程——基于menu实验的一些思考
作者:互联网
最近孟老师在课上讲解了GitHub上一个简单有趣的menu小案例,生动直观地讲解了模块化设计、可重用接口以及线程安全等问题,经过此次学习,我收获颇丰。在此我想记录一下自己的所学所思,同时感谢孟老师的辛勤教导。
本文源代码来自于孟老师的文章:https://gitee.com/mengning997/se/blob/master/README.md
一、环境的搭建
想跑通menu项目,需要在VSCode 中配置好C++环境,因为VScode不带C++的编译器。
首先从官网下载MinGW-w64,安装过程很容易,一直next即可。安装完毕之后通过cmd执行gcc -v查看安装是否成功:
安装完成之后打开VScode,在扩展部分添加C/C++插件,如图:
生成的task.json文件
{ "version": "2.0.0", "command": "g++", "args": [ "-g", "${file}", "-o", "${fileBasenameNoExtension}.exe" ], // 编译命令参数 "problemMatcher": { "owner": "cpp", "fileLocation": [ "relative", "${workspaceFolder}" ], "pattern": { "regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$", "file": 1, "line": 2, "column": 3, "severity": 4, "message": 5 } } }生成的launch.json文件
{ "version": "0.2.0", "configurations": [ { "name": "(gdb) Launch", // 配置名称,将会在启动配置的下拉菜单中显示 "type": "cppdbg", // 配置类型,这里只能为cppdbg "request": "launch", // 请求配置类型,可以为launch(启动)或attach(附加) "program": "${workspaceFolder}/${fileBasenameNoExtension}.exe", // 将要进行调试的程序的路径 "args": [], // 程序调试时传递给程序的命令行参数,一般设为空即可 "stopAtEntry": false, // 设为true时程序将暂停在程序入口处,一般设置为false "cwd": "${workspaceFolder}", // 调试程序时的工作目录,一般为${workspaceRoot}即代码所在目录 workspaceRoot已被弃用,现改为workspaceFolder "environment": [], "externalConsole": false, // 调试时是否显示控制台窗口,一般设置为true显示控制台 "MIMode": "gdb", "miDebuggerPath": "D:/GoogleDownload/MinGW/bin/gdb.exe", // miDebugger的路径,注意这里要与MinGw的路径对应 "preLaunchTask": "g++", // 调试会话开始前执行的任务,一般为编译程序,c++为g++, c为gcc "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": false } ] } ] }
至此,环境配置成功,可以运行menu小程序。
二、 软件工程一般原理分析
1、模块化设计
模块化设计,简单地说就是程序的编写不是开始就逐条录入计算机语句和指令,而是首先用主程序、子程序、子过程等框架把软件的主要结构和流程描述出来,并定义和调试好各个框架之间的输入、输出链接关系。逐步求精的结果是得到一系列以功能块为单位的算法描述。以功能块为单位进行程序设计,实现其求解算法的方法称为模块化。模块化的目的是为了降低程序复杂度,使程序设计、调试和维护等操作简单化。改变某个子功能只需相应改变相应模块即可。
模块化设计可以降低系统中的耦合度,可以进行更好的扩展和可重用。
在menu项目中,将控制逻辑和业务逻辑进行分开处理,在逻辑上进行划分。同时将接口的声明和实现放在不同的文件中,一个模块做一个模块的事情:
通过观察,不难看出menu项目中有Linktable模块,Menu模块,Test模块,其中LinkTable模块实现了对menu的各种操作,如增删改链表功能,我们对链表操作的函数无需关注细节,直接调用就好,代码易于理解,并且如果新增功能(如加上查找链表的功能),直接调用函数即可,无需重写代码。
linktable.h文件声明接口,linktable.c文件实现接口,要使用的时候只需要调用接口即可:
linktable.h文件声明接口
typedef struct DataNode { char* cmd; char* desc; int (*handler)(); struct DataNode *next; } tDataNode; /* find a cmd in the linklist and return the datanode pointer */ tDataNode* FindCmd(tDataNode * head, char * cmd); /* show all cmd in listlist */ int ShowAllCmd(tDataNode * head);
linktable.c文件实现接口
tDataNode* FindCmd(tDataNode * head, char * cmd) { if(head == NULL || cmd == NULL) { return NULL; } tDataNode *p = head; while(p != NULL) { if(!strcmp(p->cmd, cmd)) { return p; } p = p->next; } return NULL; } int ShowAllCmd(tDataNode * head) { printf("Menu List:\n"); tDataNode *p = head; while(p != NULL) { printf("%s - %s\n", p->cmd, p->desc); p = p->next; } return 0; }
menu模块并不需要知道LinkTable内方法的具体实现过程,只需要关注自身方法的具体实现,需要时调用LinkTable模块即可,这就是模块化思想的好处。
2、可重用接口
讨论完了模块化设计后,我们来讨论一下模块间的接口设计。
所谓接口设计,就是要设计一个好的接口,那什么才算是好的接口呢,我认为除了接口声明简洁明了外,就是接口是否可以重复利用了。在编程实践中,我们往往把多次重复利用的代码封装成一个函数,这样可以使代码层次清晰。然而,这样写的代码离可重用还是有相当的距离,因为每次调用函数都是执行相同的代码逻辑,如果业务逻辑发生了变动,相应的函数就可能会失效。真正的可重用,就是拿来代码直接就用,涉及到修改代码的统统都不是真正的可重用。还是以linktable模块为例:
linktable.h文件
LinktableNode结构体只保留了最基本的遍历功能,具体的data数据并没有包含,这是因为用户可以自己添加所需要的数据
而linktable.h这个通用接口只需要实现最基本的遍历功能即可,无需关心数据,只需关心遍历这一个逻辑,这样就使接口更通用,可重用性更高。
typedef struct LinkTableNode { struct LinkTableNode * pNext; }tLinkTableNode; /* * LinkTable Type */ typedef struct LinkTable { tLinkTableNode *pHead; tLinkTableNode *pTail; int SumOfNode; pthread_mutex_t mutex; }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); /* * get LinkTableHead */ tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable); /* * get next LinkTableNode */ tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
主函数调用更一般的接口来实现
main() { InitMenuData(&head); /* cmd line begins */ while(1) { char cmd[CMD_MAX_LEN]; printf("Input a cmd number > "); scanf("%s", cmd); tDataNode *p = FindCmd(head, cmd); if( p == NULL) { printf("This is a wrong cmd!\n "); continue; } printf("%s - %s\n", p->cmd, p->desc); if(p->handler != NULL) { p->handler(); } } }
为了更加通用,可以修改cmd数组,使其变为局部变量,同时增加一个args参数
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args); int SearchCondition(tLinkTableNode * pLinkTableNode, void * args) { char * cmd = (char*) args; tDataNode * pNode = (tDataNode *)pLinkTableNode; if(strcmp(pNode->cmd, cmd) == 0) { return SUCCESS; } return FAILURE; }
3、线程安全
线程安全:多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
需要考虑线程安全的情况:访问共享的变量或资源, 会有并发风险, 比如对象的属性, 静态变量, 共享缓存, 数据库等;所有依赖时序的操作, 即使每一步操作都是线程安全的, 还是存在并发的问题;不同的数据之间存在绑定关系的时候。例如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;
}
三、总结
开发软件的时候我们应该按照软件工程的一般规律,在模块化、可重用接口的设计、以及线程安全等问题上多下文章,多留意,尽可能地提高软件开发的效率和软件的质量。
最后,再次真诚地感谢孟老师为我们讲解高级软件软件工程,听了您的课,对于以后的开发大有脾益,受益终生。
标签:思考,menu,cmd,接口,软件工程,mutex,pLinkTable,tLinkTableNode,NULL 来源: https://www.cnblogs.com/ligang-ustc/p/13953184.html