4、接口设计原则(上):高内聚低耦合、接口隔离原则(ISP)在嵌入式中的落地

各位同学,咱们今天聊点实在的。接口设计这玩意儿,说白了就是给模块之间定规矩。规矩定得好,代码改起来就像换乐高积木;规矩定得烂,改一行代码能让你怀疑人生。

我做了十几年嵌入式,见过太多“接口灾难”。一个结构体里塞了三十个字段,一个驱动函数传八个参数,最后谁都不敢动。嗯,今天咱们就从两个最基础也最重要的原则开始——高内聚低耦合和接口隔离原则。

4.1 高内聚低耦合:嵌入式世界的“黄金法则”

先问个问题:你写一个传感器驱动,是把初始化、读取、校准、休眠全塞进一个函数,还是拆成几个独立的函数?

答案很明显——拆。但拆到什么程度?这就是内聚和耦合要解决的问题。

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

我习惯用一个比喻:内聚是一个模块内部“拧成一股绳”的程度;耦合是两个模块之间“纠缠不清”的程度。

  • 高内聚:模块内部的东西都是干同一件事的。比如一个温度传感器驱动,所有函数都围绕“获取温度”这个目标。
  • 低耦合:模块之间只通过明确的接口通信,不互相依赖内部实现。

核心目标:高内聚 + 低耦合 = 模块可独立开发、独立测试、独立替换。

你想想看,如果驱动层和应用层耦合在一起,换一个传感器型号,整个应用层都得重写。这谁受得了?

4.1.2 嵌入式中的内聚等级

我在项目中遇到过一种情况:一个函数既做数据采集,又做协议解析,还顺带把日志写了。这就是典型的低内聚。我把它拆成三个函数后,每个函数只干一件事,代码可读性直接翻倍。

内聚等级 描述 嵌入式示例 推荐度
功能内聚 模块内所有元素完成单一功能 一个函数只做SPI发送 ⭐⭐⭐⭐⭐
顺序内聚 元素按顺序处理同一数据 先接收、再校验、再解析 ⭐⭐⭐⭐
通信内聚 元素操作同一数据 多个函数读写同一个缓冲区 ⭐⭐⭐
逻辑内聚 元素通过参数选择执行 一个函数处理多种传感器类型 ⭐⭐
偶然内聚 元素无关联,只是放在一起 一个文件里塞了驱动、算法、UI ❌ 避免

我的习惯:写代码前先问自己——“这个模块如果被删掉,会影响几个其他模块?”如果超过3个,说明耦合太高了。

4.1.3 耦合类型与嵌入式实战

耦合这东西,分好几种。我重点说两个嵌入式里最常见的:

  • 内容耦合:一个模块直接访问另一个模块的内部数据。比如驱动层直接修改应用层的全局变量。这是最糟糕的,我曾经在一个项目里看到驱动代码直接写应用层的状态机变量,结果调试了三天才发现问题。
  • 控制耦合:通过参数传递控制信息。比如一个函数传一个“模式”参数来决定行为。这种耦合可以接受,但参数多了就要小心。

避坑指南:我曾经在一个RTOS项目里,把任务间通信全用全局变量实现。结果任务多了之后,根本不知道哪个变量被谁改了。后来全部换成消息队列,耦合度降了,调试效率升了。

4.2 接口隔离原则(ISP):别让调用者“被迫依赖”

接口隔离原则,英文叫 Interface Segregation Principle,简称 ISP。它的核心思想很简单:客户端不应该被迫依赖它不需要的接口。

说白了,你给一个模块提供接口,别一股脑全塞进去。调用者只需要它关心的那部分。

4.2.1 一个反面案例

我见过一个“万能传感器接口”,长这样:

// 反面案例:一个接口塞了所有功能
typedef struct {
    int (*init)(void);
    int (*read_temperature)(float *temp);
    int (*read_humidity)(float *humi);
    int (*read_pressure)(float *press);
    int (*calibrate)(void);
    int (*sleep)(void);
    int (*wakeup)(void);
    int (*self_test)(void);
} sensor_ops_t;

这个接口看起来挺全,但问题来了:如果我只用一个温度传感器,我为什么要实现 read_humidity 和 read_pressure?这就是典型的“被迫依赖”。

4.2.2 正确的做法:按职责拆分接口

按照 ISP,我们应该把接口拆成更小的、更专注的集合:

// 按职责拆分后的接口
typedef struct {
    int (*init)(void);
    int (*read)(float *value);
} sensor_read_t;

typedef struct {
    int (*calibrate)(void);
    int (*self_test)(void);
} sensor_maintain_t;

typedef struct {
    int (*sleep)(void);
    int (*wakeup)(void);
} sensor_power_t;

这样,温度传感器只需要实现 sensor_read_t,而带校准功能的传感器再额外实现 sensor_maintain_t。调用者只依赖它需要的接口。

关键点:接口越小,复用性越高。但也不是越小越好——太细了会增加管理成本。我一般控制在每个接口 3-5 个函数。

4.2.3 ISP 在嵌入式中的落地场景

嵌入式里 ISP 最常见的应用场景有三个:

  1. 驱动层抽象:不同外设(I2C、SPI、UART)提供不同的接口集合,而不是一个“万能总线接口”。
  2. 协议栈分层:物理层、链路层、应用层各提供独立的接口,上层不依赖下层细节。
  3. 设备管理:不同设备(传感器、执行器、显示器)只暴露它们支持的操作。

举个例子,我在一个物联网网关项目里,把设备接口拆成了三组:

接口组 包含函数 适用设备
数据采集接口 init, read, get_status 温度传感器、湿度传感器
控制输出接口 init, set, get_status 继电器、电机、LED
维护诊断接口 self_test, calibrate, reset 所有支持诊断的设备

这样,应用层代码只需要知道它操作的是哪种类型的设备,不需要关心其他接口的存在。

我的经验:接口设计时,多站在调用者的角度思考。如果调用者需要“跳过”某些函数才能用你的接口,那这个接口就该拆了。

4.3 高内聚低耦合 + ISP 的协同效果

这两个原则不是孤立的。高内聚低耦合是“宏观架构”层面的指导,ISP 是“接口设计”层面的落地方法。结合起来,效果是 1+1 > 2。

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

高内聚低耦合 + ISP 协同架构 模块A(高内聚) 模块B(高内聚) 模块C(高内聚) 接口A(ISP拆分) 接口B(ISP拆分) 接口C(ISP拆分) 调用者1 调用者2 调用者3 说明: • 左侧模块内部高内聚:每个模块只做一件事 • 中间接口按ISP拆分:调用者只依赖需要的接口 • 右侧调用者低耦合:不依赖模块内部实现,只通过接口通信

从这张图可以看出:

  • 每个模块内部是高内聚的,只做自己该做的事。
  • 接口按 ISP 拆分成小集合,调用者只依赖它需要的部分。
  • 模块之间通过接口通信,不直接访问内部数据,实现低耦合。

4.4 实战建议:从“能跑”到“好改”

最后,我分享几个实战中的小建议:

  1. 先画接口图,再写代码。我习惯在写代码前,先画出模块之间的接口关系图。这样能提前发现耦合问题。
  2. 接口参数不超过3个。如果一个函数参数超过3个,考虑用结构体封装,或者拆成多个函数。
  3. 接口函数名要“自解释”。比如 sensor_read_temperature() 比 sensor_get_data() 好得多。
  4. 定期做“接口审计”。每迭代一次,检查一下有没有接口被“膨胀”了。

避坑指南:我曾经在一个项目里,为了“方便”,把两个不相关的接口合并成一个。结果三个月后,新同事接手时完全搞不清楚哪些函数是给谁用的。从那以后,我坚持“一个接口一个职责”。