代码中的软件工程-menu项目
这次学习主要以VS Code + GCC工具集为主要环境编译,结合孟宁老师给的课程项目案例https://github.com/mengning/menu ,完成编译和调试环境配置,并对模块化设计、可重用接口、线程安全等结合代码进行理解和分析。
参考资料见:https://gitee.com/mengning997/se/blob/master/README.md#代码中的软件工程
1.配置编译和调试环境
首先进入vscode,安装C/C++插件
然后在mac终端查看是否已经安装gcc编译器,如果没有安装可以通过homebrew进行安装。可以看到系统默认使用的是clang。
关于GCC、LLVM以及Clang三种编译器的区别可以参考https://blog.csdn.net/vencentle/article/details/80269501。
确认已经安装好gcc后再进行环境测试。用vscode打开menu-master目录,然后执行make all指令,对所有.c文件进行编译生成目标.o文件,然后执行test。
进入MenuOS后,可以通过help命令查看可以使用的命令,并对这些可使用命令进行测试。
2.源码分析
vscode是一个轻量级的文本编辑器,但是它的拓展插件可以让他拓展成功能齐全的IDE,这其中就靠的是tasks.json和launch.json的配置。根据我的理解,tasks是用在launch前执行的任务,launch是读取执行文件。在当前文件是C++的情况下,tasks可以被用来做编译,而launch用来执行编译好的文件。关于tasks.json和launch.json的配置可以参考https://zhuanlan.zhihu.com/p/92175757
(1)模块化设计
所谓的模块化设计,简单地说就是将产品的某些要素组合在一起,构成一个具有特定功能的子系统,将这个子系统作为通用性的模块,可以与其他产品或要素进行多种组合,产生不同功能或应用的产品。简单概括就是要像组装积木一样组合出不同产品。让各模块之间“高内聚,低耦合”,降低代码复杂性,增加重用性、可扩展性,提高开发效率。
遵循KISS(Keep It Simple & Stupid)原则:
- 一个软件模块只做一件事
- 一个函数只做一件事
- 一个代码块只做一件事
- 一行代码只做一件事
例如menu.c中的结构体tDataNode,一个结构体它可以保存一条命令及其操作
typedef struct DataNode
{
char* cmd; //保存有命令字符,与输入的字符进行比较查询是否是此命令
char* desc; //该命令的补充介绍或是要输出的信息
int (*handler)(); //该命令对应的操作
struct DataNode *next; //指想下一个命令节点
} tDataNode;
该项目主要有下面几个主要的c文件:test.c、linktable.c、menu.c等。打开各个c文件后不难发现。我们可以看出在linktable.c中关于linktable相关操作的代码逻辑,如获得该节点、获得下个节点的方法实现,在menu.c中是在进行逻辑层的操作,最后test.c则是通过测试,将运行结果展示给用户。
linktable.c
/*
* Search a LinkTableNode from LinkTable
* int Conditon(tLinkTableNode * pNode);
*/
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args)
{
if(pLinkTable == NULL || Conditon == NULL)
{
return NULL;
}
tLinkTableNode * pNode = pLinkTable->pHead;
while(pNode != NULL)
{
if(Conditon(pNode,args) == SUCCESS)
{
return pNode;
}
pNode = pNode->pNext;
}
return NULL;
}
/*
* get LinkTableHead
*/
tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable)
{
if(pLinkTable == NULL)
{
return NULL;
}
return pLinkTable->pHead;
}
menu.c
/* add cmd to menu */
int MenuConfig(char * cmd, char * desc, int (*handler)())
{
tDataNode* pNode = NULL;
if ( head == NULL)
{
head = CreateLinkTable();
pNode = (tDataNode*)malloc(sizeof(tDataNode));
pNode->cmd = "help";
pNode->desc = "Menu List";
pNode->handler = Help;
AddLinkTableNode(head,(tLinkTableNode *)pNode);
}
pNode = (tDataNode*)malloc(sizeof(tDataNode));
pNode->cmd = cmd;
pNode->desc = desc;
pNode->handler = handler;
AddLinkTableNode(head,(tLinkTableNode *)pNode);
return 0;
}
当代码模块化之后,就需要接口来实现模块之间的相互调用。
(2)可重用接口
上一部分说的模块化设计中,很大程度上就已经利用了接口来实现一些模块之间的调用关系,上述模块化设计中已经分离出来了menu的数据结构和其操作,但是分离出来的数据结构和它的操作还有很多菜单业务上的痕迹,这个时候我们就要让模块的内聚度提高,这样才能保证一个模块所作的事情就是这个模块要做的,不涉及其他模块的干预,这个时候我们就需要定义简洁、清晰、明确的接口。
下面来学习一些理论:
-接口规格包含五个基本要素:
-
接口的目的;
-
接口使用前所需要满足的条件,一般称为前置条件或假定条件;
-
使用接口的双方遵守的协议规范;
-
接口使用之后的效果,一般称为后置条件;
-
接口所隐含的质量属性。
对于可重用的设计有两个方向:
消费者重用是指软件开发者在项目中重用已有的一些软件模块代码,以加快项目工作进度。软件开发者在重用已有的软件模块代码时一般会重点考虑如下四个关键因素:
-
该软件模块是否能满足项目所要求的功能;
-
采用该软件模块代码是否比从头构建一个需要更少的工作量,包括构建软件模块和集成软件模块等相关的工作;
-
该软件模块是否有完善的文档说明;
-
该软件模块是否有完整的测试及修订记录;
-
我们清楚了消费者重用时考虑的因素,那么生产者在进行可重用软件设计时需要重点考虑的因素也就清楚了,但是除此之外还有一些事项在进行可重用软件设计时牢记在心,我们简要列举如下:
通用的模块才有更多重用的机会;给软件模块设计通用的接口,并对接口进行清晰完善的定义描述;记录下发现的缺陷及修订缺陷的情况;使用清晰一致的命名规则;对用到的数据结构和算法要给出清晰的文档描述;与外部的参数传递及错误处理部分要单独存放易于修改。
例如在linktable.c中,各个接口对应的操作的实现也是都在自己的函数内部来进行实现,不涉及到其他模块的耦合以及内部调用等,这也就保证了在调用该接口时,不需要太关注其他的模块的限定条件,也是我们使用起来较为方便。
/*
* 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);
/*
* 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.h接口文件,就实现了软件的可重用设计,为了进一步实现软件的可重用设计,我们添加一个callback方式的接口使Linktable的查询接口更加通用。给Linktable增加Callback方式的接口,需要两个函数接口,一个是call-in方式函数,如SearchLinkTableNode函数,其中有一个函数作为参数,这个作为参数的函数就是callback函数,如代码中Conditon函数。
SearchLinkTableNode函数:
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args)
{
if(pLinkTable == NULL || Conditon == NULL)
{
return NULL;
}
tLinkTableNode * pNode = pLinkTable->pHead;
while(pNode != NULL)
{
if(Conditon(pNode,args) == SUCCESS)
{
return pNode;
}
pNode = pNode->pNext;
}
return NULL;
}
可以看到通过Condition判断之后才返回节点。
在menu.c中可以看到在ExecuteMenu函数中使用SearchLinkTableNode接口.
tDataNode *p = (tDataNode*)SearchLinkTableNode(head,SearchConditon,(void*)argv[0]);
//SearchConditon的功能就是比较参数的值和该节点存储的命令是否相等
int SearchConditon(tLinkTableNode * pLinkTableNode,void * arg)
{
char * cmd = (char*)arg;
tDataNode * pNode = (tDataNode *)pLinkTableNode;
if(strcmp(pNode->cmd, cmd) == 0)
{
return SUCCESS;
}
return FAILURE;
}
通过callback函数,接口赋予了使用它的人极大的自由并且扩张了接口的适用性。
(3)线程安全
线程安全是指如果代码的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
在linktable.c文件中结构体定义。
/*
* LinkTable Type
*/
struct LinkTable
{
tLinkTableNode *pHead;
tLinkTableNode *pTail;
int SumOfNode;
pthread_mutex_t mutex; // 引入了mutex,保证线程安全
};
又比如对链表的增加或删除操作,只要会修改链表,都会使用加锁解锁保证互斥访问。
/*
* Delete a LinkTable
*/
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;
}
/*
* Add a LinkTableNode to LinkTable
*/
int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode)
{
if(pLinkTable == NULL || pNode == NULL)
{
return FAILURE;
}
pNode->pNext = NULL;
pthread_mutex_lock(&(pLinkTable->mutex)); // 加锁
if(pLinkTable->pHead == NULL)
{
pLinkTable->pHead = pNode;
}
if(pLinkTable->pTail == NULL)
{
pLinkTable->pTail = pNode;
}
else
{
pLinkTable->pTail->pNext = pNode;
pLinkTable->pTail = pNode;
}
pLinkTable->SumOfNode += 1 ;
pthread_mutex_unlock(&(pLinkTable->mutex)); // 解锁
return SUCCESS;
}
/*
* Delete a LinkTableNode from LinkTable
*/
int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode)
{
if(pLinkTable == NULL || pNode == NULL)
{
return FAILURE;
}
pthread_mutex_lock(&(pLinkTable->mutex)); // 加锁
if(pLinkTable->pHead == pNode)
{
pLinkTable->pHead = pLinkTable->pHead->pNext;
pLinkTable->SumOfNode -= 1 ;
if(pLinkTable->SumOfNode == 0)
{
pLinkTable->pTail = NULL;
}
pthread_mutex_unlock(&(pLinkTable->mutex)); // 解锁
return SUCCESS;
}
tLinkTableNode * pTempNode = pLinkTable->pHead;
while(pTempNode != NULL)
{
if(pTempNode->pNext == pNode)
{
pTempNode->pNext = pTempNode->pNext->pNext;
pLinkTable->SumOfNode -= 1 ;
if(pLinkTable->SumOfNode == 0)
{
pLinkTable->pTail = NULL;
}
pthread_mutex_unlock(&(pLinkTable->mutex)); // 解锁
return SUCCESS;
}
pTempNode = pTempNode->pNext;
}
pthread_mutex_unlock(&(pLinkTable->mutex));
return FAILURE;
}