代码中的软件工程——Menu小程序为例

本文以孟宁老师的Menu小程序为基础,完成vsCode的编译和调试环境配置,并结合代码分析其中的软件工程方法、规范或软件工程思想。

编译和调试环境配置

  1. 在写C程序前,需要准备MinGW-64用于编译和调试。
    下载成功后在终端运行"gcc -v"查看是否能够运行gcc命令。
    20201103190254
    若失败则可能是程序所在bin文件夹未加入系统环境变量。
  2. C/C++插件
    需要C/C++的vscode插件提供debug和代码提示等支持。
    20201103190709
  3. CMake和CMake插件
    CMake是一款C/C++的跨平台安装(编译)工具,能够方便地输出Makefile文件,方便以后遇见大型项目时能更方便地build。
    需要下载CMake,并vscode下载相关插件:
    20201103192733
  4. 配置文件
    需要配置tasks.json用于编译文件,launch.json用于调试文件,CMakeLists.txt用于生成MakeFile文件(孟老师的源代码中也有Makefile文件可以直接build)。
    20201103193112
    配置文件如下:
#CMakeLists.txt
project(MYTEST)
set(CMAKE_BUILD_TYPE Debug)
add_executable(mytest linktable.c menu.c test.c)
//tasks.json
{
    "version": "2.0.0",
    "options": {
        "cwd": "${workspaceFolder}/build"
    },
    "tasks": [
        {
            "type": "shell",
            "label": "cmake",
            "command": "cmake",
            "args": [
                ".."
            ]
        },
        {
            "label": "make",
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "command": "mingw32-make.exe",
            "args": [

            ]
        },
        {
            "label": "Build",
            "dependsOn":[
                "cmake",
                "make"
            ]
        }
    ]

}
//launch.json
{
    // 使用 IntelliSense 了解相关属性。 
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "g++.exe - 生成和调试活动文件",
            "type": "cppdbg",
            "request": "launch",
            "program": "${fileDirname}\\build\\test.exe",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "miDebuggerPath": "D:\\power\\mingw64\\bin\\gdb.exe",
            "setupCommands": [
                {
                    "description": "为 gdb 启用整齐打印",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            "preLaunchTask": "Build"
        }
    ]
}

在终端依次运行如下指令进行编译,成功运行:

mkdir build
cd buid
cmake ..    //如果安装了VS,第一次需要使用指令:cmake -G "MinGW Makefiles" ..
mingw32-make.exe    //Windows下的make指令
.\mytest.exe  

20201103193928

代码分析

我们用孟宁老师的MOOC课程的项目——实现一个命令行的菜单小程序,最终目标是完成一个通用的命令行的菜单子系统便于在不同项目中重用——来分析其中的软件工程方法、规范或软件工程思想:
Menu命令行程序源码
迭代的过程如下所示:
20201103154242

lab3.1:代码风格规范

lab3.1,这次迭代完成了menu.c的初步编写:

/**************************************************************************************************/
/* Copyright (C) mc2lab.com, SSE@USTC, 2014-2015                                                  */
/*                                                                                                */
/*  FILE NAME             :  menu.c                                                               */
/*  PRINCIPAL AUTHOR      :  Mengning                                                             */
/*  SUBSYSTEM NAME        :  menu                                                                 */
/*  MODULE NAME           :  menu                                                                 */
/*  LANGUAGE              :  C                                                                    */
/*  TARGET ENVIRONMENT    :  ANY                                                                  */
/*  DATE OF FIRST RELEASE :  2014/08/31                                                           */
/*  DESCRIPTION           :  This is a menu program                                               */
/**************************************************************************************************/

/*
 * Revision log:
 *
 * Created by Mengning, 2014/08/31
 *
 */


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

  • 笔记:代码规范-行长限制
    一行代码太长不利于代码的查看,因此需要将每一行限制在100字以内,vscode可在设置中搜索Editor:Rulers进行设置。
    100
  • 笔记:代码规范-注释
    文件头注释和程序段注释需要遵循规范(如公司的规范),比如:
    20201103195033
  • 笔记:代码规范-命名
    合理的命名规范能方便团队之间的交流,这一点可参照Google命名规范:
    Google开源项目风格指南
  • 笔记:代码规范
    规范整洁。遵守常规语言规范,合理使用空格、空行、缩进、注释、括号等。
    • 参数与参数之间、参数与运算符之间可以用空格隔开,这样写出来的代码美观大气,易于阅读。
    • 合理使用缩进,通常为4个空格
    • 逻辑紧密的代码行间不设空格,逻辑不太相关的代码间设空格(如不同的方法间)
  • 通过数据结构简化代码
    将命令参数通过数组进行组织,相比之前的版本能简化代码,维护性更高。后面将用链表来进行组织。

lab3.3:模块化设计思想

/*menu.c*/
#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; 
}

/**************************************************************************************************/
/* Copyright (C) mc2lab.com, SSE@USTC, 2014-2015                                                  */
/*                                                                                                */
/*  FILE NAME             :  linklist.h                                                           */
/*  PRINCIPAL AUTHOR      :  Mengning                                                             */
/*  SUBSYSTEM NAME        :  menu                                                                 */
/*  MODULE NAME           :  linklist                                                             */
/*  LANGUAGE              :  C                                                                    */
/*  TARGET ENVIRONMENT    :  ANY                                                                  */
/*  DATE OF FIRST RELEASE :  2014/09/10                                                           */
/*  DESCRIPTION           :  linklist for menu program                                            */
/**************************************************************************************************/

/*
 * Revision log:
 *
 * Created by Mengning, 2014/09/10
 *
 */


/* data struct and its operations */

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

/**************************************************************************************************/
/* Copyright (C) mc2lab.com, SSE@USTC, 2014-2015                                                  */
/*                                                                                                */
/*  FILE NAME             :  linklist.c                                                           */
/*  PRINCIPAL AUTHOR      :  Mengning                                                             */
/*  SUBSYSTEM NAME        :  menu                                                                 */
/*  MODULE NAME           :  linklist                                                             */
/*  LANGUAGE              :  C                                                                    */
/*  TARGET ENVIRONMENT    :  ANY                                                                  */
/*  DATE OF FIRST RELEASE :  2014/09/10                                                           */
/*  DESCRIPTION           :  linklist for menu program                                            */
/**************************************************************************************************/

/*
 * Revision log:
 *
 * Created by Mengning, 2014/09/10
 *
 */

#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.3实现了重要的迭代——模块化。在系统的结构中,模块是可组合、分解和更换的单元。模块化是一种处理复杂系统分解成为更好的可管理模块的方式。它可以通过在不同组件设定不同的功能,把一个问题分解成多个小的独立、互相作用的组件,来处理复杂、大型的软件。模块化有方便代码复用、可维护、降低耦合度、增加聚合度的好处。
    这里模块化还做的不够好,后面还会继续迭代。
  • 笔记:KISS
    3.1迭代相比前面的版本更加体现了KISS原则,即一个块代码(或一行代码,或一个函数,或一个软件模块)只做一件事,使得main主函数的内容更简单。后面的迭代将更加体现这一原则。
    模块化应尽力遵循KISS原则,即Keep It Simple & Stupid。这一原则要求一行代码只做一件事,一个块代码只做一件事,一个函数只做一件事,一个软件模块只做一件事。

lab4:可重用接口

为增加重用性,孟宁老师引入了linktable模块这一链表实现。下面是其接口:

/********************************************************************/
/* Copyright (C) SSE-USTC, 2012-2013                                */
/*                                                                  */
/*  FILE NAME             :  linktable.h                             */
/*  PRINCIPAL AUTHOR      :  Mengning                               */
/*  SUBSYSTEM NAME        :  LinkTable                              */
/*  MODULE NAME           :  LinkTable                              */
/*  LANGUAGE              :  C                                      */
/*  TARGET ENVIRONMENT    :  ANY                                    */
/*  DATE OF FIRST RELEASE :  2012/12/30                             */
/*  DESCRIPTION           :  interface of Link Table                */
/********************************************************************/

/*
 * Revision log:
 *
 * Created by Mengning,2012/12/30
 *
 */

#ifndef _LINK_TABLE_H_
#define _LINK_TABLE_H_

#include <pthread.h>

#define SUCCESS 0
#define FAILURE (-1)

/*
 * LinkTable Node Type
 */
typedef struct LinkTableNode
{
    struct LinkTableNode * pNext;
}tLinkTableNode;

/*
 * LinkTable Type
 */
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);

#endif /* _LINK_TABLE_H_ */

menu主函数:


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

int Help();
int Quit();

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

/* data struct and its operations */

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

/* find a cmd in the linklist and return the datanode pointer */
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;
}

/* show all cmd in listlist */
int ShowAllCmd(tLinkTable * head)
{
    tDataNode * pNode = (tDataNode*)GetLinkTableHead(head);
    while(pNode != NULL)
    {
        printf("%s - %s\n", pNode->cmd, pNode->desc);
        pNode = (tDataNode*)GetNextLinkTableNode(head,(tLinkTableNode *)pNode);
    }
    return 0;
}

int InitMenuData(tLinkTable ** ppLinktable)
{
    *ppLinktable = CreateLinkTable();
    tDataNode* pNode = (tDataNode*)malloc(sizeof(tDataNode));
    pNode->cmd = "help";
    pNode->desc = "Menu List:";
    pNode->handler = Help;
    AddLinkTableNode(*ppLinktable,(tLinkTableNode *)pNode);
    pNode = (tDataNode*)malloc(sizeof(tDataNode));
    pNode->cmd = "version";
    pNode->desc = "Menu Program V1.0";
    pNode->handler = NULL; 
    AddLinkTableNode(*ppLinktable,(tLinkTableNode *)pNode);
    pNode = (tDataNode*)malloc(sizeof(tDataNode));
    pNode->cmd = "quit";
    pNode->desc = "Quit from Menu Program V1.0";
    pNode->handler = Quit; 
    AddLinkTableNode(*ppLinktable,(tLinkTableNode *)pNode);
 
    return 0; 
}

/* menu program */

tLinkTable * head = NULL;

main()
{
    InitMenuData(&head); 
   /* 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; 
}

int Quit()
{
    exit(0);
}

  • 笔记:可重用接口
    孟宁老师认为之前分离出来的数据结构和它的操作还有很多菜单业务上的痕迹。因此这里引入他之前写好的linktable模块,这一模块是可重用的,只对外暴露其接口linktable.h,接口是双方共同遵守的一种协议规范,是需求方和开发者之间的重要合同,好的接口应遵循单一功能原则,此外接口的命名应简洁易懂,一看就知道是做什么的。

    tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);
    

    linktable模块只做与数据处理有关的操作,不涉及对menu业务的操作,进一步实现了内聚,降低了耦合度。
    进一步分析,linktable相比之前的linklist更为灵活,可以根据需要为结点添加想要的数据,如:

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

    我们在lab3中也是使用的tDataNode名称的结构,只是更换了底层实现逻辑(模块)。

  • 笔记:开发者指南和测试用例
    要让开发者使用你的模块,需要为其提供指南,或者为其提供测试用例(文件中的testlinktable.c),方便其快速了解功能和接口。

lab5.2:Callback函数


/*
linktabe.h
...
 */

...

tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args);
/*
 * get LinkTableHead
 */

...
/*menu.c*/

...

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

/* show all cmd in listlist */
int ShowAllCmd(tLinkTable * head)
{
    ...
}

int InitMenuData(tLinkTable ** ppLinktable)
{
    ...
 
    return 0; 
}

/* menu program */

tLinkTable * head = NULL;

main()
{
    InitMenuData(&head); 
   /* 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();
        }
    
    }
}

...
  • 笔记:CALLBACK
    这一次迭代的重要改进是给linktable增加callback方式的接口。
    callback即回调函数,是一个通过函数指针调用的函数。前面SearchLinkTableNode接口接受一个condition函数指针,而上文FindCmd方法中,他调用了SearchLinkTableNode方法,并将SearchCondition函数指针和cmd作为参数传入。
    callback的主要目的就是实现解耦,即我们可以在不改变SearchLinkTableNode实现的情况下,通过传入具体的函数指针,实现特定的需求。这与OOP中的多态解决的是一类问题。
    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;		
}

如果不加锁,可能出现如下情况:假设初始链表为空,则两个线程可能同时判断到链表为空,导致pHead指向了第二个结点而非第一个,第一个结点就被永久地丢失了,无法被访问到也无法被free,产生内存泄漏。
通过加锁,保证了对pLinkTable的数据的互斥访问。

总结

孟宁老师通过一个简单的小程序,让我们可以管中窥豹软件工程的代码规范和设计思想。现总结如下:

  1. 我们在实际开发中,应意识到代码风格就如同一个人的脸面,是最先被注意的地方。保持好的代码风格规范,能够对易读性有十分大的提升。这一块小到注释、缩进、空格,大到命名规范,都需要注意。这一块应参考优秀开源项目的命名规范和团队所用规范。
  2. 模块化的设计有助于提升质量属性,使得代码易于维护和解耦,提高代码功能的内聚程度。总之好处多多,但需要遵循一系列原则,如单一原则、开闭原则等,在OOP中尤其重要。
  3. 使用可重用的接口,隐藏了底层不必要接触的细节,使得我们有更多精力集中于业务层的处理。即:不要重复造轮子。
  4. 使用回调函数,能实现类似于OOP中的多态特性。
  5. 线程安全并不复杂,但需要我们时刻警惕。
posted @ 2020-11-04 00:16  FayunYm  阅读(272)  评论(0)    收藏  举报