在Menu小程序中学习软件工程方法

时间:2020-11-08 16:52:15   收藏:0   阅读:33

前言

???在编写代码的过程中充分运用软件工程的思想是一个优秀程序员不可或缺的技能。一个优秀的程序应该具有良好的可靠性、容错性、易用性、易读性、可扩充性、可理解性和可维护性等等性能。为了达到这些要求,我们应该学会如何让自己编写的程序更加符合软件工程的思想,通过学习代码编写的规范和各种面向对象思想的实现方式,我们就能在今后的职业生涯中一帆风顺。

???学习完孟宁老师的课程后,了解到了许多软件工程的设计思想,需要注意的事项。老师通过一个menu小程序生动形象的介绍了代码的风格规范、模块化设计思想(高内聚,低耦合)、可重用接口的实现、使用回调函数和注意线程安全等问题。

????本篇文章将会分析孟宁老师的menu小程序来学习软件工程设计中的思想,从工程中汲取知识和习惯将会是提升自己最快的方法。在上完孟宁老师的高级软件工程的课程后,我对软件工程思想的了解更近了一步。下面将会对每一个部分进行详细的介绍。

参考文献:https://gitee.com/mengning997/se/blob/master/README.md#代码中的软件工程

一.VSCode中C/C++编译环境配置

本项目的编译运行环境为VSCdoe,首先我们应该配置C/C++的编译环境

1.安装C/C++ extension
技术图片

2.编辑配置文件tasks.json和launch.json来配置代码的编译命令和Debug环境:

//tasks.json的文件内容
{
	"version": "2.0.0",
	"tasks": [
		{
			"type": "cppbuild",
			"label": "C/C++: clang build active file",
			"command": "/usr/bin/clang",
			"args": [
				"-g",
				"${file}",
				"-o",
				"${fileDirname}/${fileBasenameNoExtension}"
			],
			"options": {
				"cwd": "/usr/bin"
			},
			"problemMatcher": [
				"$gcc"
			],
			"group": {
				"kind": "build",
				"isDefault": true
			},
			"detail": "compiler: /usr/bin/clang"
		}
	]
}
//launch.json的文件内容
{ 
  "version": "0.2.0",
    "configurations": [
        {
            "name": "(lldb) Launch",
            "type": "cppdbg",
            "request": "launch",
            "program": "enter program name, for example ${workspaceFolder}/a.out",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "lldb"
        }
    ]
}

3.编译运行,测试是否配置成功:
技术图片
技术图片

二.代码风格规范

1.代码风格的原则:

?????代码的风格规范可以总结为:简约而不简单。具体来说有简明、易读、无二义性这三个特性。我们把代码的风格分成三重境界:一是规范整洁。遵守常规语言规范,合理使用空格、空行、缩进、注释等;二是逻辑清晰。没有代码冗余、重复,让人清晰明了的命名规则。做到逻辑清晰不仅要求程序员的编程能力,更重要的是提高设计能力,选用合适的设计模式、软件架构风格可以有效改善代码的逻辑结构,会让代码简洁清晰;三是优雅。优雅的代码是设计的艺术,是编码的艺术,是编程的最高追求。

?????我们拿lab3.1版本中的menu.c程序来说明具体含,如果我们代码编写为这个形式:

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

2.程序块头部的注释:

文件头注释和程序段注释应该遵守规范:

3.具体代码规范总结:

代码编写规范:

命名规范:


三.模块化设计

?????模块化设计是指在对一定范围内的不同功能或相同功能不同性能、不同规格的产品进行功能分析的基础上,划分并设计出一系列功能模块,通过模块的选择和组合可以构成不同的产品,以满足市场的不同需求的设计方法。

?????一个软件系统可按功能不同划分成若干功能模块。软件系统的层次结构正是模块化的具体体现。把一个大而复杂的软件系统划分成易于理解的比较单纯的模块结构,这些模块可以被组装起来以满足整个问题的需求。

?????模块化软件设计的方法如果应用的比较好,最终每一个软件模块都将只有一个单一的功能目标,并相对独立于其他软件模块,使得每一个软件模块都容易理解容易开发。从而整个软件系统也更容易定位软件缺陷bug,因为每一个软件缺陷bug都局限在很少的一两个软件模块内。而且整个系统的变更和维护也更容易,因为一个软件模块内的变更只影响很少的几个软件模块。因此,软件设计中的模块化程度便成为了软件设计有多好的一个重要指标,一般我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度。

?????对比menu小程序的lab3.2版本和lab3.3版本,我们发程序中添加了linklist.c文件和linklist.h文件。进行了模块化设计之后我们往往将设计的模块与实现的源代码文件有个映射对应关系,因此我们需要将数据结构和它的操作独立放到单独的源代码文件中,这时就需要设计合适的接口,以便于模块之间互相调用。

技术图片

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

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

良好模块设计标准:

四.可重用软件设计

1.可重用软件简介

?????在软件开发中,由于不同的环境和功能要求,我们可以通过对以往成熟软件系统的局部修改和重组,保持整体稳定性,以适应新要求。这样的软件称为可重(chong)用软件。据统计,现今,开发一个新的应用系统,40%~60%的代码是重复以前类似系统的成分,重复比例有时甚至更高。因此,软件重用能节约软件开发成本,真正有效地提高软件生产效率。

2.接口的设计

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

接口的五个要素:

3.menu程序举例

为了更好的理解软件的可重用性设计,我们可以查看lab4中的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
{
    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_ */

?????通过启用linktable.h接口文件,就实现了软件的可重用设计,为了进一步实现软件的可重用设计,我们添加一个callback方式的接口使Linktable的查询接口更加通用。给Linktable增加Callback方式的接口,需要两个函数接口,一个是call-in方式函数,如SearchLinkTableNode函数,其中有一个函数作为参数,这个作为参数的函数就是callback函数,如代码中Conditon函数。

SearchLinkTableNode函数:

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

Condition函数:

int SearchCondition(tLinkTableNode * pLinkTableNode, void * args)
{
    char * cmd = (char*) args;
    tDataNode * pNode = (tDataNode *)pLinkTableNode;
    if(strcmp(pNode->cmd, cmd) == 0)
    {
        return  SUCCESS;  
    }
    return FAILURE;	       
}
4.接口和耦合度之间的关系:

????对于软件模块之间的耦合度,前文中提到,耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。一般在软件设计中我们追求松散耦合。更细致地对耦合度进一步划分的话,耦合度依次递增可以分为无耦合、数据耦合、标记耦合、控制耦合、公共耦合和内容耦合。这些耦合度划分的依据就是接口的定义方式,我们接下来重点分析一下公共耦合、数据耦合和标记耦合。

公共耦合:

数据耦合

标记耦合

五.可重入函数和线程安全

1.基本概念

?????线程(thread)是操作系统能够进行运算调度的最小单位。它包含在进程之中,是进程中的实际运作单位。一个线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。一般默认一个进程中只包含一个线程。操作系统中的线程概念也被延伸到CPU硬件上,多线程CPU就是在一个CPU上支持同时运行多个指令流,而多核CPU就是在一块芯片上集成了多个CPU核,比如4核8线程CPU芯片就是在集成了4个CPU核,每个CPU核上支持2个线程。有了多核多线程CPU,操作系统就可以让不同进程运行在不同的CPU核的不同线程上,从而大大减少进程调度进程切换的资源消耗。传统上操作系统工作在单核单线程CPU上是通过分时共享CPU来模拟出多个指令执行流,从而实现多进程和多线程的。

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

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

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

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

3.Menu程序中的线程安全

?????在linktable.c文件中添加pthread.h头文件,并且修改LinkTable结构体,添加 pthread_mutex_t mutex。之后就可以利用mutex变量实现进程安全:

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

};

?之后修改linktable.c文件,不需要考虑读操作,只需要考虑写操作即可。在写操作的部分加锁,代码内容如下,在加锁和解锁的部分加以注释,比如下面的代码:

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

这样就实现了线程安全。


总结

????最后感谢孟宁老师带我了解软件工程方法,学习到了很多代码编写的习惯,以及项目结构搭建过程中需要注意的点,包括代码的风格规范、模块化设计思想(高内聚,低耦合)、可重用接口的实现、使用回调函数和注意线程安全等等。

参考资料:

  1. https://gitee.com/mengning997/se/blob/master/README.md#代码中的软件工程
  2. https://github.com/mengning/menu
评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!