最近孟老师在课上讲解了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文件
生成的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文件通过给链表的增删操作加了一个互斥锁,实现线程安全。
三、总结
开发软件的时候我们应该按照软件工程的一般规律,在模块化、可重用接口的设计、以及线程安全等问题上多下文章,多留意,尽可能地提高软件开发的效率和软件的质量。
最后,再次真诚地感谢孟老师为我们讲解高级软件软件工程,听了您的课,对于以后的开发大有脾益,受益终生。
原文:https://www.cnblogs.com/ligang-ustc/p/13953184.html