跳转到内容
新建笔记

类接口设计:契约、依赖、兼容性与测试

接口是调用者与实现之间的约定:调用者必须提供什么,实现保证什么,失败时如何处理。一个接口可以是函数、类、消息或数据格式;不必先写一个名字以 I 开头的抽象类。

本页给出设计步骤;SOLID 原则用于评估变化边界,Qt 时间依赖注入展示如何把接口落实为可运行、可测试的代码。

1. 从调用场景定义边界

跳转到“1. 从调用场景定义边界”

先写一句具体需求,例如:“界面读取当前时间,并按统一格式显示。”然后列出真实变化:时间可能来自系统或测试替身,格式可能随产品要求变化,界面可能换成命令行。

边界应隐藏这些变化,而不是机械地把每个方法拆成一个类。对同一组数据、规则和生命周期紧密协作的操作,可以留在同一模块。参数很多时,先判断是否表达同一个概念;EmailMessage 可以集中收件人、主题和正文,但不应把无关选项塞入一个任意字典。

契约项必须回答的问题例子
输入类型、单位、范围是什么?采样率用 Hz,必须为正数
输出结果代表什么,顺序和单位是什么?返回时域样本,不混入缩放后的频谱
前置条件调用者需要先完成什么?设备已打开,缓冲区长度足够
失败返回错误、空结果还是异常?“未找到用户”和“数据库故障”分开
副作用是否修改文件、设备或共享状态?保存操作可能产生新版本
生命周期谁创建、谁拥有、谁销毁?引用借用对象,所有者必须活得更久
并发与时序能否跨线程、是否阻塞、是否可取消?GUI 线程只提交异步采集请求

这些项目把调用假设变成可观察的规则。C++ Core Guidelines 的接口章节强调显式契约、类型约束与所有权边界。

例如 addUser() 不应只写“失败返回 null”。至少要说明参数无效、用户名冲突和存储不可用各自怎样报告;正常返回的 ID 是否已持久化;调用方重试是否可能产生重复记录。

3. 选择足够简单的实现形式

跳转到“3. 选择足够简单的实现形式”
需要可以采用的形式额外成本
输入确定、计算直接普通函数或具体类成本最低,容易独立验证
一处替换一个小操作函数参数或回调需明确捕获对象的生命周期
多种实现遵守同一组操作约定抽象类或协议接口需要定义行为契约和替换测试
包装不匹配的旧接口适配器要说明转换时丢失或新增的语义
创建逻辑复杂且重复工厂需明确返回对象的所有权

继承表达“可替换”,组合表达“由这些能力协作完成”。不要用继承只为复用几行代码,也不必为了可能永远不会发生的变化给所有类增加一层接口。

4. 文档要让使用者能独立完成一次调用

跳转到“4. 文档要让使用者能独立完成一次调用”

以“创建用户”为例,文档可以按下面顺序组织:

  1. 用途:创建满足唯一性规则的新用户。
  2. 参数:哪些字段必填,字符编码与长度限制是什么。
  3. 成功:返回 ID,说明事务是否已经提交。
  4. 失败:逐类列出无效输入、重复对象和外部存储失败。
  5. 示例:一次成功调用与一次冲突处理。
  6. 约束:线程安全、超时、重试和日志中允许记录的字段。

注释重点解释这些契约,而不是把 addUser 翻译成“添加用户”。测试也应对应契约,而不是只检查每个方法都被调用过。

5. 兼容性不能靠“只增加”判断

跳转到“5. 兼容性不能靠“只增加”判断”

旧记录认为增加方法或参数通常不会破坏兼容性,这不成立。兼容性至少有三种:

  • 源码兼容:旧调用者重新编译是否仍通过。
  • 二进制兼容:旧编译产物能否直接使用新库。
  • 行为兼容:相同输入和使用方式是否保持约定结果。

增加必填参数会破坏旧调用;向接口增加必须实现的方法会影响已有实现类;改变默认值、排序或异常类型可能让代码继续编译却改变行为。Microsoft 的库兼容性说明提供了这些类别的具体例子。不同语言、运行时和 C++ ABI 需要分别判断,不能直接跨平台套用结论。

发布前先列出调用方与实现方,再选择保留旧入口、添加重载、引入版本化接口或提供过渡适配器。过渡期应有明确结束条件和迁移示例。

测试检查的约定
正常输入输出值、单位和顺序符合约定
边界与无效输入没有越界、隐式截断或含糊失败
替换实现相同契约测试对真实实现与替身均适用
生命周期借用对象有效,转移所有权明确
集成数据库、设备或网络异常能被正确映射
升级兼容老调用方式仍保持承诺的行为

接口设计结束时,应能让另一个人仅凭契约实现替身,并让调用方不需要了解内部数据库、具体驱动或隐藏全局状态。