代码中的软件工程——menu项目

VSCode编译和调试环境配置

 

1.安装语言工具

2.安装编译器和调试器

VSCode 的 C/C++插件并不包含 C/C++编译器和调试器,我们需要自己安装 C/C++编译器和调试器。我们这里选用 Mingw-w64(包含 GCC 和 GDB)用于 Windows 环境。下载后运行如下图所示:

 

配置环境变量后,打开cmd,输入gcc -v,出现如下信息,则表示配置成功!

 

 

 

 此时可新建一个文件,输入一个简单的小程序,点击右上角的运行。

 

 运行成功 !vsCode的调试和配置环境已完成!

 

代码中的软件工程——模块化设计(Modularity)

 

模块化是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。模块化设计的基本目标就是使整个复杂的系统相互分离,生成一个个单一的简单的问题,从而减少出错的情形。其实就是“分而治之”的思想。

模块化设计使得每个功能都在单一的模块里,

模块化软件设计的方法如果应用的比较好,最终每一个软件模块都将只有一个单一的功能目标,并相对独立于其他软件模块,使得每一个软件模块都容易理解容易开发。

模块化设计使得单一的功能在一个模块里,软件的bug只会局限在一个或两个模块里,可维护性提高。通常使用耦合度内聚度来衡量。

下面我们来看一下menu项目中的具体实例,了解模块化设计。

下面的代码将Menu中的各个命令都集中在一起处理,这种代码可读性很差,后期维护成本也比较高。

#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");
        }
    }
}
下面的代码实现了各个cmd功能的分离,对代码进行了模块化设计,使得代码看起来更加清晰,也更易读。

进行了模块化设计之后我们往往将设计的模块与实现的源代码文件有个映射对应关系,因此我们需要将数据结构和它的操作独立放到单独的源代码文件中,这时就需要设计合适的接口,以便于模块之间互相调用。

#include <stdio.h>
#include <stdlib.h>

int Help();
int Quit();

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

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

int main()
{
    /* cmd line begins */
    while(1)
    {
        char cmd[CMD_MAX_LEN];
        printf("Input a cmd number > ");
        scanf("%s", cmd);
        tDataNode *p = head;
        while(p != NULL)
        {
            if(strcmp(p->cmd, cmd) == 0)
            {
                printf("%s - %s\n", p->cmd, p->desc);
                if(p->handler != NULL)
                {
                    p->handler();
                }
                break;
            }
            p = p->next;
        }
        if(p == NULL) 
        {
            printf("This is a wrong cmd!\n ");
        }
    }
}

int Help()
{
    printf("Menu List:\n");
    tDataNode *p = head;
    while(p != NULL)
    {
        printf("%s - %s\n", p->cmd, p->desc);
        p = p->next;
    }
    return 0; 
}

int Quit()
{
    exit(0);
}
 

代码中的软件工程——可重用接口设计

 

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

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

接口规格是软件系统的开发者正确使用一个软件模块需要知道的所有信息,那么这个软件模块的接口规格定义就必须清晰明确地说明正确使用本软件模块的信息。一般来说,接口规格包含五个基本要素:

1.接口的目的

2.接口使用前所需要满足的条件,一般称为前置条件或假定条件

3.使用接口的双方遵守的协议规范

4.接口使用之后的效果,一般称为后置条件

5.接口所隐含的质量属性

 在menu的Lab5中定义了关于linktable的多个接口,我们拿一个来举例,说明接口的五个基本要素:

tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);

这个接口是从链表中取出链表头结点的函数声明,我们以此来分析。

1.该接口的目标是从链表中取出链表的头节点,函数名GetLinkTableHead清晰明确地表明了接口的目标;

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

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

4.使用该接口之后的效果是找到了链表的头节点,这里是通过tLinkTableNode类型的指针作为返回值来作为后置条件,C语言中也可以使用指针类型的参数作为后置条件;

5.该接口没有特别要求接口的质量属性,如果搜索一个节点可能需要在可以接受的延时时间范围内完成搜索;

在linktable.h中新增了一个callback接口,也就是中间的函数参数condition

在menu.c中新增了一个searchCondition函数

 

 这样用户就可以自定义搜索的条件,利用callback函数参数使Linktable的查询接口更加通用,有效地提高了接口的通用性。

我们还通过将linktable.h中不是在接口调用时必须内容转移到linktable.c中,这样可以有效地隐藏软件模块内部的实现细节,为外部调用接口的开发者提供更加简洁的接口信息,同时也减少外部调用接口的开发者有意或无意的破坏软件模块的内部数据。通过接口进行信息隐藏已经成为面向对象编程语言的标准做法,使用public和private来声明属性和方法对于外部调用接口的开发者是否可见。

 

代码中的软件工程——可重入函数与线程安全

 

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

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

什么是线程安全?

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

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

我们可以看到,在Lab5中,引入了thread.h库,并添加了信号量 mutex:

 

 在访问不可重入代码段时,会有lock和unlock,对其进行互斥访问,这就保证了线程安全。

 

 

总结

高质量软件的设计不仅仅是面向功能的,软件的可重用性和后期的可维护性是非常重要的指标,在软件的生命周期中占有至关重要的影响。感谢孟宁老师的分享,附参考资料:

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-08 16:58  黎明er  阅读(180)  评论(0编辑  收藏  举报