基于Menu学软件工程
通过高级软件工程孟宁老师关于Menu项目的讲解,使得我对项目的开发过程有了一定的了解和感悟。本文基于Menu项目根据课上老师讲解以及自己的分析展开,着重体现项目中所蕴涵的软件工程思想。
参考文献:代码中的软件工程
(一)环境配置
1、首先下载VSCode提供的C/C++扩展包
2、下载MinGW并配置环境详情参考如下博文:
https://blog.csdn.net/wxh0000mm/article/details/100666329
3、配置C++环境,分别创建launch.json和tasks.json文件。
"version": "0.2.0", "configurations": [ { "name": "(gdb) 启动", "type": "cppdbg", "request": "launch", "program": "${fileDirname}\\${fileBasenameNoExtension}.exe", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": true, "MIMode": "gdb", "miDebuggerPath": "D:\\MinGW\\mingw64\\bin\\gdb.exe", "setupCommands": [ { "description": "为 gdb 启用整齐打印", "text": "-enable-pretty-printing", "ignoreFailures": true } ],"preLaunchTask": "task g++" } ] }
{ "version": "2.0.0", "tasks": [ { "type": "shell", "label": "task g++", "command": "D:\\MinGW\\mingw64\\bin\\g++.exe", "args": [ "-g", "${file}", "-o", "${fileDirname}\\${fileBasenameNoExtension}.exe" ], "options": { "cwd": "D:\\MinGW\\mingw64\\bin" }, "problemMatcher": [ "$gcc" ], "group": "build" } ] }
至此,环境配置完毕。
(二)Menu项目的开发进程的分析
se_code文件夹下对应lab1-lab7.2,对应项目开发的各种版本。下面分析每个版本的内容以及使用到的软件工程方法。
1、lab1中的menu.c文件是菜单的简要说明。
2、lab2中的menu.c文件对功能进行了扩展,使得能够不断处理不同的命令。
3、lab3体现了软件工程中模块化设计的方法。
每新增一个功能,只要在函数链表上新增一个结点即可,体现了开闭原则--软件实体对扩展是开放的,但对修改是关闭的,即在不修改一个软件实体的基础上去扩展其功能。
将数据结构的定义和功能实现分别放在.h和.c文件中,符合模块化设计思想。
4、lab4设计了linktable模块,用于用户进行自定义操作。
5、lab5.1中设计了可重用接口,定义了一个SearchCondition函数,接着就在FindCmd函数中调用了SearchLinkTableNode函数,
在lab5.2中将全局变量改为参数传入,使FindCmd和SearchCondition两个模块耦合性降低
6、lab6进行线程安全分析,对于链表节点进行写操作的时候,需要对临界区进行加锁,写完毕后进行解锁操作。而对于链表节点进行读操作时,表面看不会影响链表的结构。但是当某个线程进行读操作时,可能有另外的线程进行写操作,若进行写操作时对链表进行了增加或者删除,那么读链表的线程可能会出现不一致的情况。如图所示,删除节点时需要进行加锁和解锁操作。
7、 lab7给Menu定义一系列可重用接口。下图是menu子系统的接口函数:MenuConfig函数用来配置菜单的命令名字、命令描述和命令操作函数,而ExecuteMenu函数用来执行函数。
(三)软件工程思想分析
1、模块化设计
模块化设计,简单地说就是程序的编写不是开始就逐条录入计算机语句和指令,而是首先用主程序、子程序、子过程等框架把软件的主要结构和流程描述出来,并定义和调试好各个框架之间的输入、输出链接关系。逐步求精的结果是得到一系列以功能块为单位的算法描述。以功能块为单位进行程序设计,实现其求解算法的方法称为模块化。模块化的目的是为了降低程序复杂度,使程序设计、调试和维护等操作简单化。改变某个子功能只需相应改变相应模块即可。
(1) 扩展新功能时,不需要修改项目的主函数。
static tDataNode head[] = { {"help", "this is help cmd!", Help,&head[1]}, {"version", "menu program v1.0", NULL, &head[2]}, {"quit", "Quit from menu", Quit, NULL} };
(2) 将数据结构的定义和功能实现分别放在.h和.c文件中。
//linklist.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);
#include <stdio.h> #include <stdlib.h> #include "linklist.h" 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; }
2、可重用接口
模块化设计后需要对接口进行细化,一旦一个接口太大,则需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。使用多个专门的接口,而不使用单一的总接口。每一个接口应该承担一种相对独立的角色必须满足单一职责原则,将一组相关的操作定义在一个接口中,且在满足高内聚的前提下,接口中的方法越少越好。
接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。在面向过程的编程中,接口一般定义了数据结构及操作这些数据结构的函数;而在面向对象的编程中,接口是对象对外开放的一组属性和方法的集合。
(1) lab5.1可重用接口设计
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode)) { if(pLinkTable == NULL || Conditon == NULL) { return NULL; } tLinkTableNode * pNode = pLinkTable->pHead; while(pNode != pLinkTable->pTail) { if(Conditon(pNode) == SUCCESS) { return pNode; } pNode = pNode->pNext; } return NULL; }
(2) menu子系统的接口
/* add cmd to menu */ int MenuConfig(char * cmd, char * desc, int (*handler)()); /* Menu Engine Execute */ int ExecuteMenu();
3、线程安全
可重入函数和不可重入函数是线程安全相关的重要的概念,可重入函数可以由多于一个任务并发使用,可以在任意时刻被中断,稍后再继续运行,不会丢失数据。相反,不可重入函数不能由超过一个任务所共享,除非能确保函数的互斥。
线程安全就是如果代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
函数的可重入性和线程安全之间的关系:可重入的函数不一定是线程安全的,可能是线程安全的也可能不是线程安全的;可重入的函数在多个线程中并发使用时是线程安全的,但不同的可重入函数(共享全局变量及静态变量)在多个线程中并发使用时会有线程安全问题; 不可重入的函数一定不是线程安全的。
(1)linktable在结构体中定义线程安全的互斥锁
struct LinkTable { tLinkTableNode *pHead; tLinkTableNode *pTail; int SumOfNode; pthread_mutex_t mutex; //用于线程安全的互斥锁 };
(2)DeleteLinkTable删除节点前后需要进行加锁和解锁。
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的开发过程,利用软件工程思想分析,深刻理解了模块化设计、可重用接口、线程安全等内容。模块化设计的方法能够使得项目各个模块结构更加清晰,提高项目的可维护性。可重用接口降低了软件模块和软件模块之间的耦合度,能够实现可维护性复用;线程安全让我们的程序可以并发执行却不会发生数据错误。同时通过项目的学习,了解到编程规范对于项目的协同合作起到很重要的作用。不仅能使得代码风格更加规范,同时进行合理的注释有利于推动项目的开发。相信在以后的软件项目开发中,孟老师所传达的软件工程思想能够很好的为我所用。