模块化的基本原理

模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离 (SoC, Separation of Concerns)

关注点的分离在软件工程领域是最重要的原则,我们习惯上称为模块化,翻译成我们中文的表述其实就是“分而治之”的方法。

关注点的分离的思想背后的根源是由于人脑处理复杂问题时容易出错,把复杂问题分解成一个个简单问题,从而减少出错的情形。

模块化软件设计的方法如果应用的比较好,最终每一个软件模块都将只有一个单一的功能目标,并相对独立于其他软件模块,使得每一个软件模块都容易理解容易开发。从而整个软件系统也更容易定位软件缺陷bug,因为每一个软件缺陷bug都局限在很少的一两个软件模块内。而且整个系统的变更和维护也更容易,因为一个软件模块内的变更只影响很少的几个软件模块。

因此,软件设计中的模块化程度便成为了软件设计有多好的一个重要指标,一般我们使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度。

耦合度(Coupling)

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

  • 无耦合是指软件模块之间完全没有互相依赖关系,各自保持完全独立。
  • 松散耦合是指软件模块之间有一些依赖关系,且互相之间的依赖关系清晰、明确。
  • 紧密耦合是指软件模块之间有很多依赖关系,而且互相之间的依赖关系错综复杂。

内聚度(Cohesion)

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

模块化代码的基本写法

在开源社区中命令行菜单常见的写法是通过一个数据结构的数组来定义一组命令,从而实现命令的定义独立于菜单引擎关键代码,其中的关键是使用指针函数handler。示例代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int Help();
int Quit();

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

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

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

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");
        }
    }
    return 0;
}

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

int Quit()
{
    exit(0);
}

运行结果如下: 

        进一步,还可以将数据结构及其操作与菜单业务处理进行分离,将数据结构及其操作独立出来,与命令的定义和菜单引擎分解开来各自独立编码。从以下代码可以看到数据结构及其操作与菜单业务处理尽管还是在同一个源代码文件中,但是已经在逻辑上做了切分,可以认为进行了初步的模块化。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int Help();
int Quit();

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

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

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

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

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

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

int Quit()
{
    exit(0);
}

        进行了模块化软件设计之后,我们往往使设计的模块与实现的源代码文件有映射对应关系,menu.c和linklist.h/linklist.c模块的映射对应关系如下图所示,因此我们需要将数据结构及其操作放到单独的源代码文件中。

下面是linklist.h中定义的软件模块接口

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

tDataNode* FindCmd(tDataNode* head, char* cmd);
int ShowAllCmd(tDataNode* head);

 对应的linklist.c中软件模块的实现代码如下

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

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.c中

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

int Help();
int Quit();

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

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

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

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

int Quit()
{
    exit(0);
}

下面使用GCC 同时编译上面的两个C文件,可以分步编译,也可以一起编译。

1、分步编译

先执行

gcc -c linklist.c menu.c

再执行

gcc -o menu linklist.o menu.o

.\menu.exe

 

 2、一起编译

gcc -o menu linklist.c menu.c

.\menu.exe

软件设计中的一些基本方法

  • KISS(Keep It Simple & Stupid)原则
  • 使用本地化外部接口来提高代码的适应能力 | 不要和陌生人说话原则
  • 先写伪代码的代码结构更好一些 | using  design to frame the code(matching design with implementation)

KISS(Keep It Simple & Stupid)原则

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

使用本地化外部接口来提高代码的适应能力

先写伪代码的代码结构更好一些

设计通常为程序提供了一个框架,程序员需要用自己的专业知识和创造性来编写代码实现设计。在从设计到编码的过程中加入伪代码阶段要好于直接将设计翻译成实现代码。

因为伪代码不需要考虑异常处理等一些编程细节,最大限度地保留了设计上的框架结构,使得设计上的逻辑结构在伪代码上体现出来。从伪代码到实现代码的过程就是反复重构的过程,这样避免了顺序翻译转换所造成的结构性损失。因此,先写伪代码的代码结构会更好一些。

另外我们也要有意识地用设计上的逻辑结构给代码提供一个编写框架,避免代码的无序生长,从而破坏设计上的逻辑结构。


以上内容为中科大软件学院《高级软件工程》课后总结,感谢孟宁老师的倾心教授,老师讲的太好啦(^_^)

参考资料:《代码中的软件工程》    孟宁  编著

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐