其他分享
首页 > 其他分享> > 代码中的软件工程——基于menu实验的一些思考

代码中的软件工程——基于menu实验的一些思考

作者:互联网

  最近孟老师在课上讲解了GitHub上一个简单有趣的menu小案例,生动直观地讲解了模块化设计、可重用接口以及线程安全等问题,经过此次学习,我收获颇丰。在此我想记录一下自己的所学所思,同时感谢孟老师的辛勤教导。

       本文源代码来自于孟老师的文章:https://gitee.com/mengning997/se/blob/master/README.md

一、环境的搭建

   想跑通menu项目,需要在VSCode 中配置好C++环境,因为VScode不带C++的编译器。

      首先从官网下载MinGW-w64,安装过程很容易,一直next即可。安装完毕之后通过cmd执行gcc -v查看安装是否成功:

 

安装完成之后打开VScode,在扩展部分添加C/C++插件,如图:

 

  生成的task.json文件

{     "version": "2.0.0",     "command": "g++",     "args": [         "-g",         "${file}",         "-o",         "${fileBasenameNoExtension}.exe"     ], // 编译命令参数     "problemMatcher": {         "owner": "cpp",         "fileLocation": [             "relative",             "${workspaceFolder}"         ],         "pattern": {             "regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",             "file": 1,             "line": 2,             "column": 3,             "severity": 4,             "message": 5         }     } }

  生成的launch.json文件

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "(gdb) Launch", // 配置名称,将会在启动配置的下拉菜单中显示
            "type": "cppdbg", // 配置类型,这里只能为cppdbg
            "request": "launch", // 请求配置类型,可以为launch(启动)或attach(附加)
            "program": "${workspaceFolder}/${fileBasenameNoExtension}.exe", // 将要进行调试的程序的路径
            "args": [], // 程序调试时传递给程序的命令行参数,一般设为空即可
            "stopAtEntry": false, // 设为true时程序将暂停在程序入口处,一般设置为false
            "cwd": "${workspaceFolder}", // 调试程序时的工作目录,一般为${workspaceRoot}即代码所在目录 workspaceRoot已被弃用,现改为workspaceFolder
            "environment": [],
            "externalConsole": false, // 调试时是否显示控制台窗口,一般设置为true显示控制台
            "MIMode": "gdb",
            "miDebuggerPath": "D:/GoogleDownload/MinGW/bin/gdb.exe", // miDebugger的路径,注意这里要与MinGw的路径对应
            "preLaunchTask": "g++", // 调试会话开始前执行的任务,一般为编译程序,c++为g++, c为gcc
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": false
                }
            ]
        }
    ]
}

  至此,环境配置成功,可以运行menu小程序。

 

二、 软件工程一般原理分析 

  1、模块化设计

  模块化设计,简单地说就是程序的编写不是开始就逐条录入计算机语句和指令,而是首先用主程序、子程序、子过程等框架把软件的主要结构和流程描述出来,并定义和调试好各个框架之间的输入、输出链接关系。逐步求精的结果是得到一系列以功能块为单位的算法描述。以功能块为单位进行程序设计,实现其求解算法的方法称为模块化。模块化的目的是为了降低程序复杂度,使程序设计、调试和维护等操作简单化。改变某个子功能只需相应改变相应模块即可。

  模块化设计可以降低系统中的耦合度,可以进行更好的扩展和可重用。

  在menu项目中,将控制逻辑和业务逻辑进行分开处理,在逻辑上进行划分。同时将接口的声明和实现放在不同的文件中,一个模块做一个模块的事情:

  通过观察,不难看出menu项目中有Linktable模块,Menu模块,Test模块,其中LinkTable模块实现了对menu的各种操作,如增删改链表功能,我们对链表操作的函数无需关注细节,直接调用就好,代码易于理解,并且如果新增功能(如加上查找链表的功能),直接调用函数即可,无需重写代码。

  linktable.h文件声明接口,linktable.c文件实现接口,要使用的时候只需要调用接口即可:

  linktable.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);
复制代码

   linktable.c文件实现接口

tDataNode* FindCmd(tDataNode * head, char * cmd)
{
    if(head == NULL || cmd == NULL)
    {
        return NULL;        
    }
    tDataNode *p = head;
    while(p != NULL)
    {
        if(!strcmp(p->cmd, cmd))
        {
            return p;
        }
        p = p->next;
    }
    return NULL;
}

int ShowAllCmd(tDataNode * head)
{
    printf("Menu List:\n");
    tDataNode *p = head;
    while(p != NULL)
    {
        printf("%s - %s\n", p->cmd, p->desc);
        p = p->next;
    }
    return 0; 
}

   menu模块并不需要知道LinkTable内方法的具体实现过程,只需要关注自身方法的具体实现,需要时调用LinkTable模块即可,这就是模块化思想的好处。

  2、可重用接口

  讨论完了模块化设计后,我们来讨论一下模块间的接口设计。

  所谓接口设计,就是要设计一个好的接口,那什么才算是好的接口呢,我认为除了接口声明简洁明了外,就是接口是否可以重复利用了。在编程实践中,我们往往把多次重复利用的代码封装成一个函数,这样可以使代码层次清晰。然而,这样写的代码离可重用还是有相当的距离,因为每次调用函数都是执行相同的代码逻辑,如果业务逻辑发生了变动,相应的函数就可能会失效。真正的可重用,就是拿来代码直接就用,涉及到修改代码的统统都不是真正的可重用。还是以linktable模块为例:

  linktable.h文件

  LinktableNode结构体只保留了最基本的遍历功能,具体的data数据并没有包含,这是因为用户可以自己添加所需要的数据

  而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);
/*
 * get LinkTableHead
 */
tLinkTableNode * GetLinkTableHead(tLinkTable *pLinkTable);
/*
 * get next LinkTableNode
 */
tLinkTableNode * GetNextLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode);
复制代码

 主函数调用更一般的接口来实现

复制代码
main()
{
    InitMenuData(&head); 
   /* 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();
        }
   
    }
}
复制代码

 为了更加通用,可以修改cmd数组,使其变为局部变量,同时增加一个args参数

复制代码
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode, void * args), void * args);

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

 

  3、线程安全

  线程安全:多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
​  需要考虑线程安全的情况:访问共享的变量或资源, 会有并发风险, 比如对象的属性, 静态变量, 共享缓存, 数据库等;所有依赖时序的操作, 即使每一步操作都是线程安全的, 还是存在并发的问题;不同的数据之间存在绑定关系的时候。例如IP与端口号. 只要修改了IP就要修改端口号, 否则IP也是无效的。 因此遇到这种操作的时候,要警醒原子的合并操作,要么全部修改成功, 要么全部修改失败。使用其他类的时候, 如果该类的注释声明了不是线程安全的,那么就不应该在多线程的场景中使用, 而应该考虑其对应的线程安全的类,或者对其做一定处理保证线程安全。

  对linktable模块进行分析,可以看出linktable.h文件通过给链表的增删操作加了一个互斥锁,实现线程安全。

复制代码

//在结构体LinkTable中定义加上互斥锁pthread_mutex_t

struct LinkTable

{
  tLinkTableNode *pHead;
  tLinkTableNode *pTail;
  int SumOfNode;
  pthread_mutex_t mutex;
};

 

//CreateLinkTable函数中使用 pthread_mutex_init对互斥锁进行初始化:

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

 

//在DeleteLinkTable函数、AddLinkTableNode函数、DelLinkTableNode函数中对链表的操作都分别进行加锁和解锁。

//以DeleteLinkTable函数为例,使用pthread_mutex_lock和pthread_mutex_unlock在删除链表结点前后分别进行加锁和解锁,

//使用pthread_mutex_destroy销毁互斥锁:

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

复制代码

 

 三、总结

  开发软件的时候我们应该按照软件工程的一般规律,在模块化、可重用接口的设计、以及线程安全等问题上多下文章,多留意,尽可能地提高软件开发的效率和软件的质量。

  最后,再次真诚地感谢孟老师为我们讲解高级软件软件工程,听了您的课,对于以后的开发大有脾益,受益终生。

标签:思考,menu,cmd,接口,软件工程,mutex,pLinkTable,tLinkTableNode,NULL
来源: https://www.cnblogs.com/ligang-ustc/p/13953184.html