模块化软件设计

模块化的基本原理

模块化(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)原则

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

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

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

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

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

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


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

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

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
come模块设计规范是一组统一的规则和标准,用于指导开发者在设计和实现come模块时的操作和风格。以下是come模块设计规范的要点: 1. 模块功能划分:come模块应该根据功能和职责进行合理的划分,每个模块应该有明确的功能,且功能不重复。 2. 单一职责原则:每个come模块应该只负责一项功能,不要将多个功能混合在一个模块中,以提高代码的可读性和可维护性。 3. 接口设计:come模块的接口应该简洁明了,命名要有意义,参数要清晰,避免使用过于复杂或冗长的接口。 4. 函数设计:每个函数都应该有一个明确的功能,并且函数命名要见名知意。函数的输入和输出应该明确,并进行足够的异常处理。 5. 代码复用:在come模块的设计中,应该尽量避免重复造轮子,合理利用已有的代码库和框架,提高开发效率。 6. 可扩展性设计:come模块应该具备良好的扩展性,以便在未来的版本迭代中可以方便地添加新的功能和修改现有功能。 7. 清晰的文档和注释:come模块的代码应该有清晰的文档和注释,用于解释代码的功能、使用方法和注意事项,便于其他开发者阅读和理解。 8. 性能和安全考虑:come模块在设计和实现时,应该充分考虑性能和安全性,避免潜在的性能瓶颈和安全漏洞。 总之,come模块设计规范旨在提高come模块的质量和可维护性,使得开发者能够更加高效地进行come模块的开发和维护工作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青衫客36

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值