接口契约定义要素
接口契约是模块之间稳定交互的“法律条文”——它不仅描述“做什么”,还定义“在什么条件下做、做完后保证什么、过程中哪些规则不能破坏”。缺少任意一个要素,依赖方向就可能在运行期偏离设计预期。
六要素总览
| 要素 | 作用域 | 违反后果 |
|---|---|---|
| 接口名称 | 身份标识 | 调用者找不到正确入口,版本混乱 |
| 输入/输出参数 | 数据交换 | 类型不匹配、字段缺失、空指针 |
| 前置条件 | 调用安全 | 参数非法、状态未就绪 |
| 后置条件 | 结果保证 | 状态未更新、资源未释放 |
| 不变式 | 全局约束 | 数据结构损坏、循环依赖 |
| 错误码约定 | 异常路径 | 调用者无法区分错误类型 |
架构价值:完整契约让模块边界可验证,依赖方向从“推测”变为“证明”。在大型系统中,70%以上的跨模块故障源于契约定义不完整。
接口名称——单向依赖的锚点
接口名称不是随意命名的字符串,它隐含了“谁依赖谁”的方向信息。推荐命名规则:
- 动词+名词:如
StoreUser、QueryInventory,避免模糊的Handle、Process。 - 方向前缀:为上层提供的接口使用
I或Api前缀;为下层回掉使用On、Callback。 - 版本嵌入:如
v2/OrderSubmit,避免二进制兼容性泄漏。
输入输出参数——数据流向的显式化
参数契约必须明确:
- 每个参数的类型、范围、是否可为 null、最大值/最小值。
- 输出的结构是否包含内部ID或敏感字段。
- 参数的生命周期:调用者分配?被调用者释放?
// ⚠️ 反模式:隐式假设
// 调用者必须自己检查 user 是否为空,但契约没说
UserInfo GetUser(string userId);
// ✅ 正确模式:显式前置条件约束
/// @pre userId 不为空且长度 <= 64
/// @post 返回对象非空,且 user.IsValid() == true
/// @invariant 全局用户缓存的哈希索引不变
UserInfo GetUser(string userId);
前置条件与后置条件——信赖链的左右边界
前置条件定义了“调用者必须满足的状态”;后置条件定义了“被调用者保证输出的状态”。两者共同构成模块间信任的最小承诺。
关键依赖方向原则:上层(调用者)必须满足前置条件,下层(被调者)必须兑现后置条件。下层不能依赖上层违反前置条件来纠正逻辑——这会引入环状依赖。
不变式——架构安全的底限
不变式跨越多次调用,是模块内部或模块间“保持成立”的规则。例如:
- 订单总金额 = 所有订单行金额之和 + 运费 - 优惠。
- 用户身份令牌在单次请求链中保持不变。
- 缓存与数据库的最终一致性窗口不超过 5 秒。
违反不变式通常意味着模块边界被穿透——上层直接修改了本应封装的数据。
错误码约定——异常路径的契约
错误码不是状态码的简单复制,它必须与模块边界对齐:
- 每个模块定义自己的错误码范围(如业务模块 20000-29999,基础设施 40000-49999)。
- 错误码在契约中必须列举所有可能值,不允许“其他错误”占位。
- 调用者只依赖错误码语义,不依赖错误消息字符串。
契约要素与依赖方向的关系图
下图展示了六要素如何约束模块 A(调用者)对模块 B(被调用者)的依赖方向:
反模式案例
反模式1:空契约
只声明方法签名,不写前置/后置条件。后果:调用者盲目传参,被调用者在内部抛出运行时异常,依赖方向失控。
只声明方法签名,不写前置/后置条件。后果:调用者盲目传参,被调用者在内部抛出运行时异常,依赖方向失控。
反模式2:后置条件依赖调用者
契约中写“调用者必须在1秒内读取结果”,这会导致下层依赖上层的行为——方向反转。
契约中写“调用者必须在1秒内读取结果”,这会导致下层依赖上层的行为——方向反转。
反模式3:错误码范围重叠
两个模块都用 1001 表示“资源不存在”,但含义不同(用户 vs 产品)。调试时无法追溯。
两个模块都用 1001 表示“资源不存在”,但含义不同(用户 vs 产品)。调试时无法追溯。
实践落地步骤
- 列出模块的所有公开接口,为每个接口撰写三行:名称 + 签名 + 一句话说明。
- 对每个接口标注前置条件:参数校验、状态是否就绪、是否需要加锁。
- 后置条件逐条写:返回值保证、副作用(写库、发消息、缓存更新)、资源释放。
- 定义模块不变式:用一个全局断言集,每次调用前后都应保持成立。
- 分配错误码段:每个模块独立分配,在接口文档中枚举所有可能码。
- 在CI中验证契约:使用契约测试框架(如Pact、双写测试)确保前置/后置条件不被实现违反。
验收标准:设计评审时,检查每个接口是否同时包含
@pre、@post、@invariant 和错误码列表。缺少任意一项即视为契约不完整,不允许进入编码阶段。