模块化设计原则(上):高内聚低耦合、接口隔离原则、依赖倒置原则、单一职责原则
模块化设计,说白了就是把一个大系统拆成若干小块。但怎么拆,拆到什么程度,这里头门道很深。我见过不少项目,模块是分了,结果改一个功能要动七八个文件,那还不如不拆。
这一节我们聊四个最核心的原则。它们不是理论空谈,是真正能帮你少加班的实战准则。
1. 单一职责原则(SRP)
一个模块只做一件事。听起来简单,做起来难。
我举个例子。你写了一个驱动,既要管硬件初始化,又要做数据校验,还要处理日志输出。这就有问题了。哪天日志格式要改,你动的是驱动代码,万一改坏了硬件配置怎么办?
核心判断标准:一个模块应该只有一个引起它变化的原因。
怎么落地?我个人的习惯是:写代码前先问自己三个问题——
- 这个函数/模块的名字能用一个动词概括吗?
- 如果需求变了,我需要改几个地方?
- 这个模块依赖了多少外部资源?
如果答案不理想,那就继续拆。别怕文件多,怕的是耦合深。
实战技巧:在嵌入式里,我经常把硬件抽象层(HAL)和业务逻辑层严格分开。哪怕MCU换了,业务代码一行不动。这就是SRP的威力。
2. 接口隔离原则(ISP)
接口要小而专,不要大而全。你想想看,一个传感器驱动接口里塞了20个方法,但你的温度传感器只需要读温度,剩下的19个方法都是空的或者返回错误码。这合理吗?
我曾经接手过一个项目,一个I2C设备驱动接口定义了15个函数。结果新加一个只支持单次采样的传感器,硬着头皮实现了所有接口,其中一半都是空函数。代码里全是 return -ENOSYS。后来我重构了,拆成三个小接口:Readable、Writable、Configurable。每个设备只实现自己需要的。
避坑指南:我曾经犯过一个错误——为了“统一管理”,把所有外设驱动接口合并成一个超级结构体。结果每次加新设备都要改接口定义,所有驱动文件都得重新编译。那叫一个痛苦。
接口隔离的核心就一句话:客户端不应该被迫依赖它不使用的方法。
| 场景 | 错误做法 | 正确做法 |
|---|---|---|
| 传感器驱动 | 一个接口包含读/写/配置/校准/自检 | 拆成 ISensorRead、ISensorConfig、ISensorCalibrate |
| 通信协议 | 一个接口包含发送/接收/解析/加密/压缩 | 拆成 ITransport、IParser、ICrypto |
| 存储模块 | 一个接口包含读/写/擦除/坏块管理/磨损均衡 | 拆成 IStorageIO、IBadBlockHandler、I WearLeveling |
3. 依赖倒置原则(DIP)
高层模块不应该依赖低层模块。两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
嗯,这句话有点绕。我换个说法:你的业务逻辑代码,不要直接调用硬件寄存器。中间加一层抽象。
举个例子。你写一个温度采集任务,直接调了某个MCU的ADC寄存器。三个月后换了MCU,所有代码都得重写。但如果你的业务代码只依赖一个 ITemperatureSensor 接口,底层实现随便换,上层代码一行不动。
// 错误做法:业务代码直接依赖硬件
void temperature_task(void) {
uint32_t adc_val = ADC1->DR; // 直接读寄存器
float temp = adc_val * 3.3f / 4096.0f * 100.0f;
// ... 业务处理
}
// 正确做法:业务代码依赖抽象接口
typedef struct {
float (*read_temperature)(void);
} ITemperatureSensor;
void temperature_task(ITemperatureSensor *sensor) {
float temp = sensor->read_temperature();
// ... 业务处理,完全不关心底层是ADC还是I2C
}
关键点:依赖倒置不是“不用底层”,而是“通过抽象来用底层”。这样换硬件、换驱动,都不会波及业务逻辑。
我个人习惯在项目初期就定义好接口头文件。哪怕底层还没写,先把接口定死。这样上层开发和底层开发可以并行推进,互不阻塞。
4. 高内聚低耦合
这是模块化设计的终极目标。前面三个原则,其实都是为它服务的。
- 高内聚:模块内部的元素之间关联紧密,共同完成一个明确的功能。
- 低耦合:模块之间的依赖关系简单、清晰、最小化。
怎么衡量?我有个土办法:看改代码时的影响范围。
如果改一个模块的内部实现,不需要动其他模块,那就是高内聚低耦合。反之,如果改一个bug要牵连五个模块,那你的耦合度就太高了。
实战指标:我一般要求模块间的接口函数不超过5个。如果超过,说明这个模块的职责可能太杂了,或者接口设计不够抽象。
下面这张图展示了高内聚低耦合的理想结构:
四个原则的关系
这四个原则不是孤立的。它们互相支撑:
- 单一职责是基础——职责单一了,内聚自然高。
- 接口隔离是手段——小接口让耦合更松散。
- 依赖倒置是保障——抽象隔离了变化,耦合降到最低。
- 高内聚低耦合是结果——前面三条做到了,这个自然达成。
注意:不要为了追求“完美”而过度设计。我见过有人把简单的LED驱动拆成6个接口文件,结果代码量翻了三倍,可读性反而下降。适度就好,过犹不及。
在实际项目中,我一般这样落地:先按功能划分模块(SRP),然后定义每个模块的对外接口(ISP),接口里只放抽象方法(DIP),最后检查模块间的依赖关系是否清晰简单(低耦合)。
这一套组合拳打下来,你的代码会变得非常“好伺候”。加功能、换硬件、修bug,都只影响局部。这才是模块化设计的真正价值。