模块化设计原则:高内聚、低耦合的定义与在嵌入式中的具体体现

做嵌入式开发久了,你会发现一个有意思的现象:有些代码改一个功能,要牵连五六个文件;有些代码加一个新模块,就像往乐高上插积木一样简单。区别在哪?说白了,就是内聚和耦合这两个东西没处理好。

我刚开始带项目那会儿,也踩过不少坑。有一次做一款工业控制器,为了赶进度,把传感器采集、数据处理、通信协议全写在一个文件里。结果客户要换一个通信协议,我改了整整三天,还引入了一堆新 bug。从那以后,我彻底明白了——高内聚、低耦合不是理论空话,是实打实的生存法则。

什么是高内聚?

高内聚,就是一个模块内部的东西,应该紧密相关。一个模块只做一件事,并且把这件事做好。

举个例子。你写一个温度传感器驱动,这个模块应该只负责:初始化传感器、读取原始数据、做必要的校准转换。它不应该去管数据怎么显示、要不要报警、要不要存到 SD 卡。那些是别的模块的事。

高内聚的判断标准:如果你要修改一个功能,只需要改一个模块,那就是高内聚。如果需要改两三个模块,说明内聚度不够。

我个人习惯用“职责单一”来检验。问自己一个问题:这个模块的名字,能不能用一句话说清楚它干什么?如果能,内聚基本合格。如果不能,赶紧拆。

嵌入式中的内聚层次

内聚层次 说明 嵌入式例子
功能内聚 模块内所有元素完成一个功能 一个函数只做 CRC 校验
通信内聚 模块内元素处理同一数据 UART 驱动包含发送、接收、中断处理
时间内聚 模块内元素在同一时间段执行 系统初始化模块
逻辑内聚 模块内元素逻辑相似但功能不同 一个函数处理所有外设的错误码(不推荐)
偶然内聚 模块内元素无实质关联 一个文件里混着 GPIO、定时器、看门狗代码(反面典型)

我的建议:嵌入式开发中,尽量做到“功能内聚”或“通信内聚”。逻辑内聚和偶然内聚,能避就避。我曾经接手过一个项目,一个 .c 文件 3000 多行,里面什么都有——那是真正的“偶然内聚”,重构起来想哭。

什么是低耦合?

低耦合,就是模块之间的依赖关系要尽可能弱。一个模块改了,其他模块尽量不受影响。

你想想看,如果模块 A 直接调用模块 B 的内部全局变量,那 A 和 B 就紧紧绑在一起了。哪天 B 改了变量名,A 就得跟着改。这就是高耦合的典型症状。

低耦合的目标是:模块之间只通过定义良好的接口通信。接口就像合同,只要合同不变,模块内部怎么改都行。

嵌入式中的耦合类型

耦合类型 耦合度 嵌入式表现
数据耦合 低 模块间只通过参数传递基本数据类型
标记耦合 较低 模块间传递结构体指针,但只使用部分字段
控制耦合 中 一个模块传递控制标志给另一个模块,改变其行为
外部耦合 较高 多个模块依赖同一个全局变量或硬件寄存器
公共耦合 高 多个模块读写同一个全局数据结构
内容耦合 极高 一个模块直接访问另一个模块的内部数据或代码

注意:嵌入式系统中,外部耦合和公共耦合非常常见,因为要操作硬件寄存器、共享缓冲区。但这不代表不能优化。我的做法是:把硬件访问封装成 HAL 层,把共享缓冲区用队列或信号量保护起来。这样即使耦合存在,也是可控的。

高内聚与低耦合的关系

这两个原则其实是同一枚硬币的两面。高内聚是“管好自己的事”,低耦合是“别管别人的事”。

我画了一张图,帮你直观理解它们的关系:

高内聚与低耦合的关系 高内聚模块 传感器采集 数据校准 错误检测 低耦合模块间 传感器 模块 显示 模块 存储 模块 通信 模块 高内聚:模块内部功能紧密相关 | 低耦合:模块间通过接口通信,依赖最小化

左侧的绿色模块内部,三个功能(采集、校准、错误检测)紧密相关,共同完成“传感器数据获取”这一职责。右侧的紫色区域里,四个模块之间通过箭头(接口)通信,每个模块只知道自己该调用哪个接口,不知道对方内部怎么实现的。

在嵌入式中的具体体现

理论说完了,咱们看看实际代码怎么写。我以 STM32 上的一个温控系统为例。

反面案例:高耦合

// main.c - 反面案例
#include "stm32f1xx.h"

int temperature_raw;
int temperature_celsius;
uint8_t display_buffer[16];

void read_sensor(void) {
    // 直接操作 ADC 寄存器
    ADC1->CR2 |= ADC_CR2_SWSTART;
    while(!(ADC1->SR & ADC_SR_EOC));
    temperature_raw = ADC1->DR;
    // 直接做转换
    temperature_celsius = temperature_raw * 3300 / 4096 / 10;
}

void update_display(void) {
    // 直接操作 LCD 引脚
    GPIOB->ODR = (GPIOB->ODR & ~0xFF) | (temperature_celsius & 0xFF);
    // 延时
    for(int i=0; i<1000; i++);
}

int main(void) {
    while(1) {
        read_sensor();
        update_display();
    }
}

这段代码的问题在哪?到处都是。ADC 操作、温度转换、显示控制全混在一起。全局变量满天飞。想换一个显示方式?得改 main.c。想换一个传感器?还是得改 main.c。这就是典型的内容耦合 + 公共耦合。

正面案例:高内聚、低耦合

// sensor.h - 传感器模块接口
#ifndef SENSOR_H
#define SENSOR_H

typedef struct {
    int raw_value;
    int celsius;
} sensor_data_t;

void sensor_init(void);
sensor_data_t sensor_read(void);

#endif

// sensor.c - 传感器模块实现(高内聚)
#include "sensor.h"
#include "stm32f1xx.h"

static void adc_init(void) {
    // ADC 初始化代码
}

static int adc_read(void) {
    ADC1->CR2 |= ADC_CR2_SWSTART;
    while(!(ADC1->SR & ADC_SR_EOC));
    return ADC1->DR;
}

static int convert_to_celsius(int raw) {
    return raw * 3300 / 4096 / 10;
}

void sensor_init(void) {
    adc_init();
}

sensor_data_t sensor_read(void) {
    sensor_data_t data;
    data.raw_value = adc_read();
    data.celsius = convert_to_celsius(data.raw_value);
    return data;
}

// display.h - 显示模块接口
#ifndef DISPLAY_H
#define DISPLAY_H

void display_init(void);
void display_show_number(int value);

#endif

// display.c - 显示模块实现(高内聚)
#include "display.h"
#include "stm32f1xx.h"

void display_init(void) {
    // LCD 初始化
}

void display_show_number(int value) {
    // 只负责显示数字
    GPIOB->ODR = (GPIOB->ODR & ~0xFF) | (value & 0xFF);
}

// main.c - 主程序(低耦合)
#include "sensor.h"
#include "display.h"

int main(void) {
    sensor_init();
    display_init();
    
    while(1) {
        sensor_data_t data = sensor_read();
        display_show_number(data.celsius);
    }
}

关键变化:

  • sensor.c 内部三个 static 函数只做传感器相关的事——高内聚
  • main.c 只调用 sensor_read() 和 display_show_number(),不知道内部细节——低耦合
  • 想换传感器?只改 sensor.c。想换显示器?只改 display.c。互不影响。

嵌入式中的特殊考量

嗯,这里要注意。嵌入式系统和纯软件项目不太一样。我们有资源限制、实时性要求、硬件依赖。所以高内聚低耦合在嵌入式里有一些特殊体现:

  • 硬件抽象层(HAL):把硬件操作封装起来,上层代码不直接操作寄存器。这是低耦合的典型实践。
  • 中断处理:中断服务函数应该尽量短,只做标记或数据搬运,具体处理放到任务级。这是功能内聚的体现。
  • 模块初始化顺序:每个模块的 init 函数只初始化自己依赖的硬件,不依赖其他模块的初始化结果。这样可以降低耦合。
  • 回调函数:用函数指针实现模块间的解耦。比如定时器模块提供一个注册回调的接口,具体做什么由调用方决定。

避坑指南:我曾经在一个项目中,为了追求“绝对的低耦合”,把每个功能都拆成独立的模块,结果模块之间的接口参数多达十几个,调用链又长又复杂。后来我意识到,低耦合不是零耦合。合理的耦合是必要的,关键是控制耦合的强度和方向。我的经验是:模块间的依赖关系应该形成有向无环图,不要出现循环依赖。

如何在实际项目中落地

说了这么多,具体怎么落地?我总结了一个三步法:

  1. 先画模块图:写代码之前,先在纸上画出模块划分。每个模块一个方框,箭头表示依赖关系。如果箭头交叉太多,说明划分有问题。
  2. 定义接口契约:每个模块的 .h 文件就是它的“合同”。接口要简洁、稳定。我习惯在接口注释里写明:输入是什么、输出是什么、有什么副作用。
  3. 用工具检查:代码写完后,可以用静态分析工具检查模块间的依赖关系。比如用 gcc -M 生成依赖树,看看有没有不该有的包含关系。

记住一句话:高内聚让你改代码时只改一处,低耦合让你改代码时不影响别处。这两条原则做到了,你的嵌入式代码就会像搭积木一样,灵活、可靠、好维护。