接口是调用者与实现之间的约定:调用者必须提供什么,实现保证什么,失败时如何处理。一个接口可以是函数、类、消息或数据格式;不必先写一个名字以 I 开头的抽象类。
本页给出设计步骤;SOLID 原则用于评估变化边界,Qt 时间依赖注入展示如何把接口落实为可运行、可测试的代码。
1. 从调用场景定义边界
跳转到“1. 从调用场景定义边界”先写一句具体需求,例如:“界面读取当前时间,并按统一格式显示。”然后列出真实变化:时间可能来自系统或测试替身,格式可能随产品要求变化,界面可能换成命令行。
边界应隐藏这些变化,而不是机械地把每个方法拆成一个类。对同一组数据、规则和生命周期紧密协作的操作,可以留在同一模块。参数很多时,先判断是否表达同一个概念;EmailMessage 可以集中收件人、主题和正文,但不应把无关选项塞入一个任意字典。
2. 把契约写完整
跳转到“2. 把契约写完整”| 契约项 | 必须回答的问题 | 例子 |
|---|---|---|
| 输入 | 类型、单位、范围是什么? | 采样率用 Hz,必须为正数 |
| 输出 | 结果代表什么,顺序和单位是什么? | 返回时域样本,不混入缩放后的频谱 |
| 前置条件 | 调用者需要先完成什么? | 设备已打开,缓冲区长度足够 |
| 失败 | 返回错误、空结果还是异常? | “未找到用户”和“数据库故障”分开 |
| 副作用 | 是否修改文件、设备或共享状态? | 保存操作可能产生新版本 |
| 生命周期 | 谁创建、谁拥有、谁销毁? | 引用借用对象,所有者必须活得更久 |
| 并发与时序 | 能否跨线程、是否阻塞、是否可取消? | GUI 线程只提交异步采集请求 |
这些项目把调用假设变成可观察的规则。C++ Core Guidelines 的接口章节强调显式契约、类型约束与所有权边界。
例如 addUser() 不应只写“失败返回 null”。至少要说明参数无效、用户名冲突和存储不可用各自怎样报告;正常返回的 ID 是否已持久化;调用方重试是否可能产生重复记录。
3. 选择足够简单的实现形式
跳转到“3. 选择足够简单的实现形式”| 需要 | 可以采用的形式 | 额外成本 |
|---|---|---|
| 输入确定、计算直接 | 普通函数或具体类 | 成本最低,容易独立验证 |
| 一处替换一个小操作 | 函数参数或回调 | 需明确捕获对象的生命周期 |
| 多种实现遵守同一组操作约定 | 抽象类或协议接口 | 需要定义行为契约和替换测试 |
| 包装不匹配的旧接口 | 适配器 | 要说明转换时丢失或新增的语义 |
| 创建逻辑复杂且重复 | 工厂 | 需明确返回对象的所有权 |
继承表达“可替换”,组合表达“由这些能力协作完成”。不要用继承只为复用几行代码,也不必为了可能永远不会发生的变化给所有类增加一层接口。
4. 文档要让使用者能独立完成一次调用
跳转到“4. 文档要让使用者能独立完成一次调用”以“创建用户”为例,文档可以按下面顺序组织:
- 用途:创建满足唯一性规则的新用户。
- 参数:哪些字段必填,字符编码与长度限制是什么。
- 成功:返回 ID,说明事务是否已经提交。
- 失败:逐类列出无效输入、重复对象和外部存储失败。
- 示例:一次成功调用与一次冲突处理。
- 约束:线程安全、超时、重试和日志中允许记录的字段。
注释重点解释这些契约,而不是把 addUser 翻译成“添加用户”。测试也应对应契约,而不是只检查每个方法都被调用过。
5. 兼容性不能靠“只增加”判断
跳转到“5. 兼容性不能靠“只增加”判断”旧记录认为增加方法或参数通常不会破坏兼容性,这不成立。兼容性至少有三种:
- 源码兼容:旧调用者重新编译是否仍通过。
- 二进制兼容:旧编译产物能否直接使用新库。
- 行为兼容:相同输入和使用方式是否保持约定结果。
增加必填参数会破坏旧调用;向接口增加必须实现的方法会影响已有实现类;改变默认值、排序或异常类型可能让代码继续编译却改变行为。Microsoft 的库兼容性说明提供了这些类别的具体例子。不同语言、运行时和 C++ ABI 需要分别判断,不能直接跨平台套用结论。
发布前先列出调用方与实现方,再选择保留旧入口、添加重载、引入版本化接口或提供过渡适配器。过渡期应有明确结束条件和迁移示例。
6. 用测试验证边界
跳转到“6. 用测试验证边界”| 测试 | 检查的约定 |
|---|---|
| 正常输入 | 输出值、单位和顺序符合约定 |
| 边界与无效输入 | 没有越界、隐式截断或含糊失败 |
| 替换实现 | 相同契约测试对真实实现与替身均适用 |
| 生命周期 | 借用对象有效,转移所有权明确 |
| 集成 | 数据库、设备或网络异常能被正确映射 |
| 升级兼容 | 老调用方式仍保持承诺的行为 |
接口设计结束时,应能让另一个人仅凭契约实现替身,并让调用方不需要了解内部数据库、具体驱动或隐藏全局状态。