模块化设计原则:高内聚低耦合、接口隔离、依赖倒置、单一职责在嵌入式中的应用
嵌入式开发做到一定规模,你就会发现——代码好不好改,跟当初怎么切模块关系太大了。我见过太多项目,前期图快,所有功能揉在一个文件里,到后面改一个 bug 能引出三个新 bug。说白了,模块化设计不是花架子,它是你后期能不能睡安稳觉的关键。
这一章,我们聊聊四个最核心的设计原则。它们不是理论空谈,是我在多个嵌入式项目里反复验证过的实战经验。
4.1 高内聚与低耦合:模块划分的黄金标准
先问个问题:你写了一个驱动,里面既管硬件寄存器,又做协议解析,还顺带处理日志——这算一个模块吗?算,但这是个坏模块。
高内聚,就是一个模块只做一类事。比如 I2C 驱动只管收发字节,别在里面掺和传感器数据校准。低耦合,就是模块之间依赖尽量少,改一个模块不影响另一个。
我在一个车载项目中遇到过这种情况:LED 指示灯模块直接调用了 CAN 通信模块的内部结构体。后来 CAN 协议升级,结构体变了,LED 模块也得跟着改。明明只是换个灯的颜色,结果牵扯到通信层——这就是耦合的代价。
怎么判断内聚和耦合好不好?看这张图:
4.2 接口隔离原则:别让调用方看到不该看的东西
接口隔离原则,说白了就是:你提供什么功能,调用方就只能看到什么功能。别把一堆内部函数全暴露出去。
举个例子。你写了一个 EEPROM 驱动,内部有 eeprom_write_page()、eeprom_erase_sector()、eeprom_wait_busy() 这些函数。但上层应用只需要 eeprom_read() 和 eeprom_write()。那你就只暴露这两个,其他的用 static 藏起来。
我曾经接手过一个项目,一个头文件里暴露了 40 多个函数。我花了三天才搞清楚哪些是外部接口,哪些是内部函数。后来我重构时,把所有内部函数都加上了 static,头文件从 40 个函数缩减到 5 个。调用方说:终于知道该调哪个了。
4.3 依赖倒置原则:让高层模块不依赖底层细节
依赖倒置听起来玄乎,其实核心就一句话:高层模块不应该依赖底层模块,两者都应该依赖抽象接口。
在嵌入式里,最常见的场景就是:应用层直接调用了某个具体硬件的驱动函数。比如:
// 坏例子:应用层直接依赖具体硬件
void temperature_report(void) {
uint8_t data = ds18b20_read_temperature(); // 直接调 DS18B20 驱动
// ... 处理数据
}
如果哪天你要把 DS18B20 换成 SHT30,你得改应用层代码。这就是依赖了具体实现。
好的做法是定义一个抽象的温度传感器接口:
// 好例子:依赖抽象接口
typedef struct {
int16_t (*read_temperature)(void);
int8_t (*init)(void);
} temp_sensor_t;
// 应用层只依赖这个接口
void temperature_report(temp_sensor_t *sensor) {
int16_t temp = sensor->read_temperature();
// ... 处理数据
}
换传感器时,只需要换一个接口实现,应用层代码不用动。
4.4 单一职责原则:一个函数只做一件事
这个原则最容易被忽视。你想想看,你写的函数是不是经常又做校验、又做转换、又做存储、又做日志?
单一职责要求:一个函数只负责一个功能点。如果一个函数有多个「修改理由」,就该拆。
举个例子:
// 坏例子:一个函数干了三件事
void process_sensor_data(uint8_t *raw, uint16_t len) {
// 1. 校验数据
if (crc_check(raw, len) != CRC_OK) return;
// 2. 转换数据格式
float value = convert_to_physical(raw);
// 3. 存储并上报
save_to_flash(value);
send_to_can_bus(value);
}
这个函数有三个修改理由:校验算法变了要改它,转换公式变了要改它,存储方式变了也要改它。拆开之后:
// 好例子:每个函数只做一件事
static bool validate_sensor_data(uint8_t *raw, uint16_t len);
static float convert_sensor_data(uint8_t *raw);
static void store_and_report(float value);
void process_sensor_data(uint8_t *raw, uint16_t len) {
if (!validate_sensor_data(raw, len)) return;
float value = convert_sensor_data(raw);
store_and_report(value);
}
拆开之后,每个函数都可以独立测试、独立修改。我在一个工业控制项目里,把原来一个 300 行的函数拆成 8 个小函数,每个不超过 40 行。后来定位 bug 的时间缩短了至少一半。
4.5 四个原则的协同关系
这四个原则不是孤立的。它们互相配合,才能让代码真正「好改」。
| 原则 | 核心问题 | 嵌入式典型场景 | 我踩过的坑 |
|---|---|---|---|
| 高内聚低耦合 | 模块怎么切 | 驱动层、协议层、应用层分离 | LED 模块直接调 CAN 结构体 |
| 接口隔离 | 暴露什么 | 头文件只放外部接口 | 头文件 40 个函数,实际只用 5 个 |
| 依赖倒置 | 依赖谁 | 应用层依赖抽象接口,不依赖具体硬件 | 换传感器要改应用层代码 |
| 单一职责 | 函数做什么 | 校验、转换、存储分开 | 300 行函数改一个功能要通读全函数 |
嗯,这四个原则说起来简单,做起来需要刻意练习。我刚开始也经常写出「万能函数」,后来每次 review 代码时都问自己一句:这个函数有多个修改理由吗?这个模块依赖了不该依赖的东西吗?慢慢就养成习惯了。
记住一句话:好的模块化设计,是改一处代码时,不需要打开其他文件。做到这一点,你的嵌入式代码就成功了一半。