代码中的软件工程--以menu项目为例

一、配置环境并运行程序

我使用的的windows下的VS Code软件 + GCC集成编译环境,首先在应用商店中安装C/C++插件。

因为VS Code不带C++编译器,我们还需要安装好GCC编译器,在官网上下载好软件并安装,然后配置好环境变量。在VS Code的控制台输入gcc --version查看当前版本,如下图所示表示配置成功。

  1. 第一个程序

  在配置好环境之后,我们在VS code中运行一个第一个程序hello.c,VS Code就会自动生成lunch.json、task.json、c_cpp_properties.json三个文件,lunch.json是用来调试的配置文件,task.json是用来告诉VS Code怎么编译程序,c_cpp_properties.json是编译路径和选择智能感知。我们在控制台输入gcc -g hello.c -o hello可以将 .c 文件编译成 .exe 文件,然后输入./hello.exe执行该文件,就可以在终端输出hello world!

        2. 编译一个项目

  上面只是编译了一个 .c 文件,而在实际项目中,需要将多个 .c 文件编译成一个可执行程序,比如menu项目中存在test.c menu.c linktable.c三个文件,我们可以在控制台gcc -g test.c menu.c linktable.c -o test,将这三个文件放在一起编译,形成一个可执行程序。然后输入./test.exe执行这个程序,可以在终端看到如下图所示的效果。

  我们也可以修改下lunch.json和task.json文件达到同样的效果。将lunch.json中的program修改为"${fileDirname}\test",然后将task.json文件中"args"中的"-g"后面添加要编译的三个源文件,"-o"后面添加输出名字。然后只需要点击运行,就可以将结果输出到终端了。

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "gcc.exe - 生成和调试活动文件",
            "type": "cppdbg",
            "request": "launch",
            "program": "${fileDirname}\\test",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "miDebuggerPath": "E:\\vscode_x64\\mingw64\\mingw64\\bin\\gdb.exe",
            "setupCommands": [
                {
                    "description": "为 gdb 启用整齐打印",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            "preLaunchTask": "gcc.exe build active file"
        }
    ]
}
{
    "tasks": [
        {
            "type": "shell",
            "label": "gcc.exe build active file",
            "command": "E:\\vscode_x64\\mingw64\\mingw64\\bin\\gcc.exe",
            "args": [
                "-g",
                "${fileDirname}\\test.c",
                "${fileDirname}\\menu.c",
                "${fileDirname}\\linktable.c",
                "-o",
                "${fileDirname}\\test"
            ],
            "options": {
                "cwd": "E:\\vscode_x64\\mingw64\\mingw64\\bin"
            }
        }
    ],
    "version": "2.0.0"
}

二、menu项目中的软件工程

2.1 模块化软件设计

2.1.1 模块化软件设计的基本原理

  • 模块化是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离 。

  • 关注点的分离在软件工程领域是最重要的原则,我们习惯上称为模块化,翻译成我们中文的表述其实就是“分而治之”的方法。

  • 关注点的分离的思想背后的根源是由于人脑处理复杂问题时容易出错,把复杂问题分解成一个个简单问题,从而减少出错的情形。

2.1.2 模块化软件设计的优点

  • 模块化软件设计的方法如果应用的比较好,最终每一个软件模块都将只有一个单一的功能目标,并相对独立于其他软件模块,使得每一个软件模块都容易理解容易开发。
  • 从而整个软件系统也更容易定位软件缺陷bug,因为每一个软件缺陷bug都局限在很少的一两个软件模块内。
  • 而且整个系统的变更和维护也更容易,因为一个软件模块内的变更只影响很少的几个软件模块。
  • 因此,软件设计中的模块化程度便成为了软件设计有多好的一个重要指标,一般我们使用耦合度和内聚度来衡量软件模块化的程度。

2.1.3 耦合度和内聚度

  • 耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合、松散耦合和无耦合。在软件设计中我们追求松散耦合。

  • 内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性。

  我们来看一个例子,在lab2中,每次都将传进来字符数组cmd与"help"或"quit"比较,然后输出相应的结果。但是这段代码具有很大的局限性,这段代码只能将cmd与两个字符串进行比较,要想与更多的字符串比较就要修改代码,十分麻烦且无法对代码进行重用。

#include <stdio.h>
#include <stdlib.h>
int main()
{
    char cmd[128];
    while(1)
    {
        scanf("%s", cmd);
        if(strcmp(cmd, "help") == 0)
        {
            printf("This is help cmd!\n");
        }
        else if(strcmp(cmd, "quit") == 0)
        {
            exit(0);
        }
        else
        {
            printf("Wrong cmd!\n");
        }
    }
}

在lab3.1将要比较的字符串定义为一个链表结构,对链表中的节点定义了相关操作。我们如果想定义新的操作,只需要添加新的节点就可以了。

{
    char*   cmd;
    char*   desc;
    int     (*handler)();
    struct  DataNode *next;
} tDataNode;
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}
};

  上述代码是在主函数中对链表进行查找操作,当找到目标值时调用相应的处理函数,主函数中的业务逻辑和数据结构耦合到了一起。因此,我们可以单独定义一个在链表中查找目标值的函数,这样的话,就做到了业务逻辑和数据结构的解耦,当使用的是其他的数据结构时,我们只需要添加另外一个查找目标值的函数,而不需要修改主函数中的代码。

tDataNode* FindCmd(tLinkTable * head, char * cmd)
{
    tDataNode * pNode = (tDataNode*)GetLinkTableHead(head);
    while(pNode != NULL)
    {
        if(strcmp(pNode->cmd, cmd) == 0)
        {
            return  pNode;  
        }
        pNode = (tDataNode*)GetNextLinkTableNode(head,(tLinkTableNode *)pNode);
    }
    return NULL;
}

2.2 可重用接口

  • 尽管已经做了初步的模块化设计,但是分离出来的数据结构和它的操作还有很多菜单业务上的痕迹,我们要求这一个软件模块只做一件事,也就是功能内聚,那就要让它做好链表数据结构和对链表的操作,不应该涉及菜单业务功能上的东西;同样我们希望这一个软件模块与其他软件模块之间松散耦合,就需要定义简洁、清晰、明确的接口。

  • 接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。

  • 在面向过程的编程中,接口一般定义了数据结构及操作这些数据结构的函数;而在面向对象的编程中,接口是对象对外开放的一组属性和方法的集合。函数或方法具体包括名称、参数和返回值等。

2.2.1 我们将重点介绍两种函数接口方式,即Call-in方式的函数接口和Callback方式的函数接口。

如下函数接口代码是从链表中取出链表的头节点的函数声明,

tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);
  • 该接口的目标是从链表中取出链表的头节点,函数名GetLinkTableHead清晰明确地表明了接口的目标;

  • 该接口的前置条件是链表必须存在使用该接口才有意义,即pLinkTable != NULL;

  • 使用该接口的双方遵守的协议规范是通过数据结构tLinkTableNode和tLinkTable定义的;

  • 使用该接口之后的效果是找到了链表的头节点,这里是通过tLinkTableNode类型的指针作为返回值来作为后置条件。

2.2.2 给Linktable增加Callback方式的接口

  给Linktable增加Callback方式的接口,需要两个函数接口,一个是call-in方式函数,如SearchLinkTableNode函数,其中有一个函数作为参数,这个作为参数的函数就是callback函数,如代码中Conditon函数。利用callback函数参数使Linktable的查询接口更加通用,有效地提高了接口的通用性。

tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable,
int Condition(tLinkTableNode * pNode));

Condition函数负责搜索链表tLinkTableNode,发现指定节点就将结果返回给SearchLinkTableNode函数。

int SearchCondition(tLinkTableNode * pLinkTableNode)
{
    tDataNode * pNode = (tDataNode *)pLinkTableNode;
    if(strcmp(pNode->cmd, cmd) == 0)
    {
        return  SUCCESS;  
    }
    return FAILURE;           
}

/* find a cmd in the linklist and return the datanode pointer */
tDataNode* FindCmd(tLinkTable * head, char * cmd)
{
    return  (tDataNode*)SearchLinkTableNode(head,SearchCondition);
}

  我们在查找指定内容时是将cmd定义为全局变量,然后condition查找与在链表中查找与cmd相同值的目标,我们可以定义一个传入参数args,在链表中查找与args相同值的目标,然后将结果返回给SearchLinkTableNode函数,这样可以有效地隐藏软件模块内部的实现细节,为外部调用接口的开发者提供更加简洁的接口信息,同时也减少外部调用接口的开发者有意或无意的破坏软件模块的内部数据。

int SearchCondition(tLinkTableNode * pLinkTableNode, void * args)
{
    char * cmd = (char*) args;
    tDataNode * pNode = (tDataNode *)pLinkTableNode;
    if(strcmp(pNode->cmd, cmd) == 0)
    {
        return  SUCCESS;  
    }
    return FAILURE;           
}
/* find a cmd in the linklist and return the datanode pointer */
tDataNode* FindCmd(tLinkTable * head, char * cmd)
{
    return  (tDataNode*)SearchLinkTableNode(head,SearchCondition,(void*)cmd);
}

2.2.3 定义通用接口的基本方法

  • 参数化上下文
  • 移除前置条件
  • 简化后置条件

  例如在lab4中对链表的各种操作的函数进行了封装形成一个头文件,我们在其他模块中如果想使用这些函数,只需要在当前模块添加 #include"linktable.h",就可以将头文件导入进来,那么在当前模块对链表进行各种操作时也可以直接调用这些函数,这样可以使得对链表的操作更加方便了。我们如果想对链表添加新的操作,只需要在头文件中添加编写好的函数声明就可以了。

 /*
 * 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.2 线程安全

2.3.1 可重入函数

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

  例如linktable.c在创建链表结点时,使用molloc函数分配一块内存空间,然后对存储区进行初始化。当多个线程访问该代码区时,会为每一个线程都分配一块内存区域,且分配的内存区域相互独立,没有共享内存区域。可以说这个函数时可重用函数。

/*
 * Create a LinkTable
 */
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;
}

2.3.2 线程安全

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

  例如linktable.c中添加链表节点时,为了实现线程安全,可以在对链表进行操作时添加一把读写互斥锁,这样做使得该线程对链表的独占,其他线程无法对链表进行操作,在该线程完成对链表的操作后,就会释放读写互斥锁,其他线程此时才能对链表进行修改。这样就实现了多个线程在操作链表时的线程安全。

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

三、总结

  通过在VS Code中练习menu项目,深刻体会到了软件工程中使用的思想。在项目开发中,始终追求的是可重用性和可扩展性。而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

posted @ 2020-11-09 21:25  XRush1  阅读(131)  评论(0编辑  收藏  举报