接口是模块间通信的强制契约,需明确定义输入输出语义、避免实现细节、通过拆分而非默认参数实现版本兼容,并以契约测试保障各方理解一致。

接口不是语法糖,而是模块间通信的强制契约。它不保证功能实现,只约束调用方式、参数边界和响应语义——一旦定义,所有实现方必须严格遵守,否则通信就会在运行时崩断。
interface 必须声明明确的输入输出语义
很多团队把 interface 当成空壳,只写方法名和返回类型,结果不同模块对 save() 的理解千差万别:A 认为是异步落库,B 当成同步写 Flash,C 还以为会触发网络上报。这直接导致集成阶段大量“逻辑对不上”的隐性故障。
正确做法是:每个方法注释里写清三件事——
- 输入参数的物理/业务含义(比如
timeoutMs指 UART 帧间隔超时,不是整个操作总耗时) - 成功时的副作用(是否已刷写 EEPROM?是否自动重启看门狗?)
- 失败时的确定性行为(返回
null还是抛IOException?是否保证状态回滚?)
示例:void setJointAngle(int jointId, int angleDeg, int timeoutMs) 的契约必须注明:“angleDeg 范围为 -180~+180;超时后电机保持上一有效指令位置,不进入急停”。
避免在 interface 中暴露实现细节
常见错误是把缓冲区大小、中断标志位、DMA 句柄这类底层东西塞进 interface。这等于把模块绑死在某一代硬件上,后续换 STM32H7 时,整个接口要重写。
真正该暴露的只有业务动作和可观测状态:
- 用
getMotorStatus()替代readRegister(0x2F) - 用
startCalibration(CalibrationMode mode)替代sendCommand(0x05, new byte[]{0x01}) - 错误码统一走枚举
MotorError,而不是裸传int或字符串
接口里出现任何芯片手册编号、寄存器地址、内存布局描述,都是设计倒退。
版本兼容必须靠接口拆分,不能靠参数可选
有人想“向后兼容”就给方法加默认参数,比如 sendCommand(byte[] data, boolean withAck, boolean useCRC),第三版再加个 int priority。很快调用方代码里全是 sendCommand(buf, true, false, 0) 这种魔法调用,没人记得每个 false 对应哪一版语义。
可靠做法是——
- 旧接口保留不动,打上
@Deprecated并注明替代接口 - 新需求走全新
interface,比如从MotorControlV1升级到MotorControlV2 - 通过依赖注入容器绑定具体实现,让 V1 和 V2 实现类共存于系统中
这样上位机升级时可逐步切换,下位机固件也能按节奏分批更新,不会出现“一半模块认 V1,一半认 V2”的协议撕裂。
测试接口契约比测试实现更重要
单元测试常聚焦在“这个类有没有把角度算对”,但更关键的是:“当 setJointAngle() 被传入 -200 时,是否按契约返回 INVALID_ANGLE 错误,且电机状态不变?”
建议为每个 interface 写一份契约测试(Contract Test),覆盖——
- 边界值输入(最大/最小/空/null)
- 并发调用下的线程安全承诺(是否要求调用方加锁?)
- 资源异常模拟(比如模拟 UART 发送失败时的 fallback 行为)
这类测试不依赖具体实现类,只面向接口定义跑,能提前暴露实现方对契约的理解偏差。
最难的不是写出 interface,而是让所有模块负责人坐下来,一条一条确认每行注释是否达成共识。一次没对齐,后面十次集成调试都得填这个坑。

















