? 通过软件工程课程的学习,孟宁老师以一个菜单小程序深入浅出地讲解了代码中的软件工程。我深刻地了解到,要做好一个应用软件,不仅仅是要完成用户的需求,也要从通用性,安全性,结构性,可维护性出发,才能做出一个成功的软件。
? 本文以孟宁老师的menu小程序为分析案例。
源代码https://github.com/mengning/menu
参考资料:https://gitee.com/mengning997/se/blob/master/README.md#代码中的软件工程
? 在VS Code中支持多种语言的插件,只需要在应用商店中搜索相应的语言插件下载。
? C++拓展中并不包括C++编译器和调试器,所以需要另外这些工具,我们选择安装MinGW。安装好后,添加环境变量,在CMD中执行 gcc -v查看是否安装成功。
? 要想在VS Code上成功运行调试程序,还需要对配置文件修改,需要修改。vscode下的launch.json和task.json文件。
? 在vscode中打开hello.c文件,点击运行和调试,选择gcc.exe。这时生成了launch.json和task.json文件。
完成后,点击F5执行hello.c程序。
!(C:\Users\licb\AppData\Roaming\Typora\typora-user-images\image-20201107162556920.png)
? 模块化设计,软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。一个好的软件应该尽量做到良好的模块化设计,这种设计在易于理解程度,debug,维护方面都有更加容易的优势。
? 因此,软件设计中的模块化程度便成为了软件设计有多好的一个重要指标,一般我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度。
? 耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。
? 一般在软件设计中我们追求松散耦合。
? 内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。
? 理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性(Feather)。
? 耦合度是各个模块间的关系,松散耦合要求程序的各个模块中没有太多的联系,而是简单的关联进行相互的调用,来增加各模块的独立性。
? 内聚是单个模块中各个元素的紧密程度,开发中我们追求高内聚的模块,使得各模块专注于完成单一功能的设计。
?
? 在本次实验的源代码中,五个基本文件的简介如下:
文件名 | 文件内容 | 调用文件 |
---|---|---|
linktable.c | 定义和实现linktable的功能 | |
menu.c | 定义和实现linktable的功能 | linktable.h |
test.c | menu具体实现和页面展示 | menu.h |
? 看表可见,每一个模块中都有自己独立的功能,当要使用别的模块提供的功能接口时,通过引入相关模块的头文件即可。这么做使得整个程序的耦合度降低,内聚度提高。
? 以初始化Menu配置为例,在test.c中调用menu.c中的MenuConfig()函数,提供相关参数而并不知其内部实现细节。随后,MenuConfig函数中又调用了linktable.c中的CreateLinkTable()函数创建一个链表。每个模块各司其职,仅仅通过相互提供的接口对功能进行调用,而不必深究其中的实现细节。模块与模块间的数据独立而不影响,这是松散耦合的表现。同时,各个模块的功能高度内聚,每个模块专注于完成单一的功能操作。
test.c
int main()
{
PrintMenuOS();
SetPrompt("MenuOS>>");
MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL);
MenuConfig("quit","Quit from MenuOS",Quit);
MenuConfig("time","Show System Time",Time);
MenuConfig("time-asm","Show System Time(asm)",TimeAsm);
ExecuteMenu();
}
menu.c
if ( head == NULL)
{
head = CreateLinkTable();
……
}
linktable.c
tLinkTable * CreateLinkTable()
{
……
}
? 在项目代码中,linktable.c只是定义了链表这种数据结构的操作,并没有涉及到项目的义务逻辑中,凡是涉及到链表的使用,只需要导入linktable.h即可。
? 消费者重用是指软件开发者在项目中重用已有的一些软件模块代码,以加快项目工作进度。
? 软件开发者在重用已有的软件模块代码时一般会重点考虑如下四个关键因素:
? 1.该软件模块是否能满足项目所要求的功能;
? 2.采用该软件模块代码是否比从头构建一个需要更少的工作量,包括构建软件模块和集成软件模块等相关的工作;
? 3.该软件模块是否有完善的文档说明;
? 4.该软件模块是否有完整的测试及修订记录;
? 我们清楚了消费者重用时考虑的因素,那么生产者在进行可重用软件设计时需要重点考虑的因素也就清楚了
? 接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。
? 接口规格是软件系统的开发者正确使用一个软件模块需要知道的所有信息,一般来说,接口规格包含五个基本要素:
? 接口的目的;
? 接口使用前所需要满足的条件,一般称为前置条件或假定条件;
? 使用接口的双方遵守的协议规范;
? 接口使用之后的效果,一般称为后置条件;
? 接口所隐含的质量属性。
? 我们将重点介绍两种函数接口方式,即Call-in方式的函数接口和Callback方式的函数接口。
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;
}
在linktabl.c中,SearchLinkTableNode函数作为call-in方式函数,conditon函数作为参数,也就是callback函数。
menu.c在调用这个函数时,传递的实参是SearchConditon函数,而SearchConditon是如下定义的。
tDataNode *p = (tDataNode*)SearchLinkTableNode(head,SearchConditon,(void*)argv[0]);
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函数交还给数据结构层操作就行了。
? 可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。
? 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
以linktable.c中的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;
}
? 删除节点操作的代码是执行在互斥锁之间的,这样子当别的进程进入时,就会因为互斥锁而进行等待,从而不会造成删除同一节点的线程不安全问题。
? 但是在最后对删除节点的空间进行释放的时候,free(pLinkTable)。由于没有增加互斥锁,当多个线程执行时,就有可能会发生同时free同一地址空间的问题,这时线程不安全的。
? 综上所示,当多个进程访问同一临界区并进行操作时,就可能发生线程不安全问题,当处于这种情况时,我们要对临界区操作代码上锁,只允许单线程的操作。
? 阅读完menu小程序的源代码后,我对软件工程有了更深的理解。在代码构建之始,我们就要对接口的设计,线程安全等问题考虑其中,便于后续我们的调试和维护。
原文:https://www.cnblogs.com/richbiao/p/13944255.html