从menu项目理解软件工程
虽然软件工程这个词条我们经常会听到,但想要通过字面去理解其思想往往是比较抽象晦涩的,所以我们要通过一些项目案例去仔细阅读分析源代码,结合代码分析其中的软件工程方法、规范或软件工程思想。这样才能让我们更加精确的理解软件工程的思想。
参考资料见:https://gitee.com/mengning997/se/blob/master/README.md#%E4%BB%A3%E7%A0%81%E4%B8%AD%E7%9A%84%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B
实验环境:macOs,vscode,gcc
1:环境配置
1、用VSCode,在扩展部分安装C/C++插件:

2、在终端安装homebrew和gcc

3、测试环境配置

2:模块化设计
1、模块化基本原理:模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。
关注点的分离在软件工程领域是最重要的原则,我们习惯上称为模块化,翻译成我们中文的表述其实就是“分而治之”的方法。
孟老师也在课堂上讲过“写代码要小步快跑不断迭代,罗马不是一天建成的,不要期望一撮而就。”这就很好的体现了模块化已经分而治之的思想。
2、模块化设计原则:“高内聚低耦合”,内聚即是指每一个单独模块内部的各元素的依赖程度,耦合则是指各个不同的模块之间的依赖程度;我们在设计模块
时,要做到一个软件模块只做一件事,各个模块解耦合,只需设计接口用于模块之间的相互调用。
3、代码案例分析(lab3.2和lab3.3):
menu2:
#include <stdio.h>
#include <stdlib.h>
int Help();
#define CMD_MAX_LEN 128
#define DESC_LEN 1024
#define CMD_NUM 10
/* data struct and its operations */
typedef struct DataNode
{
char* cmd;
char* desc;
int (*handler)();
struct DataNode *next;
} tDataNode;
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 program */
static tDataNode head[] =
{
{"help", "this is help cmd!", Help,&head[1]},
{"version", "menu program v1.0", NULL, NULL}
};
int main()
{
/* 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();
}
}
}
int Help()
{
ShowAllCmd(head);
return 0;
}
模块化设计后的menu3:#include <stdio.h>
#include <stdlib.h>
#include "linklist.h"
int Help();
#define CMD_MAX_LEN 128
#define DESC_LEN 1024
#define CMD_NUM 10
/* menu program */
static tDataNode head[] =
{
{"help", "this is help cmd!", Help,&head[1]},
{"version", "menu program v1.0", NULL, NULL}
};
main()
{
/* 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();
}
}
}
int Help()
{
ShowAllCmd(head);
return 0;
}
这两个案例所实现的功能都一样,但是很显然menu3的代码要比menu2简洁很多,而且非常清晰。这是因为menu3进行了模块化设计,将menu3所需要调用的函数进行了模块化设计,
封装在linklist.h中,然后通过linklist.c对这些功能进行实现,这样就能通过不同模块之间的接口进行相互调用,从而实现模块之间的解耦合,每一个模块只需做一件事就好。
linklist.h和linklist.c的代码如下:
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; }
3:可重用接口设计
1、软件模块接口在面向过程的语言中一般是定义一些数据结构和函数接口API,在面向对象的编程语言中一般在类或接口类中定义一些公有的(public)属性和方法。
尽管已经做了初步的模块化设计,但是分离出来的数据结构和它的操作还有很多菜单业务上的痕迹,我们要求这一个软件模块只做一件事,也就是功能内聚,那就要让它做好链表数据结构和对链表的操作,不应该涉及菜单业务功能上的东西;同样我们希望这一个软件模块与其他软件模块之间松散耦合,就需要定义简洁、清晰、明确的接口。
如下代码块,MENU程序也设计了好的接口。
//linktable.h文件 //LinktableNode结构体只保留了最基本的结点指针,具体的data数据并没有包含,面向抽象不依赖具体 typedef struct LinkTableNode { struct LinkTableNode * pNext; }tLinkTableNode; /* * LinkTable Type */ //而inktable这个结构体则在引入头文件的情况下,嵌入了LinkTableNode结构体,并且另外定义了数据头//尾指针以及互斥量,相当于实现了遍历功能和线程保护 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);
2、通用接口总结
通用接口定义的基本方法
- 参数化上下文
- 移除前置条件
- 简化后置条件
4:线程安全
1、线程安全概念:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
2、函数的可重入性与线程安全之间的关系:可重入的函数不一定是线程安全的,可能是线程安全的也可能不是线程安全的;可重入的函数在多个线程中并发使用时是线程安全的,但不同的可重入函数(共享全局变量及静态变量)在多个线程中并发使用时会有线程安全问题;不可重入的函数一定不是线程安全的。所以我们在调用不可重入函数时,需要加锁来实现线程的同步操作,不然可能会发生不可预料的数据错误。
3、代码分析:
/* * 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; }
这里的通过上锁来实现线程的同步,即是我们操作系统所学的PV操作。
5:总结
1、模块化设计:主要是实现”高内聚低耦合“,降低各个模块之间的耦合度。
2、可重用接口设计:主要是面向接口编程,提高程序的扩展性以及多态性。
3、线程安全:主要是判断所调用的函数是否为可重入函数,如果是,则必须加锁实现这些函数的原子操作性,不能执行一半就去调用别的函数,造成数据错误。
感谢孟老师让我通过这些案例中更加深刻到体会到了软件工程的思想。
浙公网安备 33010602011771号