代码中的软件工程分析

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

一:环境准备

  1 安装c/c++语言工具插件

    

  2 安装,此处我们选择 MinGW-w64 

  MinGW 的全称是:Minimalist GNU on Windows 。它实际上是将经典的开源 C语言 编译器 GCC 移植到了 Windows 平台下,并且包含了 Win32API ,因此可以将源代码编译为可在 Windows 中运行的可执行程序。而且还可以使用一些 Windows 不具备的,Linux平台下的开发工具。一句话来概括:MinGW 就是 GCC 的 Windows 版本 。

以上是 MinGW 的介绍,MinGW-w64 与 MinGW 的区别在于 MinGW 只能编译生成32位可执行程序,而 MinGW-w64 则可以编译生成 64位 或 32位 可执行程序。

正因为如此,MinGW 现已被 MinGW-w64 所取代,且 MinGW 也早已停止了更新,内置的 GCC 停滞在了 4.8.1 版本。

  因为在墙内,选择官网的下载器安装,老是出现下载不正确的问题,我们选择下载免安装版,解压后,将bin目录添加到环境变量中,打开控制台查看版本,此处是8.1.0最新版

  

   3 程序从HelloWorld 开始

  创建一个HelloWorld文件,我们放在d盘下,注意路径不要有中文,可能会出现一些奇怪的问题。在vs中打开,按快捷键Ctrl+Shift+P调出命令面板,输入C/C++,选择“Edit Configurations(UI)”进入配置,方便起见我们使用g++。配置完成后在test文件夹下生成了.vscode文件夹,并且里面有一个c_cpp_properties.json的配置文件。

  

 

  1.g++会自动链接C++标准库,比如algorith,string,vector等。
  2.gcc会根据文件后缀(.c,.cpp)自动识别是C文件还是C++文件,g++均认为是C++文件。
  3.gcc编译C文件少很多宏定义,gcc编译C++会多一些宏定义,均属于the GNU Compiler Collection,gcc是鼻祖,后来才有了g++。

  新建控制终端,进行编译和运行,成功输出hello,world

  

   4 在vscode中启动调试模式

  点击运行,启动调试运行,这里我们选用g++ ,会给我们生成两个json文件,launch.json和tasks.json文件,

  在HelloWorld.c文件中打上断点,我们运行结果如下

  

  

  

  到这里,我们成功完成编译环境和debug环境的设置。

 

  这两个json文件的相关变量是vscode特有的,但是和其他系统环境变量写法相似的变量,在网上找到相关说明如下: 
 

 变量名含义
${workspaceRoot} 当前打开的文件夹的绝对路径+文件夹的名字
${workspaceRootFolderName} 当前打开的文件夹的名字
${file} 当前打开正在编辑的文件名,包括绝对路径,文件名,文件后缀名
${relativeFile} 从当前打开的文件夹到当前打开的文件的路径,如当前打开的是test文件夹,当前的打开的是main.c,并有test/first/second/main.c 那么此变量代表的是 first / second / main.c
${fileBasename} 当前打开的文件名+后缀名,不包括路径
${fileBasenameNoExtension} 当前打开的文件的文件名,不包括路径和后缀名
${fileDirname} 当前打开的文件所在的绝对路径,不包括文件名
${fileExtname} 当前打开的文件的后缀名
${cwd} 任务开始运行时的当前工作目录
${lineNumber} 前打开的文件,光标所在的行数

   5 使用makefile 构建工程 

  在menu项目中我们只需要修改task.json和launch.json配置文件就可以很方便的构建工程了,最后进行测试结果如下:

  

 

 二 代码中的软件工程方法

  1 模块化软件设计

  模块化: 把程序划分成若干个模块, 每个模块完成一个子功能, 把这些模块集总起来组成一个整体,可以完成指定的功能,满足问题的功能.

模块: 一个拥有明确定义的输入、输出和特性的程序实体。
  • 将系统划分成模块
  • 决定每个模块的功能
  • 决定模块的调用关系
  • 决定模块的界面, 即模块间传递的数据

  模块化是在软件系统设计时保持系统内部各部分的相互独立,也就是降低整个系统的耦合度,以便各个系统可以被独立的设计开发。其核心思想在于“分而治之”,将一个复杂的问题分解成若干独立的小问题,然后逐一解决。通过模块化设计,用高内聚低耦合的原则来实现系统,可以极大得提高软件开发效率,降低维护成本。模块化是好的软件设计的一个基本准则,可减小钥匙所需要的总工作。

  分解 分解 高层模块 =======> 分解复杂问题 ========> 较小问题

  模块化程度是软件设计有多好的一个重要指标,一般我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度,耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。一般在软件设计中我们追求松散耦合。

  这里引用课上ppt图片,对模块化做一个形象的展示

  

 

   在一下代码中,DataNode储存了命令名、命令描述、函数和下一结点的指针,将数据结构和它的操作从菜单业务中分离出来,独立定义。

/* 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);
typedef struct DataNode
{
    char*   cmd;
    char*   desc;
    int     (*handler)();
    struct  DataNode *next;
} tDataNode;

  上面对数据进行定义,而将业务逻辑抽离出来,形成一个模块,让各个模块之间的功能,和边界界限更加鲜明,尽最大可能做到解耦,我们将业务逻辑编写在下面。

#include <stdio.h>
#include <stdlib.h>
#include "linklist.h"

int Help();

#define CMD_MAX_LEN 128
#define DESC_LEN    1024
#define CMD_NUM     10

/* menu program */

static tDataNode head[] = 
{
    {"help", "this is help cmd!", Help,&head[1]},
    {"version", "menu program v1.0", NULL, NULL}
};

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

int Help()
{
    ShowAllCmd(head);
    return 0; 
}

  将复杂的问题抽离出去,调用其接口。每一个软件模块都将只有一个单一的功能目标,并相对独立于其他软件模块,使得每一个软件模块都容易理解容易开发。也使得后期维护变得简单一些,整个软件系统更容易定位软件缺陷bug,因为每一个软件缺陷bug都局限在很少的一两个软件模块内。而且整个系统的变更和维护也更容易,因为一个软件模块内的变更只影响很少的几个软件模块。代码中体现了松散耦合的思想和最求,遵循了KISS原则。

    KISS原则:

    一行代码只做一件事

    一块代码只做一件事

    一个函数只做一件事

    一个软件模块只做一件事

    同时使用了本地化外部接口来提高代码的适用能力:

  

 

 

  2 可重用接口设计

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

    接口规格包含五个基本要素:

      接口的目的;

      接口使用前所需要满足的条件,一般称为前置条件或假定条件;

      使用接口的双方遵守的协议规范;

      接口使用之后的效果,一般称为后置条件;

      接口所隐含的质量属性。

    如上四个关键因素需要按照顺序依次评估。

  

*
 * 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_ */
1 int SearchCondition(tLinkTableNode * pLinkTableNode)
2 {
3     tDataNode * pNode = (tDataNode *)pLinkTableNode;
4     if(strcmp(pNode->cmd, cmd) == 0)
5     {
6         return  SUCCESS;  
7     }
8     return FAILURE;           
9 }  
    tNode* pTempNode = (tNode*)SearchLinkTableNode(pLinkTable,SearchCondition);

  3 可重入函数与线程安全

  基本定义

     线程安全:简单来说线程安全就是多个线程并发同一段代码时,不会出现不同的结果,我们就可以说该线程是安全的;

    线程不安全:说完了线程安全,线程不安全的问题就很好解释,如果多线程并发执行时会产生不同的结果,则该线程就是不安全的。

   线程安全产生的原因:

    大多是因为对全局变量和静态变量的操作

   常见的线程不安全的函数

     (1)不保护共享变量的函数

    (2)函数状态随着被调用,状态发生变化的函数

    (3)返回指向静态变量指针的函数

    (4)调用线程不安全函数的函数

   常见的线程安全的情况

    (1)每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的;

    (2)类或者接口对于线程来说都是原子操作;

    (3)多个线程之间的切换不会导致该接口的执行结果存在二义性;

  我们需要注意在编写可重入函数时,如果涉及到公共资源的使用,必须对其加以保护,即进行同步操作来避免产生线程安全的问题。

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

/*
 * Add a LinkTableNode to LinkTable
 */
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;
}

  4 总结

  软件设计的方法与原则:

    第一个原则:单一原则

    第二个原则:里式替换原则

    第三个原则:依赖倒置原则

    第四个原则:接口隔离原则

    第五个原则:迪米特法则

       第六个原则:组合聚合原则

    第七个原则:开闭原则

  

 

posted @ 2020-11-09 18:00  下饭java学习者  阅读(206)  评论(0)    收藏  举报