模块化设计基础:模块划分原则、接口设计规范、高内聚低耦合实战、模块间通信机制
好,咱们进入第二章。上一章聊了为什么要模块化,这一章直接上干货——怎么分模块、怎么定接口、怎么让模块之间不打架。
我见过太多项目,一开始图省事,把所有功能塞进一个 main.c。结果呢?三个月后,改一个定时器参数,莫名其妙把 SPI 通信搞崩了。这种痛,经历过一次就够了。
模块划分原则:别拍脑袋,要有章法
模块划分不是拍脑袋。我个人习惯,拿到需求先画一张「功能依赖图」。把每个独立的功能块圈出来,看看谁依赖谁。
核心原则就三条:
- 单一职责——一个模块只做一件事,做好它
- 接口最小化——暴露给外部的 API 越少越好
- 依赖单向——模块之间不要形成循环依赖
举个例子。一个温控系统,我习惯拆成:温度采集模块、PID 计算模块、加热控制模块、用户界面模块。每个模块各司其职,谁也不越界。
我的经验: 模块粒度控制在 3~8 个函数为宜。太细了管理成本高,太粗了复用性差。我曾经把一个驱动拆成 20 个小文件,结果自己都找不着函数在哪。
接口设计规范:接口就是合同
接口设计,说白了就是签合同。合同写得模糊,后面全是扯皮。
我总结了一套接口设计 checklist,每次写接口前过一遍:
| 检查项 | 说明 | 反面案例 |
|---|---|---|
| 参数类型明确 | 用 typedef 定义专用类型,别裸用 int | void setTemp(int t); |
| 返回值统一 | 统一用 int 返回错误码,0 成功,负值错误 | 有的返回指针,有的返回 bool |
| 命名规范 | 模块前缀 + 动作 + 对象 | temp_set_target() 优于 set_temperature() |
| 头文件自包含 | 每个 .h 能独立编译,不依赖前置包含 | 忘了 #include 就编译不过 |
// 好的接口示例
typedef enum {
TEMP_OK = 0,
TEMP_ERR_SENSOR = -1,
TEMP_ERR_RANGE = -2
} temp_err_t;
temp_err_t temp_set_target(int16_t temp_celsius);
temp_err_t temp_get_current(int16_t *out_temp);
避坑指南: 我曾经在接口里直接传结构体指针,结果模块内部改了结构体布局,外部全部编译失败。后来我强制用不透明指针 + 访问函数,接口稳定多了。
高内聚低耦合实战:别让模块互相「串门」
高内聚,就是模块内部的东西自己消化。低耦合,就是模块之间只通过接口说话,别直接访问对方的全局变量。
我常用的手法:
- 全局变量私有化——所有全局变量加 static,只通过 getter/setter 暴露
- 回调函数解耦——A 模块需要通知 B 模块时,用注册回调的方式,而不是直接调用
- 状态机封装——模块内部状态对外不可见,外部只发事件
// 低耦合示例:通过回调解耦
typedef void (*temp_alert_cb_t)(int16_t temp);
static temp_alert_cb_t alert_cb = NULL;
void temp_register_alert_cb(temp_alert_cb_t cb) {
alert_cb = cb;
}
static void temp_check_alert(void) {
if (alert_cb && current_temp > TEMP_ALERT_THRESHOLD) {
alert_cb(current_temp);
}
}
实战技巧: 我习惯在模块头文件里只放接口声明,所有实现细节藏进 .c 文件。头文件里不要出现 #define 内部常量,更不要暴露内部结构体。这样别人看你的头文件,一眼就知道这个模块能干什么。
模块间通信机制:选对路子,事半功倍
模块之间总要通信。选什么机制,取决于场景。
| 通信方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 直接函数调用 | 同步、紧耦合、性能敏感 | 简单、零延迟 | 耦合度高 |
| 回调函数 | 事件驱动、异步通知 | 解耦好、灵活 | 回调地狱、调试困难 |
| 消息队列 | 多任务、异步、缓冲 | 解耦彻底、可缓冲 | 额外内存、延迟 |
| 共享变量 + 互斥 | 实时性要求高、数据量小 | 速度快 | 容易出竞态 |
| 全局事件标志 | 状态同步、简单通知 | 轻量、无数据拷贝 | 只能传标志位 |
我个人偏好:能用回调就别用全局变量,能用消息队列就别用共享内存。但也不是绝对的——在中断服务函数里,我通常只用标志位或消息队列,绝不在中断里做复杂调用。
血的教训: 我曾经在中断里直接调用另一个模块的函数,那个函数里有个互斥锁,结果死锁了。从那以后,我规定中断里只能发消息或置标志,所有处理挪到任务级。
模块化设计知识体系
下面这张图,是我做模块化设计时脑子里始终绷着的一根弦。它把模块划分、接口设计、高内聚低耦合、通信机制串在了一起。