2、模块化设计原则:高内聚、低耦合的实战理解
模块化设计,说白了就是把一个复杂系统拆成若干小块。但怎么拆,拆到什么程度,这里面的门道很深。我见过不少项目,模块是分了,结果改一个功能要动七八个文件,那还不如不拆。
今天咱们就聊聊两个核心原则——高内聚和低耦合。这不是什么玄学,是实实在在能帮你少加班的工程方法。
2.1 什么是内聚?什么是耦合?
先给个直观理解:
- 内聚——模块内部的东西,关联度有多高。
- 耦合——模块与模块之间,依赖有多深。
高内聚,就是模块里放的东西都是「一家人」。低耦合,就是模块之间只通过「电话」联系,别把门打通。
核心口诀:高内聚是「管好自己的事」,低耦合是「别管别人的事」。
2.2 内聚的七个层次
我在项目中遇到过一种情况:一个 .c 文件三千行,里面既有按键扫描,又有 LCD 刷屏,还有 PID 计算。这就是典型的低内聚。后来重构时,我按功能拆成了三个模块,每个模块只做一件事,维护成本直接降了一半。
内聚从低到高分七个层次,咱们只看最常用的三种:
| 内聚类型 | 描述 | 实战评价 |
|---|---|---|
| 功能内聚 | 模块内所有元素共同完成一个功能 | ✅ 理想目标 |
| 顺序内聚 | 元素按处理顺序排列,前一个输出是后一个输入 | ✅ 可接受 |
| 通信内聚 | 元素操作同一数据或设备 | ⚠️ 需注意 |
| 偶然内聚 | 元素之间无关联,只是凑在一起 | ❌ 坚决避免 |
我的习惯:写模块前先问自己——这个模块的职责能用一句话说清楚吗?说不清楚,内聚肯定有问题。
2.3 耦合的六种级别
耦合越低越好,但也不是越低越好。完全解耦意味着模块之间没法通信,那系统就散了。关键是要找到平衡点。
| 耦合类型 | 特征 | 实战建议 |
|---|---|---|
| 数据耦合 | 模块间只传递基本参数 | ✅ 最理想 |
| 标记耦合 | 传递结构体等复合数据 | ✅ 可接受 |
| 控制耦合 | 传递控制标志改变对方逻辑 | ⚠️ 尽量少用 |
| 外部耦合 | 共享外部设备或文件 | ⚠️ 需封装 |
| 公共耦合 | 多个模块读写同一全局变量 | ❌ 尽量避免 |
| 内容耦合 | 一个模块直接访问另一个模块内部 | ❌ 绝对禁止 |
我曾经踩过的坑:一个项目里,三个模块都直接操作同一个全局数组。后来需求变了,要改数组长度,结果三个模块全要改,还改出了两个 bug。从那以后,我规定全局变量必须通过接口函数访问。
2.4 实战中的模块划分方法
光讲理论没用,咱们看看实际怎么操作。下面这张图是我常用的模块划分思路:
2.5 代码层面的实战技巧
光画图不够,咱们看代码。下面是一个典型的「低耦合」接口设计:
/* ========== sensor.h ========== */
#ifndef SENSOR_H
#define SENSOR_H
#include <stdint.h>
/* 只暴露接口,不暴露内部结构 */
void sensor_init(void);
int16_t sensor_read_temperature(void);
int16_t sensor_read_humidity(void);
#endif
/* ========== sensor.c ========== */
#include "sensor.h"
/* 内部结构对外完全隐藏 */
static int16_t raw_to_temperature(uint16_t raw) {
return (int16_t)(raw * 0.1f - 40);
}
void sensor_init(void) {
/* 初始化 I2C、GPIO 等 */
}
int16_t sensor_read_temperature(void) {
uint16_t raw = 0;
/* 读取硬件寄存器 */
return raw_to_temperature(raw);
}
关键点:外部只需要知道 sensor_init() 和 sensor_read_temperature(),完全不需要知道内部用了 I2C 还是 SPI,更不需要知道原始数据怎么换算。这就是信息隐藏,也是低耦合的基础。
2.6 模块间通信的三种模式
模块之间怎么「说话」?我总结了三种常用模式:
- 函数调用——最直接,A 模块调用 B 模块的接口函数。适合同步操作。
- 回调函数——A 模块注册一个函数指针给 B,B 在特定事件发生时调用。适合事件驱动。
- 消息队列——模块之间通过队列传递消息。适合异步、多任务场景。
| 模式 | 耦合度 | 适用场景 | 注意事项 |
|---|---|---|---|
| 函数调用 | 低 | 模块间依赖明确 | 避免循环调用 |
| 回调函数 | 中 | 中断、定时器事件 | 注意回调执行时间 |
| 消息队列 | 低 | 多任务、异步处理 | 注意队列溢出 |
我的建议:能用函数调用就别用回调,能用回调就别用消息队列。越简单的通信方式,越容易调试。别为了「解耦」而过度设计。
2.7 避坑指南
最后分享几个我亲身踩过的坑:
- 过度解耦——一个简单的读取温度操作,非要经过三层回调、两个队列。结果代码可读性极差,调试时根本不知道数据流到哪了。
- 接口膨胀——一个模块暴露了二十多个接口函数。其实大部分是内部函数,不该暴露。接口要精简,能少就少。
- 循环依赖——A 模块调用 B,B 又调用 A。编译时没问题,运行时死循环。我建议用分层结构,上层可以调用下层,下层绝不能调用上层。
我曾经犯过的错:一个项目里,我把所有模块的初始化函数都放在一个 init_all() 里。后来要加一个新模块,得改这个函数。再后来要删一个模块,又得改。这就是典型的「高耦合」。正确的做法是每个模块自己负责初始化,上层只负责按顺序调用。
嗯,模块化设计说到底就是两件事:管好自己的事,别管别人的事。高内聚让你模块内部清晰,低耦合让你模块之间独立。这两点做到了,你的代码就能像积木一样,想换哪块换哪块。