代码中的软件工程

以VS Code + GCC工具集为主要环境编译调试课程项目案例https://github.com/mengning/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

 

一、Windows下在vscode中配置c++

1、下载mingw64并配置

mingw64是c/c++得编译器,下载安装完成后配置环境变量,在系统环境变量Path中添加新的变量值,即mingw的安装目录下的bin文件夹路径,如d:\mingw\bin

2、下载vscode

直接官网下载

3、配置vscode的相关设置

 

 

在extensions中下载c/c++ tools插件,新建一个cpp文件,点击左侧Run,自动生成lanuch.json配置文件

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "g++.exe - 生成和调试活动文件",
            "type": "cppdbg",
            "request": "launch",
            "program": "${fileDirname}\\${fileBasenameNoExtension}.exe",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": true,
            "MIMode": "gdb",
            "miDebuggerPath": "D:\\Program Files\\mingw64\\bin\\gdb.exe",
            "setupCommands": [
                {
                    "description": "为 gdb 启用整齐打印",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            "preLaunchTask": "task g++"
        }
    ]
}

修改如下内容:

"externalConsole": true
"preLaunchTask": "task g++"
回到cpp文件中,按f5进入调试,软件提示“找不到 task g++”,后生成文件task.json
{
    "tasks": [
        {
            "type": "shell",
            "label": "task g++",
            "command": "D:\\Program Files\\mingw64\\bin\\g++.exe",
            "args": [
                "-g",
                "${file}",
                "-o",
                "${fileDirname}\\${fileBasenameNoExtension}.exe"
            ],
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ],
    "version": "2.0.0"
}
修改如下内容:
"label": "task g++"
再次回到cpp文件中,按f5进入调试,c++程序运行成功
 
二、模块化设计
 
所谓的模块化设计,简单地说就是将产品的某些要素组合在一起,构成一个具有特定功能的子系统,将这个子系统作为通用性的模块与其他产品要素进行多种组合,构成新的系统,产生多种不同功能或相同功能、不同性能的系列产品。一方面可以缩短产品研发与制造周期,增加产品系列,提高产品质量,快速应对市场变化;另一方面,可以减少或消除对环境的不利影响,方便重用、升级、维修和产品废弃后的处理。
在软件工程中的模块化设计便是将一个将为庞大复杂的程序,拆分成多个部分,彼此之间相关性不大,但各部分本身内容很集中,即实现高内聚低耦合。耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。 一般在软件设计中我们追求松散耦合。
内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。 理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性(Feather)

模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离 (SoC, Separation of Concerns),是由软件工程领域的奠基性人物Edsger Wybe Dijkstra(1930~2002)在1974年提出,没错就是Dijkstra最短路径算法的作者。 关注点的分离在软件工程领域是最重要的原则,我们习惯上称为模块化,翻译成我们中文的表述其实就是“分而治之”的方法。 关注点的分离的思想背后的根源是由于人脑处理复杂问题时容易出错,把复杂问题分解成一个个简单问题,从而减少出错的情形。

在孟老师的实验lab3.2和lab3.3中充分表达了什么是模块化
lab3.2中,仅有一个menu.c文件,所有的代码逻辑和变量申明全都包括其中,非常的混乱,可读性较差。

int Help();

#define CMD_MAX_LEN 128
#define DESC_LEN 1024
#define CMD_NUM 10

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;
}

static tDataNode head[] =
{
{"help", "this is help cmd!", Help,&head[1]},
{"version", "menu program v1.0", NULL, NULL}
};

int main()

{
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;
}

但是在lab3.3中有着3个文件menu.c,linklist.c,linklist.h。
lab3.3将lab3.2的内容拆分
在linklist.h中仅存放程序所需的数据结构,即一个链表结构

typedef struct DataNode
{
char* cmd;
char* desc;
int (*handler)();
struct DataNode *next;
} tDataNode;

tDataNode* FindCmd(tDataNode * head, char * cmd);
int ShowAllCmd(tDataNode * head);

在linklist.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.c中存放的是主程序逻辑

int Help();

#define CMD_MAX_LEN 128
#define DESC_LEN 1024
#define CMD_NUM 10

static tDataNode head[] =
{
{"help", "this is help cmd!", Help,&head[1]},
{"version", "menu program v1.0", NULL, NULL}
};

main()
{
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;
}

代码的拆分最直观的好处便是可读性提升,其次便是便于对代码的管理,后期维护和修改。
 
三、可重用接口
 
1、接口的概念

尽管已经做了初步的模块化设计,但是分离出来的数据结构和它的操作还有很多菜单业务上的痕迹,我们要求这一个软件模块只做一件事,也就是功能内聚,那就要让它做好链表数据结构和对链表的操作,不应该涉及菜单业务功能上的东西;同样我们希望这一个软件模块与其他软件模块之间松散耦合,就需要定义简洁、清晰、明确的接口。 这时进一步优化这个初步的模块化代码就需要设计合适的接口。定义接口看起来是个很专业的事情,其实在我们生活中无处不在,比如我们看的电视剧中“天王盖地虎,宝塔镇河妖”就是黑社会接头定义的接口,比如两个人对话交流沟通使用的就是汉语普通话或标准英语这么一个接口规范。

接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。 在面向过程的编程中,接口一般定义了数据结构及操作这些数据结构的函数;而在面向对象的编程中,接口是对象对外开放(public)的一组属性和方法的集合。函数或方法具体包括名称、参数和返回值等。

接口规格是软件系统的开发者正确使用一个软件模块需要知道的所有信息,那么这个软件模块的接口规格定义就必须清晰明确地说明正确使用本软件模块的信息。一般来说,接口规格包含五个基本要素: 接口的目的; 接口使用前所需要满足的条件,一般称为前置条件或假定条件; 使用接口的双方遵守的协议规范; 接口使用之后的效果,一般称为后置条件; 接口所隐含的质量属性。

2、可重用接口

将通用的Linktable模块集成到我们的menu程序中:

通用Linktable模块的接口参见linktable.h,对应的实现代码linktable.c 有关Linktable模块的写法可以参考menu4-1.mp4。在使用通用的Linktable模块之后menu程序业务代码变得复杂了一些,使用起来比较繁琐,这部分代码的写法参考menu4-2.mp4。

#ifndef _LINK_TABLE_H_
#define _LINK_TABLE_H_

#include <pthread.h>

#define SUCCESS 0
#define FAILURE (-1)

typedef struct LinkTableNode
{
struct LinkTableNode * pNext;
}tLinkTableNode;

typedef struct LinkTable
{
tLinkTableNode *pHead;
tLinkTableNode *pTail;
int SumOfNode;
pthread_mutex_t mutex;
}tLinkTable;

tLinkTable * CreateLinkTable();
int DeleteLinkTable(tLinkTable *pLinkTable);
int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);
tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);

#endif /* _LINK_TABLE_H_ */

给Linktable增加Callback方式的接口:

给Linktable增加Callback方式的接口,需要两个函数接口,一个是call-in方式函数,如SearchLinkTableNode函数,其中有一个函数作为参数,这个作为参数的函数就是callback函数,如代码中Conditon函数。 这一部分代码的编写可以参考menu5-1.mp4。callback接口方式非常像谍战剧里派遣卧底,这里SearchLinkTableNode函数派遣了一个卧底Conditon并指定了卧底负责收集的情报范围tLinkTableNode * pNode,一旦发现目标情报卧底就被激活return pNode。

利用callback函数参数使Linktable的查询接口更加通用:

前面的方式中用户程序定义了卧底SearchCondition,这个卧底函数需要向基地查询目标信息,也就是使用了全局变量cmd。与现实世界的情况类似这样会大大增加卧底暴露的风险,为了降低风险增加了args参数,这样在派遣卧底的同时指定了目标情报的内容,卧底在行动过程中就不需要和基地建立联系,只有在搜集到目标情报args时才向基地报道完成任务。利用callback函数参数使Linktable的查询接口更加通用,有效地提高了接口的通用性。 我们还通过将linktable.h中不是在接口调用时必须内容转移到linktable.c中,这样可以有效地隐藏软件模块内部的实现细节,为外部调用接口的开发者提供更加简洁的接口信息,同时也减少外部调用接口的开发者有意或无意的破坏软件模块的内部数据。通过接口进行信息隐藏已经成为面向对象编程语言的标准做法,使用public和private来声明属性和方法对于外部调用接口的开发者是否可见。 这一部分代码的编写可以参考menu5-2.mp4。

 

tLinkTable * CreateLinkTable()
{
tLinkTable * pLinkTable = (tLinkTable *)malloc(sizeof(tLinkTable));
if(pLinkTable == NULL)
{
return NULL;
}
pLinkTable->pHead = NULL;
pLinkTable->pTail = NULL;
pLinkTable->SumOfNode = 0;
pthread_mutex_init(&(pLinkTable->mutex), NULL);
return pLinkTable;
}
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;
}
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;
}
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;
}

tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable)
{
if(pLinkTable == NULL)
{
return NULL;
}
return pLinkTable->pHead;
}

tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode)
{
if(pLinkTable == NULL || pNode == NULL)
{
return NULL;
}
tLinkTableNode * pTempNode = pLinkTable->pHead;
while(pTempNode != NULL)
{
if(pTempNode == pNode)
{
return pTempNode->pNext;
}
pTempNode = pTempNode->pNext;
}
return NULL;
}

四、可重入函数与线程安全

1、线程的基本概念

线程(thread)是操作系统能够进行运算调度的最小单位。它包含在进程之中,是进程中的实际运作单位。一个线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。一般默认一个进程中只包含一个线程。

2、可重入函数及基本要求

可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用局部变量,要么在使用全局变量时保护自己的数据。

不为连续的调用持有静态数据; 不返回指向静态数据的指针; 所有数据都由函数的调用者提供; 使用局部变量,或者通过制作全局数据的局部变量拷贝来保护全局数据; 使用静态数据或全局变量时做周密的并行时序分析,通过临界区互斥避免临界区冲突; 绝不调用任何不可重入函数。

3、线程安全

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。 线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

4、可重入函数和线程安全的关系

可重入的函数不一定是线程安全的,可能是线程安全的也可能不是线程安全的;可重入的函数在多个线程中并发使用时是线程安全的,但不同的可重入函数(共享全局变量及静态变量)在多个线程中并发使用时会有线程安全问题; 不可重入的函数一定不是线程安全的。

5、menu子系统的可重用接口设计

menu作为一个子系统会用在不同项目中,如何给menu子系统设计可重用的接口呢?学习了Linktable链表的通用接口定义的方法之后,我们往往会参照执行,这是经常犯的典型错误——“手里有把锤子,看哪里都是钉子”,menu子系统不像Linktable链表,Linktable链表是一个非常基础的软件模块,重用的机会非常多,应用的场景也非常多,而menu子系统的重用机会和应用场景都比较有限,我们没有必要花非常多的心思把接口定义的太通用,通用往往意味着接口的使用不够直接明了。所以在menu子系统的接口设计我们的原则是“够用就好——不要太具体,也不要太通用”。如下代码是我们在menu.h中定义的两个接口,一个是通过给出命令的名称、描述和命令的实现函数定义一个命令;另一个是启动menu引擎。

/* add cmd to menu */

int MenuConfig(char * cmd, char * desc, int (*handler)());

/* Menu Engine Execute */ int ExecuteMenu()

 

posted @ 2020-11-04 18:17  汪鹏  阅读(229)  评论(0)    收藏  举报