模块化设计原则(上):高内聚低耦合、接口隔离原则、依赖倒置原则、单一职责原则

模块化设计,说白了就是把一个大系统拆成若干小块。但怎么拆,拆到什么程度,这里头门道很深。我见过不少项目,模块是分了,结果改一个功能要动七八个文件,那还不如不拆。

这一节我们聊四个最核心的原则。它们不是理论空谈,是真正能帮你少加班的实战准则。

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个。如果超过,说明这个模块的职责可能太杂了,或者接口设计不够抽象。

下面这张图展示了高内聚低耦合的理想结构:

模块化设计核心逻辑:高内聚低耦合 模块 A(高内聚) 函数 a1 函数 a2 函数 a3 函数 a4 内部紧密协作 对外只暴露 2 个接口 模块 B(低耦合) 函数 b1 函数 b2 函数 b3 通过接口调用 A 不依赖 A 的内部实现 接口调用(松耦合) 核心:模块内部高内聚(函数间紧密协作),模块之间低耦合(通过最小接口通信)

四个原则的关系

这四个原则不是孤立的。它们互相支撑:

  • 单一职责是基础——职责单一了,内聚自然高。
  • 接口隔离是手段——小接口让耦合更松散。
  • 依赖倒置是保障——抽象隔离了变化,耦合降到最低。
  • 高内聚低耦合是结果——前面三条做到了,这个自然达成。

注意:不要为了追求“完美”而过度设计。我见过有人把简单的LED驱动拆成6个接口文件,结果代码量翻了三倍,可读性反而下降。适度就好,过犹不及。

在实际项目中,我一般这样落地:先按功能划分模块(SRP),然后定义每个模块的对外接口(ISP),接口里只放抽象方法(DIP),最后检查模块间的依赖关系是否清晰简单(低耦合)。

这一套组合拳打下来,你的代码会变得非常“好伺候”。加功能、换硬件、修bug,都只影响局部。这才是模块化设计的真正价值。