接口契约的基本要素:前置条件、后置条件、不变量、异常约定
接口契约(Interface Contract)是模块依赖方向控制的核心工具。它显式声明了调用方与被调用方之间的责任边界,使模块间的信任关系可验证、可测试。契约由四个基本要素构成:前置条件(Precondition)、后置条件(Postcondition)、不变量(Invariant)、异常约定(Exception Contract)。这四个要素共同定义了模块边界上的控制流与数据流约束,是分层架构中“依赖方向反转”与“信任链路”的硬性保障。
2.1 四要素定义与模块边界映射
| 要素 | 语义 | 在模块边界的角色 | 依赖方向控制点 |
|---|---|---|---|
| 前置条件 | 调用方在调用前必须满足的状态/参数约束 | 定义“什么能进来” | 调用方必须提供合法输入,否则契约失效 |
| 后置条件 | 被调用方在正常返回后必须保证的状态/输出约束 | 定义“什么能出去” | 被调用方不得输出不符合约定状态;否则视为违规 |
| 不变量 | 模块内部状态在整个调用周期中始终保持的属性 | 划定“内部稳定性”边界 | 调用方不得破坏被调用方内部不变量,被调用方也不得自己违反 |
| 异常约定 | 当前置条件、后置条件或不变量被违反时,契约要求的行为 | 定义“故障边界” | 异常传播路径必须受控,不能跨层泄露内部实现细节 |
架构提示: 四个要素中,前置条件和后置条件是“单向信任”的显式表达——调用方信任接口文档,被调用方信任调用方的输入。不变量则是“双向信任”的锚点,异常约定是信任破裂时的安。在依赖方向控制中,前置条件和异常约定往往被用来阻断反向依赖(例如禁止上层模块直接操作下层内部状态)。
2.2 在分层架构中的组件职责分配
- 表现层(UI/API网关):负责校验输入参数,即强制满足前置条件。不允许将非法参数传递到业务层。
- 业务层:保证业务不变量(如订单总额必须大于0),并在返回DTO前验证后置条件。
- 数据访问层:保证持久化操作的前后状态一致(如事务不变量),异常约定统一转换为业务异常,不暴露ORM异常。
- 公共接口层:以接口契约形式定义各层之间的信任边界,控制依赖方向(上层只能依赖下层的接口抽象,且下层不能反向引用)。
2.3 数据流、控制流与信任链路
在契约驱动下,数据流遵循“输入 → 前置检查 → 业务处理 → 后置检查 → 输出”的路径,控制流由异常约定加以分支。信任链路的建立依赖每个模块对自身契约的严格实现:
- 调用方信任被调用方会在满足前置条件时正常工作;
- 被调用方信任调用方不会传递越界参数;
- 双方信任不变量在调用前后始终成立;
- 异常约定限定了信任破裂的修复范围。
2.4 选型权衡:契约的严格度与灵活性
| 契约策略 | 优点 | 风险 | 适用场景 |
|---|---|---|---|
| 防御式(Defensive)——每个模块内部重复校验所有输入输出 | 容忍外部调用不规范,但不保证契约一致性 | 冗余校验导致性能下降,维护时容易遗漏 | 第三方API集成、安全敏感边界 |
| 契约式(Design by Contract)——严格前置/后置/不变量,违规即异常 | 依赖方向清晰,bug快速暴露 | 要求团队有较高纪律性,初期构建成本高 | 核心业务模块、框架基础设施层 |
| 松散契约(Loose)——仅定义接口签名,不强制约束 | 开发速度快,灵活 | 模块间信任度低,后期维护依赖文档和口头约定 | 原型阶段、低频变更模块 |
架构风险点: 若模块边界上的异常约定缺失或模糊,异常会沿调用栈向上传播,打破分层隔离。例如数据访问层直接抛出
SQLException 到业务层,导致业务层必须依赖底层实现细节,依赖方向逆转为“业务层依赖存储层实现”。此时必须用异常约定将底层异常包装为业务异常,切断反向依赖。
2.5 落地步骤:在模块依赖方向控制中实施契约
- 识别模块边界:使用依赖关系图标注每一对调用方与被调用方,标记当前是否存在反向依赖。
- 定义前置条件:为每个公开接口的参数、全局状态(如配置、用户上下文)写出不可违背的约束条件。
- 定义后置条件:明确接口正常返回后,数据、状态、资源释放必须满足的属性。
- 定义不变量:每个模块内部选出最重要的不变属性(如缓存一致性、线程安全状态),写入契约。
- 定义异常约定:决定哪些异常属于契约违规、哪些属于预期异常,约定异常类型与传播范围。
- 工具化验证:使用契约检查框架(如 .NET Code Contracts、JSR 303 Bean Validation、自定义assert)在开发阶段自动验证。
2.6 验收标准
- 完整性:每个公开接口的前置、后置、不变量、异常约定均有文档(契约定义文件或代码注解)。
- 可执行:契约检查能够在开发环境、测试环境、CI流水线中自动运行,违反即报错。
- 隔离性:异常约定执行后,模块内部实现细节(如底层异常、临时文件、内部数据结构)不会泄露到调用方。
- 可追溯:任意模块边界上的依赖方向违反都能被契约的异常约定或前置条件捕捉,并暴露具体违反的契约要素。
案例参考(非代码,仅模式): 在一个支付系统中,支付接口的契约定义如下:
前置条件: 交易金额 > 0 且 交易金额 ≤ 单笔限额
交易订单号不重复
支付帐户状态为“正常”
后置条件: 支付流水已生成,状态为“待扣款”
订单成功标记(非最终提交)
不变量: 订单总金额 = 各支付项金额之和
帐户余额 + 冻结金额 = 历史常量
异常约定: 金额越界 → PaymentAmountOutOfBoundsException
重复订单 → DuplicateOrderException
内部DB异常 → PaymentServiceException(不暴露DB类型)