代码中的软件工程

前言

本篇博客基于孟宁老师的高级软件工程课程,以VS Code + GCC工具集为主要环境编译调试课程项目案例,结合代码分析其中的软件工程方法、规范和软件工程思想。

参考资料: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

 


 

一、编译和调试环境配置

 

  1、安装vscode中的C/C++扩展

    打开vscode,点击侧边栏的扩展(Ctrl+Shift+X),搜索c++,点击安装C/C++

     

    C/C++扩展不包含C++编译器或调试器

 

  2、安装C++编译器和调试器 

    本机使用Windows系统,因此下载Mingw-w64(Windows中的GCC,http://mingw-w64.org)

    安装过程中有几个选项需要说明:

      Version制定版本号,从4.9.1-8.x.0,按需选择,没有特殊要求就用最新版吧;

      Architecture跟操作系统有关,64位系统选择x86_64,32位系统选择i686;

      Threads设置线程标准可选posix或win32;

      Exception设置异常处理系统,x86_64可选为seh和sjlj,i686为dwarf和sjlj;

      Build revision构建版本号,选择最大即可

    

    安装完成后,需要将安装目录下的bin目录加入环境变量

    添加环境变量后,打开CMD,执行gcc -v看看是否安装成功

    

 

   3、运行和调试

    使用vscode打开一个C文件,点击侧边栏的运行(Ctrl+Shift+D),点击运行和调试,选择环境C++(GBD/LLDB)

    #注意文件夹路径不能有中文

    

    选择配置gcc.exe

    

     生成了.vscode文件夹,其中有launch.json和tasks.json文件

    

    tasks.json告诉vscode如何生成(编译)程序的文件,将调用gcc编译器基于源代码创建可执行文件

    launch.json用于调试器设置

    

 


 

二、软件工程方法

 

  一个菜单程序的生长

  初始代码lab2(menu.c):

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

  

  1、模块化设计

    模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发,这个做法背后的基本原理是关注点的分离 (SoC, Separation of Concerns),是由软件工程领域的奠基性人物Edsger Wybe Dijkstra(1930~2002)在1974年提出。关注点的分离在软件工程领域是最重要的原则,我们习惯上称为模块化,翻译成我们中文的表述其实就是“分而治之”的方法

    模块化程度是软件设计有多好的一个重要指标,一般我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度

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

    

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

    引入链表结构,DataNode储存了命令名、命令描述、函数和下一结点的指针,将数据结构和它的操作从菜单业务中分离出来

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

    并将链表结构定义和函数操作声明放在linklist.h中,对链表的操作的具体实现放在linklist.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; 
}

    主程序menu.c主体并没有很大变化,将复杂的问题抽离出去,调用其接口。每一个软件模块都将只有一个单一的功能目标,并相对独立于其他软件模块,使得每一个软件模块都容易理解容易开发。也使得后期维护变得简单一些,整个软件系统更容易定位软件缺陷bug,因为每一个软件缺陷bug都局限在很少的一两个软件模块内。而且整个系统的变更和维护也更容易,因为一个软件模块内的变更只影响很少的几个软件模块。

    使用本地化外部接口来提高代码的适应能力:

    

 

  2、可重用接口

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

    接口规格包含五个基本要素:

      接口的目的;

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

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

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

      接口所隐含的质量属性。

    lab4代码(linktalbe.c):

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

    该接口的目标是从链表中下一个链表节点,函数名GetNextLinkTableNode清晰明确地表明了接口的目标;

    该接口的前置条件是链表必须存在,同时当前结点不为空,使用该接口才有意义,也就是链表(pLinkTable != NULL)&&(pNode != NULL);

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

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

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

 

  3、线程安全

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

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

    线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

    添加互斥锁:

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

    在创建链表函数中初始化互斥锁:

pthread_mutex_init(&(pLinkTable->mutex), NULL);

    在修改链表时需要加锁解锁:

pthread_mutex_lock(&(pLinkTable->mutex));
pLinkTable->pHead = pLinkTable->pHead->pNext;
pLinkTable->SumOfNode -= 1 ;
pthread_mutex_unlock(&(pLinkTable->mutex));

    删除链表时销毁互斥锁:

pthread_mutex_destroy(&(pLinkTable->mutex));

 


 

三、小结

  通过孟老师课堂上的耐心讲解,以及课后阅读孟老师menu项目的迭代过程,结合代码分析其中的软件工程方法,这使得我们对软件的开发有了更深刻的认识,同时意识到了以前自己开发软件的混杂,在此深表谢意。 

 

posted @ 2020-11-08 22:35  test271  阅读(190)  评论(0编辑  收藏  举报