代码中的软件工程(menu项目分析及感受)

本博客参考自孟宁老师提供的资料:https://gitee.com/mengning997/se/blob/master/README.md#代码中的软件工程
并分析孟老师提供的menu项目代码,思考软件工程是如何在代码中体现,并分析其中的软件工程方法,规范和软件工程思想。

一、C/C++编译调试环境配置

1.安装Visual Studio Code

Visual Studio Code(vscode)是一个轻量且强大的代码编辑器,支持Windows,LINUX,OS X,并且拥有丰富的插件生态环境,通过安装插件可以支持C++,C#,Python,PHP等其他语言,因此我们使用vscode进行接下来的代码分析体验。
由于在孟老师讲解了git的使用之后,我已经安装并使用了Visual Studio Code来用git进行项目管理,因此不需要再次安装。

2.在vscode中安装C++插件

在vscode中按ctrl+shift+X进入扩展界面,搜索C++,并点击安装,如下图所示:

3.安装C++编译器Mingw-w64/GCC

上一步我们在vscode中安装了C++扩展,但是C++扩展中并不包括C++编译器,为了在不同环境下保持一致,我们选择Mingw-w64/GCC,这里从官网链接下载并安装,安装如下图所示:

根据电脑配置,安装参数调整如图,
Version:如果没有特殊需求就选择最新版本进行安装,这里选择8.1.0版本;
Architecture:跟操作系统有关,64位选择x86_64;
Threads:设置线程标准,如果是在Windows下开发程序,选择win32,如果是开发在LINUX,Unix,Mac OS等其他操作系统下的程序,就选择posix,这里选择win32;
Exception:设置处理异常系统,seh相较于sjlj发明更晚,且性能更好,但是不支持32位,这里选择性能更好的seh;
Build revision:构建版本号,使用默认值。
参数配置好之后,点击安装进行在线下载安装,安装完毕之后不要忘了配置环境变量:

用cmd测试安装是否成功,输入gcc -v,显示如图所示,则安装成功:

至此,C/C++编译环境配置完成。

4.在vscode中成功运行c项目

在vscode中打开项目文件夹,选择运行选项,创建launch.json文件,选择配置如下图所示


成功运行main.c文件,至此,在vscode中配置C/C++完成,并可以成功运行。

二、代码中的软件工程分析

接下来,我将从以下几个在软件工程中非常重要的知识出发,介绍其原理并分析其在代码中的具体实现方法,从而更加深刻的理解并体会代码中的软件工程思想,这里再次感谢孟宁老师提供给我们的非常规范的memu项目作为参考。

1.模块化设计

我们首先先来介绍一下模块化(Modularity),模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离。关注点的分离在软件工程领域是最重要的原则,我们习惯上称为模块化,翻译成我们中文的表述其实就是“分而治之”的方法。关注点的分离的思想背后的根源是由于人脑处理复杂问题时容易出错,把复杂问题分解成一个个简单问题,从而减少出错的情形。模块化软件设计的方法如果应用的比较好,最终每一个软件模块都将只有一个单一的功能目标,并相对独立于其他软件模块,使得每一个软件模块都容易理解容易开发。
因此,软件设计中的模块化程度就成为了软件设计中有多好的一个重要指标,我们通常使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度。

1.1耦合度

耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合,松散耦合和无耦合,三种耦合程度的见下图所示,无耦合中各个模块之间互不依赖,这显然不是我们所想要的情况,紧密耦合中各个模块中的依赖程度又过高,这在实际的编程中很可能改变其中某个模块就会导致其他模块的大规模改动,引起类型雪崩的后果。
因此我们一般在软件设计中都追求松散耦合。

1.2内聚度

内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。理想的内聚是功能内聚,即根据功能将各个模块进行分类,一个模块只做一件事,只完成一个主要功能点或一个软件特性。

1.3保证模块化的一些基本方法

a. KISS(Keep it Simple & Stupid)原则

这里的KISS原则主要指的是要保证我们的代码简单易读,这样就算别人在看我们写的代码的时候也会更加轻松舒服,最好的代码就是即使没有注释,读者也可以通过代码的命名和模块理解代码,这里我们就要尽量做到一行代码只做一件事,一个块代码只做一件事,一个函数只做一件事,一个软件模块只做一件事。
b. 使用本地化外部接口的方法

使用本地化接口的主要目的是提高我们代码的适应能力,自己编写接口也可以保证内部代码的安全性

c. 先用伪代码的代码结构

因为设计为代码不需要考虑异常处理等一些变成细节,因此可以更大程度的保证设计逻辑性上的框架结构,是的逻辑结构可以在伪代码更好的体现出来,逻辑设计好之后再进行真正代码的实现,从伪代码到实现代码的过程就是反复重构的过程。

1.4代码分析

我们来看menu项目中的链表的实现,如下是linktable.h的代码实现


/********************************************************************/
/* Copyright (C) SSE-USTC, 2012-2013                                */
/*                                                                  */
/*  FILE NAME             :  linktabe.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 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);
/*
 * Search a LinkTableNode from LinkTable
 * int Conditon(tLinkTableNode * pNode,void * args);
 */
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args);
/*
 * get LinkTableHead
 */
tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);
/*
 * get next LinkTableNode
 */
tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);

#endif /* _LINK_TABLE_H_ */

如下是截取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>
#include <string.h>
#include "linktable.h"
#include "menu.h"

tLinkTable * head = NULL;
int Help();
int Quit();

#define CMD_MAX_LEN 		1024
#define CMD_MAX_ARGV_NUM 	32
#define DESC_LEN    		1024
#define CMD_NUM     		10

char prompt[CMD_MAX_LEN] = "Input Cmd >";

/* data struct and its operations */

typedef struct DataNode
{
    tLinkTableNode * pNext;
    char*   cmd;
    char*   desc;
    int     (*handler)(int argc, char *argv[]);
} tDataNode;

int SearchConditon(tLinkTableNode * pLinkTableNode,void * arg)
{
    char * cmd = (char*)arg;
    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)
{
    tDataNode * pNode = (tDataNode*)GetLinkTableHead(head);
    while(pNode != NULL)
    {
        if(!strcmp(pNode->cmd, cmd))
        {
            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;
}


linktable.h这个头文件中声明了链表的结构,是这个项目中的底层用到的链表的数据结构的定义和一些方法的实现,这里函数和定义只牵扯到链表的定义和操作,而不干预上层的代码实现,比如menu菜单中的函数,通过定义的各种接口,方便上层的文件进行调用,实现了底层数据结构的定义和业务逻辑的分离。
上层的menu.c文件在编写是,并不关心底层的链表数据结构是如何实现的,而是通过#include "linktable.h"头文件调用的方法,使用底层编写好的数据结构和实现的接口来操作链表,这种耦合程度也符合我们所说的松散耦合。而linktable和menu两个文件分别各自实现了链表结构的定义和菜单功能的实现,按照功能对模块进行分类,符合我们所追求的理想的功能内聚。

2.可重用接口

2.1接口的定义及代码分析

我们通过功能内聚,将各个模块按照功能划分之后,就要考虑一个模块与另一个模块之间的耦合关系,为了达到我们理想的松散耦合,就要定义简洁,清晰,明确的接口。
接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务
接口规格是软件系统的开发者正确使用一个软件模块需要知道的所有信息,那么这个软件模块的接口规格定义就必须清晰明确地说明正确使用本软件模块的信息。一般来说,接口规格包含五个基本要素,下面,我将以linktable.h文件中定义的添加节点接口int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);为例,分析结构规格包含的五个基本要素:
1.接口的目的:从接口名我们可以看出,这个函数的目的是在已有链表上插入一个新的节点;
2.接口使用前所要满足的条件,一般称为前置条件或假定条件:该接口的前置条件有两个,第一个是链表必须存在,这个接口才有意义,即pLinkTable !=NULL,另一个前置条件是要添加的节点不能为空,即pNode != NULL
3.使用接口的双方遵守的协议规范:协议规定在链表的数据结构定义和链表节点的数据结构定义中已经规定好了,开发者和使用者都必须遵守这个协议规范;
4.接口使用之后的效果:使用的效果是在原始链表的末尾,插入了一个新的节点,插入成功则返回int类型0,插入失败则返回int类型-1;
5.接口所隐含的质量属性:该接口没有特别要求接口的质量属性;

2.2通用接口定义的基本方法及代码分析

在实际编程中,我们都要对接口的定义进行通用化处理,以方便上层的函数可以方便使用这个接口来操作底层的数据结构,以下以删除链表结点的函数为例进行分析:
1.参数化上下文:我们要通过参数来传递上下文的信息,而不是隐含依赖上下文条件;
以删除节点为例,若是我们定义的删除节点函数如下:

tLinkTableNode * pNode = qNode;
int DelLinkTableNode(tLinkTable *pLinkTable)
{
      //函数代码
}

此时上层调用该函数时,就会不知道要删除的节点究竟是哪一个,因此我们必须要通过参数来调用函数:

void DelLinkTableNode(tLinkTable *pLinkTable , tLinkTableNode * pNode)
{
    //函数代码
}

2.移除前置条件,参数化上下文之后,我们发现上层在使用这个接口是仍然具有很大的局限性,比如调用这个接口必须有一个前提,就是传入的链表和传入的节点一定不能为空,如果为空,函数就会出错,这对上层调用显然不够通用化,因此我们可以在函数实现中加入判断条件:

void DelLinkTableNode(tLinkTable *pLinkTable , tLinkTableNode * pNode)
{
    if(pLinkTable == NULL || pNode == NULL)
    {
        return;
    }
    //函数代码
}

这样,这个接口就变得更加通用了,上层在调用接口时不用担心链表和节点是否为空;
3.简化后置条件,我们在调用这个接口后,可以以返回删除后的链表作为返回对象,就像这样:tLinkTable * DelLinkTableNode(tLinkTable *pLinkTable , tLinkTableNode * pNode)
但我们并不知道删除是否成功,或者链表中是否有要删除的节点,此时后置条件可能是删除成功,或者是链表没有进行任何改变就直接返回,因此我们需要简化后置条件,将返回值类型改为int型,若返回为0则表示删除成功,返回-1则表示删除失败,经过改写并加入线程安全之后,我们的完整代码如下所示(下面的FAILURE和SUCCESS分别被宏定义为-1和0):

int DelLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode)
{
    if(pLinkTable == NULL || pNode == NULL)
    {
        return FAILURE;
    }
    pthread_mutex_lock(&(pLinkTable->mutex));
    if(pLinkTable->pHead == pNode)
    {
        pLinkTable->pHead = pLinkTable->pHead->pNext;
        pLinkTable->SumOfNode -= 1 ;
        if(pLinkTable->SumOfNode == 0)
        {
            pLinkTable->pTail = NULL;	
        }
        pthread_mutex_unlock(&(pLinkTable->mutex));
        return SUCCESS;
    }
    tLinkTableNode * pTempNode = pLinkTable->pHead;
    while(pTempNode != NULL)
    {    
        if(pTempNode->pNext == pNode)
        {
            pTempNode->pNext = pTempNode->pNext->pNext;
            pLinkTable->SumOfNode -= 1 ;
            if(pLinkTable->SumOfNode == 0)
            {
                pLinkTable->pTail = NULL;	
            }
            pthread_mutex_unlock(&(pLinkTable->mutex));
            return SUCCESS;				    
        }
        pTempNode = pTempNode->pNext;
    }
    pthread_mutex_unlock(&(pLinkTable->mutex));
    return FAILURE;		
}

3.线程安全

3.1线程安全及相关概念介绍

线程(thread)是操作系统能够进行运算调度的最小单位。它包含在进程之中,是进程中的实际运作单位。一个线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。一般默认一个进程中只包含一个线程。
可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用局部变量,要么在使用全局变量时保护自己的数据。
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

3.2代码分析

下面我们以linktable.c文件中的线程安全进行分析,首先是链表的结构定义:

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

};

pthread_mutex_t mutex;声明了mutex变量,用来保证当多个线程同时执行读写操作时资源的互斥,以确保线程安全。
接着我们来看删除节点的函数:

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

在这里,线程在进行真正的删除操作之前,我们使用pthread_mutex_lock(&(pLinkTable->mutex));,对链表进行上锁操作,保证同一时间只能有一个线程对这个链表进行写操作,保证多个线程同时操作时的安全性,再伤处操作完成之后,我们再使用pthread_mutex_unlock(&(pLinkTable->mutex));进行解锁,这是其他的线程才可以继续对代码进行写操作。

三、总结

经过孟老师这次工程化编程实战课程的学习,我对代码实战中的软件工程有了进一步的理解,再加上仔细阅读了menu项目的源代码,我也更加感受到了自己平时在写代码时的不规范性,这让我对之后的项目实战有了很大的启发,由于自己也是跨专业考研来的软件学院,项目经历本来就十分匮乏,所以更加庆幸能够在一开始就遇到孟老师,并传授给我们标准且高效的软件设计方法,接下来我也会将理论融入到自己的代码中,使自己的项目更加规范严谨。
参考资料:
https://gitee.com/mengning997/se/blob/master/README.md#代码中的软件工程
课程项目案例:https://github.com/mengning/menu

posted @ 2020-11-08 14:46  EndArthur  阅读(183)  评论(0编辑  收藏  举报