阅读 62

代码中的软件工程-命令行菜单小程序

1、前言

 通过软件工程的学习,孟宁老师通过一个菜单小程序,深入浅出地讲解了一些软件项目构建的注意事项。我深刻地了解到,好的软件,不仅仅是要实现用户的功能,还要从软件的可读性、通用性、扩展性等多方面考虑问题。

   本篇博客结合孟宁老师的menu小程序的不断迭代简要的分析一下隐藏在代码中的软件工程思想。

   参考文献:

    源代码

 

2.编译和调试环境配置

  • 官网下载mingw-64
  • 配置环境变量 C:\MinGW\mingw64\bin(根据自己的安装目录决定)

  

  • 在命令行上 gcc --version测试一下 ,出现如下图标表示安装成功

  

 

  • 在vscode中按如下配置好launch.json和tasks.json文件

launch.json

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "gcc.exe - 生成和调试活动文件",
            "type": "cppdbg",
            "request": "launch",
            "program": "${fileDirname}\\${fileBasenameNoExtension}.exe",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "miDebuggerPath": "C:\\MinGW\\mingw64\\bin\\gdb.exe",
            "setupCommands": [
                {
                    "description": "为 gdb 启用整齐打印",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            "preLaunchTask": "C/C++: gcc.exe build active file"
        }
    ]
}

 

tasks.json

{
    "tasks": [
        {
            "type": "shell",
            "label": "C/C++: gcc.exe build active file",
            "command": "C:\\MinGW\\mingw64\\bin\\gcc.exe",
            "args": [
                "-g",
                "${file}",
                "menu.c",
                "linktable.c",
                "-o",
                "${fileDirname}\\${fileBasenameNoExtension}.exe",
                "-lpthread"
            ],
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ],
    "version": "2.0.0"
}
  • 运行test.c文件,并测试命令

  

  到此,完成编译和调试环境的配置。

 

3.如何写一个好的项目

3.1 不要重复造轮子  

  首先,一个项目的起始不是应该直接写代码,而是应该看是否有人在之前实现了类似的功能,然后决定是重新写一个还是维护已有的项目来达成目标。

3.2 良好的代码风格

  首先,代码要符合编写规范(可以是社区规范,也可以是本项目的通用编码风格)。其次,代码要逻辑清晰,让其他人看代码能跟上你的思路。最后,设计要优雅,这是代码的最高追求。

3.3 代码迭代

  有句经典的老话,“罗马不是一日建成的”。这在软件的编写中非常实用。一个软件在设计之初,设计者不可能思考的面面俱到,也需要在不断的调试中修正自己的思路,最终实现自己的目的。

  例如在实现menu程序时,可以先从一个经典的输出“hello world”的小程序开始。

hello.c

 

#include 

int main()
{
    while (1)
    {
       printf("hello world!\n");
    }
    return 0;
}

 

 

为什么要从这个程序开始呢?这就要从这个程序的特点说起,这个程序有一个特殊的地方,就是一个while(1)的无限循环,为什么要加这个,我们可以想一想,在常见的命令行程序比如cmd中,我们可以不断地输入命令,不断地获得响应,倘若我们一直持续输入,这个程序是不是也不会停止,因此我们就发现了命令行程序的一个特点就是无限循环,只有出现特殊的情况才会退出(比如exit),因此从这个不断打印的程序开始,我们好像真的在一步步接近最终的命令行程序。

 

既然是命令行小程序,那么肯定得加一些命令,那么先加一个最常见的help命令。

menu.c--version1.0

int main()
{
    while(true)
    {
        scanf(cmd);
        int ret = strcmp(cmd, "help");
        if(ret == 0)
        {
            dosth();
        }
        int ret = strcmp(cmd, "others");
        if(ret == 0)
        {
            dosth();
        }
    }
}

 这里使用的是伪代码,使用伪代码可以摒弃掉繁杂的实现,让人理清逻辑。

 

理清好思路后,可以具体实现了。

menu.c--version2.0

int main()
{
    char cmd[128];
    while(1)
    {
        scanf("%s", cmd);
        if(strcmp(cmd, "help") == 0)
        {
            printf("This is help cmd!\n");
        }
        else if(strcmp(cmd, "quit") == 0)
        {
            exit(0);
        }
        else
        {
            printf("Wrong cmd!\n");
        }
    }
}

到这为止其实一个可以互动的命令行程序已经完成,之后不过是不断加入更多功能而已。

 

从这个例子可以看出,代码迭代可以帮我们理清实现思路,同时一步步地迭代,可以及时发现错误,而不是一次编译很多代码发现众多错误纠缠在一块。

可以这么说,每一个成熟的软件都是从一个小幼苗开始,不断迭代最终才能成长为参天大树。

 

4.模块化设计

4.1不使用模块化设计的代码有什么特点呢?

1.函数内代码太长

2.命名可能不规范(命名冲突)

3.功能复杂(功能应该按需加载而非全部加载) 

4.可读性不好 

5.不好维护

 

4.2模块化设计优点

1.简单化(相当于把复杂问题分解成一个个简单问题)

2.可维护性,模块化编程存在逻辑边界,单个模块的修改对其他部分影响低  

3.可重用性(多次使用无需再编程)

4.命名不冲突

5.容易定位bug

 

从lab3.1 - 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 ");
        }
    }
}

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

可以看到,将查找过程封装到FindCmd()函数中,业务代码变得更加易读。

 

lab3.3 引入了linklist.h,linklist.c 更进一步实现了将数据结构、业务、具体操作(功能实现)的分离。这样的接口设计,可以让各个接口模块耦合性更低,每个接口只了解自身相关的内容,与自身无关的则完全交给其他模块实现,这样各个接口独立性增强,出现bug时,可以很快定位bug,同时也不会影响到其他模块的使用。

linklist.h

typedef struct DataNode
{
    char*   cmd;
    char*   desc;
    int     (*handler)();
    struct  DataNode *next;
} tDataNode;

/* find a cmd in the linklist and return the datanode pointer */
tDataNode* FindCmd(tDataNode * head, char * cmd);
/* show all cmd in listlist */
int ShowAllCmd(tDataNode * head);

 

4.3模块化设计要遵守的原则

KISS(Keep It Simple & Stupid)原则

 

5.可重用接口

  如果想要软件让更多人使用,那么接口就必须要有通用性,而不能只针对某个特定的业务,这就需要将业务代码和功能实现完全分离。

  从lab4 - lab5.1,删除了原来的linklist.c和linklist.h,引入了linktable.c和linktable.h,从下面代码可以看出,这些接口是通用化的接口,不针对某个具体业务,只要满足接口的前置条件就可以使用,这使得接口和业务完全隔离。

 

linktable.h

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);
/*
 * Search a LinkTableNode from LinkTable
 * int Conditon(tLinkTableNode * pNode);
 */
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode));
/*
 * get LinkTableHead
 */
tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);
/*
 * get next LinkTableNode
 */
tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);

#endif /* _LINK_TABLE_H_ */

这里有一个小插曲,即一个回调函数(call-back)的实现,因为通用接口SearchLinkTableNode()只是简单的遍历整个链表,通用接口因为与业务隔离,完全不知道具体链表中的节点有什么内容,因此就无法进行比较,这就要借助业务代码中的接口,在通用接口中使用业务接口这就是函数回调。这样通用接口就可以完全不用知道业务内容是什么,从而实现链表节点的查找,真正的实现业务与接口的分离。

在软件接口设计当中,我们一般追求松散耦合,在这里SearchLinkTableNode()还有一点小小的问题。

具体看一下引用SearchLinkTableNode()的代码。

SearchCondition()

 

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

 

FindCmd()

tDataNode* FindCmd(tLinkTable * head, char * cmd)
{
    return  (tDataNode*)SearchLinkTableNode(head,SearchCondition);
}

在SearchCondition()函数实现的过程中,我们依然使用了全局变量cmd,这就会造成公共耦合,这是软件接口设计中要避免的。

一般来说,我们会通过添加参数的方法将其进行转化,这里我们从函数调用的源端进行修改,可以发现函数的调用过程是 FindCmd---SearchLinkTableNode---SearchCondition,要想SearchCondition获得一个cmd参数,那么就只能从顶层的调用方获得,因为FindCmd函数已经存在cmd参数,因此只需要在 SearchLinkTableNode和SearchCondition中加入一个(void*)args, 这样可以传入一个或者多个参数。这正是lab5.2所做的工作。

SearchCondition()

 

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

 

SearchLinkTableNode()

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

 

 6.线程安全

  线程是轻量级进程,与进程相比,其不用进行环境的切换,因此提高了并发程度,但是并发程度的提高带来了一个问题,那就是数据的共享,这就会造成一定的安全问题。

  如果函数仅使用局部变量,那么线程切换就不会出现数据不一致的问题,因为数据都被独立保存在各自的线程栈中,如果使用了全局变量,如果不进行防护,就会导致数据不一致的问题。

  那么线程的安全怎么实现?

  可以先考虑实现一个简单的问题,可重入函数。

6.1可重入函数

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

  6.1.1可重入函数的基本要求

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

6.2可重入函数与线程安全         

 可重入的函数不一定是线程安全的。因为不同线程同时访问不同的可重入函数依然会导致线程安全问题,可能需要更高级的技术,比如读写锁,这里就不讨论。

 不可重入的函数一定不是线程安全的。

 尽管可重入函数只是必要条件,但对实现线程安全有一定的帮助。

lab7.1中就对各个函数加入互斥锁实现了可重入函数,这样不同线程访问同一个可重入函数时,就不会导致数据不一致的情况。
 

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

 

 

7.总结

  阅读完menu程序的源码后,对软件的编写有了一些初步的认识,希望能在以后的编码生涯中能够充分应用出来。

 

 

 

 

 

  

 

 

 

原文:https://www.cnblogs.com/tangxin621/p/13928575.html

文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐