代码中的软件工程-menu项目代码分析

本篇博客通过孟宁老师的课堂教学,完成了在VsCode中的C/C++编译调试环境配置,及通过老师给的menu项目源代码,对软件工程中的模块化设计、可重用接口、线程安全等问题进行简单分析,最后是本人对软件工程项目的总结和思考。

编译调试环境配置

  1. 由于VSCode不附带C++编译器,因此我们需要从官网链接http://sourceforge.net/projects/mingw-w64/files/Toolchains%20targetting%20Win32/Personal%20Builds/mingw-builds/installer/mingw-w64-install.exe/download处下载MinGW编译器并安装,如下图所示:

这里有五个参数,其中Version选择当前支持的最高版本。

Architecture:由于我的是win10 64位,因此选择了x86_64。如果是32位电脑,需要选择i686。

Threads:这个世界上只有两种操作系统,符合 posix 协议的,和 Windows 。如这个笑话所说的,如果你想要开发 Windows 程序,需要选择 win32 ,而开发 Linux、Unix、Mac OS 等其他操作系统下的程序,则需要选择 posix 。我只开发在 Windows 下 运行的程序,所以选择了 win32 。

Exception:异常处理在开发中非常重要,你在开发的过程中,大部分的时间会耗在处理各种异常情况上。如果你之前选择了 64位,则这里有两个异常处理模型供你选择,seh 是新发明的,而 sjlj 则是古老的。seh 性能比较好,但不支持 32位。 sjlj 稳定性好,支持 32位。 我这里选择了seh。如果之前架构选择的是i686,则选项是 dwarf 和 sjlj 两种异常处理模型。同样的,dwarf 的性能要优于 sjlj ,他们都不支持 64位 。

Build revision:使用默认值。

点击next安装,会在线下载安装。等待安装完毕后,需要配置系统环境变量。

右击我的电脑-属性-高级系统设置-环境变量,点击path后新建环境变量,添加变量值,这里我的安装位置是 D:\mingw-w64\x86_64-8.1.0-win32-seh-rt_v6-rev0\mingw64\bin,如下图所示:

(注意添加新环境变量时前面要加分号';')
点击确定后,打开命令提示符,输入gcc -v,如下图所示,则安装成功。

  1. 随后打开VSCode,点击扩展按钮,输入C++,安装C++插件,如下图所示

安装完成后,在vscode中打开工作区文件夹,打开代码文件后按ctrl+F5,会选择调试环境和编译器,调试选择C++(GDB/LLDB),编译器选择gcc.exe,即可启动编译。打开工作区文件夹后可发现文件夹下生成了一个名为.vscode的隐藏文件夹,里面有tasks.json和launch.json两个子文件。点击运行后,代码运行成功。

代码中的软件工程分析

模块化设计

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

关注点的分离的思想背后的根源是由于人脑处理复杂问题时容易出错,把复杂问题分解成一个个简单问题,从而减少出错的情形。模块化软件设计的方法如果应用的比较好,最终每一个软件模块都将只有一个单一的功能目标,并相对独立于其他软件模块,使得每一个软件模块都容易理解容易开发。从而

  • 整个软件系统也更容易定位软件缺陷bug,因为每一个软件缺陷bug都局限在很少的一两个软件模块内。
  • 整个系统的变更和维护也更容易,因为一个软件模块内的变更只影响很少的几个软件模块。

我们一般使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度。

1.耦合度

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

2.内聚度

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

3.代码分析

下面我们来分析代码中的模块化思想。

//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业务功能。
而下面的代码段通过#include "linktable.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;	       
}
......

可重用接口

1.接口的相关概念

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

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

2.接口使用实例

在linktable.h文件中,我们定义了一系列的接口,如GetLinkTableHead函数,代码如下:

/*
 * get LinkTableHead
 */
tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);

该函数提供了获取下一个链表的头节点的接口,使得调用者只需完成自身的功能设计,而无需顾及接口所需要实现的功能的细节,如下menu.c程序中的FindCmd函数对本接口进行了调用

/* 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;
}
3. 接口与耦合度之间的关系

对于软件模块之间的耦合度,前文中提到,耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。一般在软件设计中我们追求松散耦合。下面我们分析一下公共耦合、数据耦合和标记耦合。

公共耦合

当软件模块之间共享数据区或变量名的软件模块之间即是公共耦合,显然两个软件模块之间的接口定义不是通过显式的调用方式,而是隐式的共享了共享了数据区或变量名。

数据耦合

在软件模块之间仅通过显式的调用传递基本数据类型即为数据耦合。

标记耦合

在软件模块之间仅通过显式的调用传递复杂的数据结构(结构化数据)即为标记耦合,这时数据的结构成为调用双方软件模块隐含的规格约定,因此耦合度要比数据耦合高。但相比公共耦合没有经过显式的调用传递数据的方式耦合度要低。

线程安全

1. 线程的基本概念

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

2. 可重入函数:

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

可重入函数有如下要求:

  • 不为连续的调用持有静态数据;
  • 不返回指向静态数据的指针;
  • 所有数据都由函数的调用者提供;
  • 使用局部变量,或者通过制作全局数据的局部变量拷贝来保护全局数据;
  • 使用静态数据或全局变量时做周密的并行时序分析,通过临界区互斥避免临界区冲突;
  • 绝不调用任何不可重入函数。
3. 线程安全的定义:

当代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。 线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行读写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

4. 函数的可重入性与线程安全之间的关系:

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

5. menu项目中的线程安全模块分析
/*
 * LinkTable Type
 */
struct LinkTable
{
    tLinkTableNode *pHead;
    tLinkTableNode *pTail;
    int			SumOfNode;
    pthread_mutex_t mutex;

};

上述结构体完成了对LinkTable的定义,其中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时,进行了加锁操作,在清空链表之后,进行了解锁。

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

删除LinkTable时,对代码块进行了加锁,防止其他代码访问此函数出错,完成功能后进行了解锁。

总结与思考

模块化设计使得代码段功能相互独立,易于理解与后期维护;可重入接口和线程安全设计提升了代码运行的稳定性。这几者相辅相成,为开发高质量的软件提供保障。


参考资料: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
https://blog.csdn.net/iningwei/article/details/101649090

posted @ 2020-11-07 21:17  zerone01  阅读(151)  评论(0编辑  收藏  举报