接口契约定义要素

接口契约是模块之间稳定交互的“法律条文”——它不仅描述“做什么”,还定义“在什么条件下做、做完后保证什么、过程中哪些规则不能破坏”。缺少任意一个要素,依赖方向就可能在运行期偏离设计预期。

六要素总览

要素作用域违反后果
接口名称身份标识调用者找不到正确入口,版本混乱
输入/输出参数数据交换类型不匹配、字段缺失、空指针
前置条件调用安全参数非法、状态未就绪
后置条件结果保证状态未更新、资源未释放
不变式全局约束数据结构损坏、循环依赖
错误码约定异常路径调用者无法区分错误类型
架构价值:完整契约让模块边界可验证,依赖方向从“推测”变为“证明”。在大型系统中,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(被调用者)的依赖方向:

接口契约六要素与依赖方向约束 模块 A(调用者) 模块 B(被调用者) 前置条件 后置条件 接口名称 输入/输出参数 不变式 全局约束 错误码约定(异常路径) 虚线箭头表示约束方向:前置条件约束调用者,后置条件约束被调用者,其余要素绑定接口签名

反模式案例

反模式1:空契约
只声明方法签名,不写前置/后置条件。后果:调用者盲目传参,被调用者在内部抛出运行时异常,依赖方向失控。
反模式2:后置条件依赖调用者
契约中写“调用者必须在1秒内读取结果”,这会导致下层依赖上层的行为——方向反转。
反模式3:错误码范围重叠
两个模块都用 1001 表示“资源不存在”,但含义不同(用户 vs 产品)。调试时无法追溯。

实践落地步骤

  1. 列出模块的所有公开接口,为每个接口撰写三行:名称 + 签名 + 一句话说明。
  2. 对每个接口标注前置条件:参数校验、状态是否就绪、是否需要加锁。
  3. 后置条件逐条写:返回值保证、副作用(写库、发消息、缓存更新)、资源释放。
  4. 定义模块不变式:用一个全局断言集,每次调用前后都应保持成立。
  5. 分配错误码段:每个模块独立分配,在接口文档中枚举所有可能码。
  6. 在CI中验证契约:使用契约测试框架(如Pact、双写测试)确保前置/后置条件不被实现违反。
验收标准:设计评审时,检查每个接口是否同时包含 @pre、@post、@invariant 和错误码列表。缺少任意一项即视为契约不完整,不允许进入编码阶段。