4. ES Modules 规范

说实话,我见过太多C语言项目,一开始跑得挺欢,三个月后就成了“屎山”。

为什么?因为大家只关注功能实现,没人管模块怎么划分、接口怎么设计。今天我们就聊聊模块化设计,这是我从“码农”走向“架构师”的关键一步。

核心观点:模块化不是把代码拆成多个文件就完事了。它是一套完整的设计哲学,包含内聚、耦合、接口、依赖四个维度。

一、高内聚低耦合:模块设计的“黄金法则”

先问个问题:你改一个模块的代码,会不会导致其他模块也跟着改?

如果答案是“会”,那你的耦合度就太高了。

高内聚,说白了就是一个模块只干一件事,而且干得漂亮。比如一个“串口驱动模块”,它只负责收发数据,不关心数据怎么解析、怎么显示。

低耦合,就是模块之间尽量少“说话”。非要说话时,也要通过明确的接口,而不是直接访问对方的全局变量。

我的经验:我在做智能家居网关项目时,有个同事把WiFi配置、MQTT连接、数据解析全写在一个文件里。后来要换WiFi模块,改了三天,还引入了三个新bug。这就是典型的“高耦合低内聚”。

怎么判断内聚和耦合是否合理?我有个简单方法:

  • 如果模块内部函数之间频繁调用,说明内聚高
  • 如果模块A要频繁调用模块B的内部函数,说明耦合高
  • 如果改模块A时,需要同步改模块B、C、D,说明耦合失控了

二、模块划分策略:别凭感觉来

模块怎么切?很多人凭直觉,结果切出来的模块要么太粗(一个文件几千行),要么太细(一个函数一个文件)。

我常用的划分策略有三种:

策略 适用场景 例子
按功能划分 业务逻辑清晰的项目 传感器模块、显示模块、存储模块
按层次划分 嵌入式系统、驱动开发 硬件抽象层、中间件层、应用层
按数据流划分 数据处理、协议栈 接收模块、解析模块、处理模块、发送模块

我个人习惯用“功能+层次”混合策略。比如一个物联网终端项目:

  • 硬件抽象层:gpio.c、uart.c、spi.c
  • 中间件层:mqtt.c、json_parser.c、ota.c
  • 应用层:sensor_manager.c、data_report.c

避坑指南:我曾经把一个“工具函数模块”搞成了“垃圾堆”。什么字符串处理、CRC校验、时间转换全塞进去,最后这个模块有2000多行,谁都不敢动。记住:工具函数也要按功能细分,比如string_util.c、crc_util.c、time_util.c。

三、接口设计原则:头文件就是“合同”

在C语言里,接口就是头文件。头文件设计得好,模块之间合作就顺畅;设计得烂,那就是一场灾难。

我总结了几条铁律:

  1. 头文件只暴露“必要”的内容:能放在.c里的就别放在.h里。内部函数、内部宏、内部结构体,统统藏起来。
  2. 接口要稳定:一旦发布,尽量不改。实在要改,也要向前兼容。
  3. 命名要清晰:模块名_功能名,比如uart_send()、uart_recv()。

来看一个“好”的头文件示例:

/* uart_driver.h */
#ifndef UART_DRIVER_H
#define UART_DRIVER_H

#include <stdint.h>

/* 初始化串口,波特率可配置 */
int uart_init(uint32_t baudrate);

/* 发送数据,返回实际发送字节数 */
int uart_send(const uint8_t *data, uint32_t len);

/* 接收数据,非阻塞,返回实际接收字节数 */
int uart_recv(uint8_t *buf, uint32_t max_len);

#endif /* UART_DRIVER_H */

你看,这个头文件只暴露了三个函数。用户不需要知道内部用了哪个定时器、哪个DMA通道。这就是“接口隔离”。

小技巧:我习惯在头文件里加注释,说明每个函数的“前置条件”和“后置条件”。比如“调用uart_init之前,必须先调用system_clock_init”。这能帮调用者少踩很多坑。

四、模块间的依赖管理:别让依赖“绕圈”

依赖管理,说白了就是“谁依赖谁”。最怕的是循环依赖:A依赖B,B依赖C,C又依赖A。这种代码,编译能过,但维护起来想哭。

怎么避免?我推荐“单向依赖”原则:

  • 上层模块依赖下层模块
  • 下层模块绝不依赖上层模块
  • 同层模块尽量不互相依赖

举个例子,一个典型的嵌入式项目依赖关系:

应用层 (app_main.c)
    ↓
中间件层 (mqtt.c, json.c)
    ↓
硬件抽象层 (uart.c, gpio.c)
    ↓
硬件层 (寄存器操作)

这种结构,你改应用层代码,不会影响硬件抽象层。反过来,换硬件平台,只需要改硬件抽象层,应用层不用动。

我曾经踩过的坑:有个项目,模块A和模块B互相调用对方的函数。后来要加一个新功能,改了一个模块,另一个模块就崩了。最后花了三天重构,把公共功能抽到模块C里,才解决。记住:循环依赖是毒药,发现一个消灭一个。

五、知识体系总览

下面这张图,是我对模块化设计的理解。你可以把它当作“检查清单”,每次设计模块时对照一下。

模块化设计知识体系 模块化设计 高内聚低耦合 模块划分策略 接口设计原则 依赖管理 单一职责 最小依赖 信息隐藏 按功能划分 按层次划分 按数据流划分 最小暴露 接口稳定 命名清晰 单向依赖 避免循环 层次分明 好的模块化设计 = 高内聚 + 低耦合 + 清晰接口 + 单向依赖 改一处,不影响全局;加功能,不破坏原有结构

嗯,模块化设计就聊到这儿。记住一句话:好的模块化设计,改一处不影响全局;加功能不破坏原有结构。这不是理论,是每个C工程师必须掌握的实战技能。

最后送你一个检查清单:

  • ☐ 每个模块是否只做一件事?
  • ☐ 模块之间是否通过接口通信,而不是全局变量?
  • ☐ 头文件是否只暴露了必要内容?
  • ☐ 依赖关系是否单向?有没有循环依赖?
  • ☐ 改一个模块,是否需要改其他模块?

如果全部打勾,恭喜你,你的代码已经具备工程化落地的能力了。