模块化设计:模块划分原则、高内聚低耦合、接口设计、模块间通信

各位同学,今天我们来聊聊模块化设计。这个话题,说白了就是怎么把一个庞大的系统拆成一个个小零件,再把这些小零件拼回去。我见过太多项目,一开始图省事,把所有代码塞到一个文件里,结果三个月后连自己都看不懂了。嗯,咱们今天就把这事儿掰扯清楚。

一、模块划分原则:别把鸡蛋放在一个篮子里

模块划分这事儿,我个人的习惯是:先看功能,再看数据流。你想想看,一个嵌入式系统里,有采集数据的、有处理数据的、有存储数据的、有通信的,这些天然就是不同的模块。

具体来说,我总结了三个原则:

  • 单一职责原则:一个模块只做一件事。比如ADC驱动模块,就只管把模拟量转成数字量,别在里面掺和什么滤波算法。
  • 接口最小化原则:暴露给外部的接口越少越好。我在项目中遇到过,有人把模块内部的结构体直接暴露出去,结果后面改结构体定义,整个项目都要重新编译。
  • 可替换性原则:模块之间应该是可以替换的。比如你用一个温度传感器,今天用DS18B20,明天想换成SHT30,只需要改驱动模块,上层代码完全不用动。

核心思想:模块划分不是越细越好,也不是越粗越好。我见过有人把每个函数都搞成一个模块,那叫过度设计。合适的粒度是:一个模块大概300-500行代码,对外暴露3-5个接口函数。

二、高内聚低耦合:模块设计的黄金法则

高内聚低耦合,这六个字我念叨了十几年。什么意思呢?

高内聚:模块内部的元素之间关系紧密。比如一个UART驱动模块,它的初始化函数、发送函数、接收函数、中断处理函数,都是围绕UART这个硬件工作的,这就是高内聚。

低耦合:模块之间的依赖关系要弱。说白了,就是A模块出问题了,B模块不应该受影响。

我曾经在一个项目中,把日志模块和存储模块耦合在一起。结果存储模块的驱动换了,日志模块也要跟着改。后来我重构了,日志模块只负责格式化数据,通过回调函数把数据交给存储模块。这样两个模块就解耦了。

我的经验:判断耦合度有个简单方法——如果你改了一个模块的代码,需要同时改三个以上的其他模块,那耦合度就太高了。

三、接口设计:模块之间的契约

接口设计是模块化设计的核心。我把它比作「合同」——双方都要遵守的约定。

接口设计有几个要点:

  • 接口要稳定:一旦发布,尽量不要改。我习惯在接口函数前面加个版本号,比如 v1_uart_send(),这样后面升级接口时,旧接口还能继续用。
  • 参数要简单:能用基本类型就别用结构体。如果必须用结构体,记得用指针传递,别传值。
  • 返回值要明确:每个接口函数都要有返回值,表示成功或失败。我见过有人写函数不返回错误码,出了问题都不知道是哪里挂了。
// 好的接口设计示例
typedef enum {
    MODULE_OK = 0,
    MODULE_ERR_PARAM,
    MODULE_ERR_TIMEOUT,
    MODULE_ERR_BUSY
} module_err_t;

// 初始化模块
module_err_t sensor_init(void);

// 读取数据
module_err_t sensor_read(float *temperature, float *humidity);

// 设置采样率
module_err_t sensor_set_rate(uint32_t rate_hz);

注意:接口函数不要做太多事情。一个函数只做一件事,这是铁律。我曾经见过一个接口函数,既初始化硬件,又校准参数,还启动DMA传输。结果出了问题,都不知道是哪个环节导致的。

四、模块间通信:让模块们好好说话

模块之间怎么通信?这是个大学问。我总结了三种常见方式:

通信方式 适用场景 优点 缺点
函数调用 同步操作,模块间关系紧密 简单直接,效率高 耦合度高,不适合跨任务
消息队列 异步操作,模块间关系松散 解耦好,支持多任务 需要额外内存,有延迟
共享内存 数据量大,实时性要求高 速度快,效率高 需要同步机制,容易出bug

我个人比较推荐消息队列的方式。为什么呢?因为消息队列天然实现了低耦合。发送方只管把消息丢进队列,接收方只管从队列里取消息,双方不需要知道对方的存在。

举个例子,我在一个物联网项目中,传感器模块采集到数据后,通过消息队列发给处理模块。处理模块处理完后,再通过另一个消息队列发给通信模块。这样三个模块完全解耦,每个模块都可以独立测试、独立升级。

// 消息队列通信示例
typedef struct {
    uint32_t msg_id;
    uint8_t data[64];
    uint32_t len;
} message_t;

// 发送消息
void msg_queue_send(message_t *msg);

// 接收消息(阻塞)
message_t* msg_queue_receive(void);

// 接收消息(非阻塞)
int msg_queue_try_receive(message_t *msg);

避坑指南:我曾经在消息队列里传递指针,结果发送方释放了内存,接收方还在用那个指针,导致野指针问题。后来我改成传值或者用引用计数,再也没出过问题。

五、模块化设计的SVG框架图

下面这张图,是我对模块化设计核心逻辑的总结。你看一眼就能明白整个体系。

模块化设计核心逻辑 模块划分原则 单一职责 接口最小化 可替换性 高内聚低耦合 模块内部紧密关联 模块之间依赖弱 独立测试与维护 接口设计 稳定契约 参数简单 返回值明确 模块间通信 函数调用 消息队列 共享内存 模块化设计 = 合理划分 + 低耦合 + 稳定接口 + 高效通信

这张图把模块化设计的四个核心要素串起来了。你从模块划分开始,然后追求高内聚低耦合,接着设计好接口,最后选择合适的通信方式。每一步都走扎实了,你的项目架构就不会差。

最后说一句:模块化设计不是一蹴而就的。我每次写代码,都会先画个模块图,把接口定义好,然后再开始写实现。这样虽然前期多花点时间,但后面调试、维护、升级的时候,你就知道有多香了。


公众号:蓝海资料掘金营,微信deep3321