2、模块化设计原则:高内聚、低耦合的实战理解

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

今天咱们就聊聊两个核心原则——高内聚和低耦合。这不是什么玄学,是实实在在能帮你少加班的工程方法。

2.1 什么是内聚?什么是耦合?

先给个直观理解:

  • 内聚——模块内部的东西,关联度有多高。
  • 耦合——模块与模块之间,依赖有多深。

高内聚,就是模块里放的东西都是「一家人」。低耦合,就是模块之间只通过「电话」联系,别把门打通。

核心口诀:高内聚是「管好自己的事」,低耦合是「别管别人的事」。

2.2 内聚的七个层次

我在项目中遇到过一种情况:一个 .c 文件三千行,里面既有按键扫描,又有 LCD 刷屏,还有 PID 计算。这就是典型的低内聚。后来重构时,我按功能拆成了三个模块,每个模块只做一件事,维护成本直接降了一半。

内聚从低到高分七个层次,咱们只看最常用的三种:

内聚类型 描述 实战评价
功能内聚 模块内所有元素共同完成一个功能 ✅ 理想目标
顺序内聚 元素按处理顺序排列,前一个输出是后一个输入 ✅ 可接受
通信内聚 元素操作同一数据或设备 ⚠️ 需注意
偶然内聚 元素之间无关联,只是凑在一起 ❌ 坚决避免

我的习惯:写模块前先问自己——这个模块的职责能用一句话说清楚吗?说不清楚,内聚肯定有问题。

2.3 耦合的六种级别

耦合越低越好,但也不是越低越好。完全解耦意味着模块之间没法通信,那系统就散了。关键是要找到平衡点。

耦合类型 特征 实战建议
数据耦合 模块间只传递基本参数 ✅ 最理想
标记耦合 传递结构体等复合数据 ✅ 可接受
控制耦合 传递控制标志改变对方逻辑 ⚠️ 尽量少用
外部耦合 共享外部设备或文件 ⚠️ 需封装
公共耦合 多个模块读写同一全局变量 ❌ 尽量避免
内容耦合 一个模块直接访问另一个模块内部 ❌ 绝对禁止

我曾经踩过的坑:一个项目里,三个模块都直接操作同一个全局数组。后来需求变了,要改数组长度,结果三个模块全要改,还改出了两个 bug。从那以后,我规定全局变量必须通过接口函数访问。

2.4 实战中的模块划分方法

光讲理论没用,咱们看看实际怎么操作。下面这张图是我常用的模块划分思路:

模块化分层设计核心逻辑 应用层(业务逻辑) 模块层(高内聚、低耦合) 传感器模块 通信模块 存储模块 显示模块 硬件抽象层(HAL) 硬件(MCU / 外设) 模块间只通过接口通信 不直接访问内部数据 HAL 隔离硬件差异 上层不关心具体硬件

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 模块间通信的三种模式

模块之间怎么「说话」?我总结了三种常用模式:

  1. 函数调用——最直接,A 模块调用 B 模块的接口函数。适合同步操作。
  2. 回调函数——A 模块注册一个函数指针给 B,B 在特定事件发生时调用。适合事件驱动。
  3. 消息队列——模块之间通过队列传递消息。适合异步、多任务场景。
模式 耦合度 适用场景 注意事项
函数调用 低 模块间依赖明确 避免循环调用
回调函数 中 中断、定时器事件 注意回调执行时间
消息队列 低 多任务、异步处理 注意队列溢出

我的建议:能用函数调用就别用回调,能用回调就别用消息队列。越简单的通信方式,越容易调试。别为了「解耦」而过度设计。

2.7 避坑指南

最后分享几个我亲身踩过的坑:

  • 过度解耦——一个简单的读取温度操作,非要经过三层回调、两个队列。结果代码可读性极差,调试时根本不知道数据流到哪了。
  • 接口膨胀——一个模块暴露了二十多个接口函数。其实大部分是内部函数,不该暴露。接口要精简,能少就少。
  • 循环依赖——A 模块调用 B,B 又调用 A。编译时没问题,运行时死循环。我建议用分层结构,上层可以调用下层,下层绝不能调用上层。

我曾经犯过的错:一个项目里,我把所有模块的初始化函数都放在一个 init_all() 里。后来要加一个新模块,得改这个函数。再后来要删一个模块,又得改。这就是典型的「高耦合」。正确的做法是每个模块自己负责初始化,上层只负责按顺序调用。

嗯,模块化设计说到底就是两件事:管好自己的事,别管别人的事。高内聚让你模块内部清晰,低耦合让你模块之间独立。这两点做到了,你的代码就能像积木一样,想换哪块换哪块。