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

本次作业使用孟老师上课时使用的menu小项目为例进行代码中的软件工程分析,使用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

  • 准备工作

下载VS Code和GCC并配置,因为上次作业已经使用过VS Code,在这里就不再赘述本机为Windows10系统,使用MinGW,下载官网为:https://osdn.net/projects/mingw/releases/,选择适合自己的版本下载安装即可。配置完成之后打开cmd,输入gcc -v,可以看到相关信息。如图

 

 

 

注意:windows操作系统不支持pthread函数库,需要下载windows系统支持的版本,下载地址:http://sourceware.org/pub/pthreads-win32/pthreads-w32-2-8-0-release.exe,具体配置方法见:https://blog.csdn.net/htf15/article/details/16846143

程序运行截图

 

 

 

 

  • 代码风格和代码规范

在实际的软件项目开发过程中,仅靠个人便能完成整个项目变得不再实际,事实上每个人都有自己需要完成的部分,而各个部分又是彼此关联的,所以在完成自己工作的同时如何给合作伙伴带来更大的便利性就成了一个很重要的问题,这时,良好的代码风格和代码规范就成了解决问题的关键。

  1. 代码风格的原则 简明 易读 无二义性
  2. 在符合上述原则的前提下,什么才是好的代码风格呢,在这里我们把代码风格分为三重境界
  3. 程序块头部注释
  • 第一重境界也是最基本的就是规范整洁。符合常规语言规范,合理使用空格、空行、缩进、注释等。
  • 第二重境界要求逻辑清晰。没有冗余代码,做到路基清晰不仅要求程序员的编程能力,更重要的是提高设计能力。
  • 第三重境界是优雅。优雅的代码是设计的艺术,是编码的艺术,是编程的最高追求。

 

 如上图,该程序块头部的注释将函数功能、编程语言、运行环境、版本时间、简单描述等一一列举出来,这往往是模块的对外接口,以便自动生成开发者文档。

4.代码风格规范总结

  • 缩进:4个空格;
  • 行宽:<100个字符;
  • 在一个函数体内,逻辑上密切相关的语句之间不加空行,逻辑上不相关的代码块之间要适当留有空行;
  • 在复杂的表达式中要用括号来表示逻辑优先级;
  • 不要把多条语句和多个变量的定义放在同一行;
  • 命名:合适的命名大大增加代码的可读性;
  • 类名、函数名、变量名等的命名一定要与程序利的函数保持一致,便于阅读理解;
  • 一般变量名、对象名等使用LowerCamel风格;
  • 类型、类、函数名等一般使用Pascal风格;
  • 函数名一般使用动词或者动宾短语,如get/set,RenderPage;

 

  • 模块化设计

模块化是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离,是由软件工程领域的奠基性任务迪杰斯特拉提出来的。

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

 

 

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

以lab3.1、lab3.2和lab3.3为例进行分析模块化设计的优点

lab3.1

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

可以看到在main函数里有查找功能,利用代码实现

lab3.2

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

在lab3.2中,查找功能变成了一个函数FindCmd()

在lab3.3中,本来menu.c中实现的全部功能分离了一部分放在了linklist.c中,menu.c需要做的只是引用linklist.h头文件就可以实现原先的功能,实现了模块之间的互相调用。

软件设计中的一些基本方法

KISS(Keep It Simple & Stipid)原则

  1. 一行代码只做一件事
  2. 一个代码块只做一件事
  3. 一个函数只做一件事
  4. 一个软件模块只做一件事

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

 

 

先写伪代码的代码结构更好一些

在从设计到编码的过程中假如伪代码要好于直接将设计翻译成实现代码

 

  • 可重用接口

接口的基本概念

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

由以上定义可知,可重用接口就是通用的接口,不能只是被某个业务特用。

那么,以menu项目为例,简要分析可重用接口的设计。

首先,将链表的数据结构和操作独立出来,即将通用的LinkTable模块集成到menu程序中

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

};


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

可以看到在使用LinkTable模块之后menu程序业务代码变得复杂,使用起来比较繁琐,这是因为我们的接口定义的还不够好,接下来让我们进一步改进接口设计。

给LinkTable增加Callback方式的接口

tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args)
{
    if(pLinkTable == NULL || Conditon == NULL)
    {
        return NULL;
    }
    tLinkTableNode * pNode = pLinkTable->pHead;
    while(pNode != NULL)
    {    
        if(Conditon(pNode,args) == SUCCESS)
        {
            return pNode;                    
        }
        pNode = pNode->pNext;
    }
    return NULL;
}

给LinkTable增加Callback方式的接口,需要两个函数接口,一个是call-in方式函数,如SearchLinkTableNode函数,其中有一个函数作为参数,这个作为参数的函数就是callback函数,如Conditiion函数。

这里重点谈一下callback函数,这个函数负责收集tLinkTableNode *pNode,一旦发现pNode,就返回pNode.

tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args)

call-in方式的函数接口SearchLinkTableNode增加了一个参数args,callback函数Condition也增加了一个参数args,那么这么做的好处是什么呢?或许可以从全局变量cmd身上得到答案,因为cmd是全局变量,耦合度过高,那么就很不符合可重用接口的定义,这样我们增加一个args参数,当调用接口的时候,只需要传入指针就行。

 

  • 可重入函数与线程安全

开始之前,先看看什么是线程

  • 线程是操作系统能够进行运算调度的最小单位。它包含在进程之中,是进程的实际运作单位。一个线程指的是进程中一个单一顺序的控制流,一个进程可以并发多个线程,每条线程执行不同的任务。
  • 线程安全:如果进程中由多个线程在同时运行,而这些线程可能会同时运行这段代码,如果每次运行结果和单线程运行的结果一样,而且其他的变量的值也和预期的是一样的,就是线程安全的。

那么什么是可重入函数呢

  • 可重入函数可以由多于一个任务并发使用,而不必担心数据错误。

LinkTable软件模块的线程安全分析

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

以本段代码为例,可以看到在删除节点的时候,会进行加锁,在节点删除完毕后,会解锁,通过互斥锁实现了函数的可重入,在进行删除操作时,加锁之后就只有当前线程可以进行相关操作,自然不用担心一你想线程安全。

  • 总结

本次作业不仅对于基础的代码风格和代码规范有了更深一步的了解,更重要的是对于代码中的软件工程有了初步的了解,作业只是一次很小的实践,在以后的工作中还需要不断地提高,在项目中总结经验,并把总结的经验和学到的理论再应用到项目中去。

 

posted on 2020-11-05 11:58  昵称叫啥好呢  阅读(174)  评论(0编辑  收藏  举报

导航