实例分析软件工程中的思想

前言

  但我们自己在编写代码的时候,有没有想过我们的代码能不能重复使用,而不是每个程序的同样功能都要编写同样的一套代码?有没有想过我们的代码也可以给别人使用?或者是我们去使用别人的代码?当我们去思考这些问题的时候我们就开始了软件工程。当然,软件工程博大精深绝不仅仅局限于解决这几个问题。

menu程序

  在linux操作系统中,我们通常使用命令行输入来操作程序,而menu程序就是用来解析我们所输入命令的一个程序。下面我将通过分析menu程序,并且结合孟宁老师课上的内容来领悟程序中有关软件工程的思想
程序地址:https://github.com/mengning/menu
vscode环境配置参考:https://mp.weixin.qq.com/s/sU9Wh12JZqQyJfEBBafFAQ
本人使用wsl2.0的ubuntu20.04版本已经通过sudo apt install -y build-essential搭建好了远程vscode开发环境,本文不再做过多介绍

代码规范和代码风格

  我们可能会认为,代码写出来是给机器看的,其实不然,代码是给人看的。别人需要使用你的代码或者维护你的代码,首先要看懂你的代码。所以代码规范和风格就显得尤为重要。
  孟宁老师把代码的风格分成三重境界:

  • 一是规范整洁。遵守常规语言规范,合理使用空格、空行、缩进、注释等;
  • 二是逻辑清晰。没有代码冗余、重复,让人清晰明了的命名规则。做到逻辑清晰不仅要求程序员的编程能力,更重要的是提高设计能力,选用合适的设计模式、软件架构风格可以有效改善代码的逻辑结构,会让代码简洁清晰;
  • 三是优雅。优雅的代码是设计的艺术,是编码的艺术,是编程的最高追求。


    一般来讲,我们对代码风格的基本原则要求是简明、易读、无二义性。

在menu程序中的例子

1.内容详细整齐的头部注释

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

2.程序快头部的注释
  简短的注释还是很有必要的,特别是在很难通过函数名来直接理解到代码的涵义时

/* add cmd to menu */
int MenuConfig(char * cmd, char * desc, int (*handler)());

3.简明的代码风格

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

  可以看到即使在if语句下之后一行代码也没有省去大括号,让代码的分解清晰,更加易读。

代码风格规范总结——C版本

  • 缩进:4个空格;
  • 行宽:< 100个字符;
  • 代码行内要适当多留空格,如“=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前后应当加空格。对于表达式比较长的for语句和if语句,为了紧凑起见可以适当地去掉一些空格,如for (i=0; i<10; i++)和if ((a<=b) && (c<=d));
  • 在一个函数体内,逻揖上密切相关的语句之间不加空行,逻辑上不相关的代码块之间要适当留有空行以示区隔;
  • 在复杂的表达式中要用括号来清楚的表示逻辑优先级;
  • 花括号:所有 ‘{’ 和 ‘}’ 应独占一行且成对对齐;
  • 不要把多条语句和多个变量的定义放在同一行;
  • 命名:合适的命名会大大增加代码的可读性;
  • 类名、函数名、变量名等的命名一定要与程序里的含义保持一致,以便于阅读理解;
  • 类型的成员变量通常用m_或者_来做前缀以示区别;
  • 一般变量名、对象名等使用LowerCamel风格,即第一个单词首字母小写,之后的单词都首字母大写,第一个单词一般都表示变量类型,比如int型变量iCounter;
  • 类型、类、函数名等一般都用Pascal风格,即所有单词首字母大写;
  • 类型、类、变量一般用名词或者组合名词,如Member
  • 函数名一般使用动词或者动宾短语,如get/set,RenderPage;
  • 注释和版权信息:注释也要使用英文,不要使用中文或特殊字符,要保持源代码是ASCII字符格式文件;
  • 不要解释程序是如何工作的,要解释程序做什么,为什么这么做,以及特别需要注意的地方;
  • 每个源文件头部应该有版权、作者、版本、描述等相关信息。

模块化

  在软件设计中最重要的就是模块话的设计。通过模块化的设计让系统内的各个部分保持相对对了,以便每一个部分可以被独立地进行设计和开发。模块化设计的原理是关注点分离,相当于把复杂的问题分解成一个个简单的问题。
  在menu程序中:

  我们可以看出,主要分为三个模块:

  • 程序的入口test
  • 菜单逻辑menu
  • 菜单使用到的数据结构linktable


    其中test作为程序的入口和添加命令操作的地方,它完全于menu隔开,当我们需要添加新的命令的时候,我们只需要在test模块中进行修改尔不用涉及到menu中的代码。同样的,当数据结构需要跟换的时候,我可以直接修改linktable中的代码而不用涉及menu。


    通过上述的分析,我们不难发现模块化的作用:
  • 更好团队合作
  • 更好进行代码的迭代

模块化遵循的原则——KISS(Keep it simple & stupid)原则

  • 一行代码只做一件事
  • 一个代码块只做一件事
  • 一个函数只做一件事
  • 一个软件模块只做一件事

接口化设计

  上面我们提到了模块化的设计,但是我们有没有想过模块之间怎么进行调用?负责不同模块开发的人之间怎么进行合作?怎么样设计一个模块才能让该模块在别的软件中也能够重复使用?
  这些问题的解决方案就是接口化的设计。接口就是双方需要遵守的条约,只有遵守了条约才能使用这一功能。我们也可以理解为我们日常生活中的常用usb接口,电脑自己定义了一个usb接口给别的硬件使用,如果一个u盘的接口也是usb形状的那么他们就可以进行进行连通了。下面列举出接口规范包含的五个基本要素:

  • 接口的目的
  • 接口使用前所需要满足的条件,称前置条件或假定条件
  • 使用接口的双方遵守的协议规范
  • 接口使用之后的效果,称后置条件
  • 接口所隐含的质量属性

  在menu程序的linktable模块中定义了许多的接口:

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

  我们通过接口GetNextLinkTableNode来了解接口的基本要素:

  • 目标:通过注释或者接口名称可以知道目标就是获得下一个链表节点
  • 前置条件:前置条件可以看作是参数,需要链表和当前节点都存在的情况下才能找到一下一个节点
  • 协议:就是链表和节点都应该是tLinkTable和tLinkTableNode类型的指针
  • 后置条件:接口执行后将返回但前节点的下一个节点
  • 质量属性:该接口没有特比的质量要求

  通过接口化的设计,任何需要使用链表结构的程序都可以通过导入linktable模块然后调用接口进行实现,极大的提高的程序中代码的重用率。

接口化设计中的点睛之笔——callback

  在SearchLinkTableNode接口中我们不难发现一个函数形参:int Conditon(tLinkTableNode * pNode, void * args),这个函数形参是做什么用的?在回答这个问题之前我们应该先看看SearchLinkTableNode这个接口,这个接口的目的肯定就是
寻找链表中的一个节点,但是要寻找什么样的节点呢?那么这个找什么样节点就由Condition负责。
  通过代码寻找Condition的实体:
在linktable.c中

/*
 * Search a LinkTableNode from LinkTable
 * int Conditon(tLinkTableNode * pNode);
 */
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判断之后才返回节点。
在menu.c中可以看到在ExecuteMenu函数中使用SearchLinkTableNode接口

tDataNode *p = (tDataNode*)SearchLinkTableNode(head,SearchConditon,(void*)argv[0]);

int SearchConditon(tLinkTableNode * pLinkTableNode,void * arg)
{
    char * cmd = (char*)arg;
    tDataNode * pNode = (tDataNode *)pLinkTableNode;
    if(strcmp(pNode->cmd, cmd) == 0)
    {
        return  SUCCESS;  
    }
    return FAILURE;	       
}

  SearchConditon的功能就是比较参数的值和该节点存储的命令是否相等。


  这就是callback函数。通过callback函数,接口赋予了使用它的人极大的自由并且扩张了接口的适用性。

数据无关

  细心的朋友可能已经注意到了linktable.h中的猫腻,就是链表节点的定义

/*
 * LinkTable Node Type
 */
typedef struct LinkTableNode
{
    struct LinkTableNode * pNext;
}tLinkTableNode;

  可以看到节点中是没有存储任何数据的,只有下一个节点的指针。
但是在menu.c中我们又可以看到,tDataNode的声明中第一个成员是tLinkTableNode:

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

  在后续的代码中可以看到通过指针的强制类型转换将tDataNode的指针和tLinkTableNode进行互换。这里涉及到一个C指针的知识:C指针实际上并没有什么区别都只是一个内存地址,但是给指针一个类型的目的是为了明确要获得该地址开始的多少比特数据。
  当指针类型为tDataNode的时候,将获取下图中的四个数据,但是指针类型为tLinkTableNode的时候,将只获取第一个数据



  这种数据无关让linktable这个数据结构几乎适用于任何数据

posted @ 2020-11-02 23:55  宇智波派  阅读(255)  评论(0)    收藏  举报