Interface Contract(接口契约)
解决什么问题:同步 RPC 调用方凭什么相信被调方的承诺—— Interface Contract = 同步接口的"宪法",定义输入输出格式、错误语义、版本、可观测性。任何接口被 100 个调用方依赖时,契约就是"法律"。
它是什么
Interface Contract(接口契约) 是 L2 设计层关于同步接口的强约束契约——比 API Design 更聚焦于"契约层面"而不是"URL 设计层面"。
契约的 4 个核心要素:
- 格式契约(Format)——字段名、类型、是否必填、嵌套结构
- 语义契约(Semantics)——业务含义、不变量、状态约束
- 错误契约(Errors)——错误码、错误消息、重试语义、幂等性
- 演化契约(Evolution)——版本、兼容性、deprecation、删除
SE@G Ch 18 给的核心论断: "The most important principle of API design is consistency. Once you've chosen a style, apply it everywhere." "A contract is a promise — and once made, must be kept, or its breaking must be loud."
Interface Contract vs Event Contract:
| 维度 | Interface Contract | Event Contract | |
|---|---|---|---|
| 通信模型 | 同步请求-响应 | 异步发布-订阅 | |
| 调用方 | 1 个或几个明确调用方 | N 个订阅者(常未知) | |
| 失败处理 | 立即返回错误 | 失败处理属于消费者 | |
| 兼容性影响 | 1 个调用方坏 = 1 处坏 | 1 个调用方坏 = 所有同 schema 调用方可能坏 | |
| 演化 | 双侧协调(同 schema) | Registry 自动(独立演化) | |
| 删除 | 直接下架(带 deprecation) | 只能停发,保留历史 |
V2 视角(V2.7 B2 段 4 Hyrum's Law + B7 段 5 演化): 接口契约 = BC 之间的"宪法"——契约违约 = BC 边界崩塌。 接口契约的演化必须显式、渐进、可监控——默默改字段 = 制造隐性技术债。
关键属性
| 属性 | 值/含义 |
|---|---|
| 契约优先 | IDL(protobuf / OpenAPI / Thrift)先于代码 |
| 代码生成 | Server Stub + Client SDK + 测试桩自动生成 |
| 向后兼容 | 加字段 OK,删/改字段必须新版本 |
| 幂等性 | 至少一次或精确一次(取决于业务) |
| 错误码语义 | 业务错误码 + HTTP/gRPC status 双层 |
| 超时与重试 | 必须显式声明(不可假设) |
| 可观测性 | traceId / spanId 必须贯穿 |
| 反模式 | 字段偷偷改、错误码随版本变、不声明幂等 |
| 判错标志 | 调用方要看实现代码 / 多版本共存无人管理 |
怎么用
场景 1:Protobuf 作为"代码 + 契约"的双源
// order_service.proto
syntax = "proto3";
package com.example.orders.v1;
service OrderService {
rpc PlaceOrder(PlaceOrderRequest) returns (PlaceOrderResponse);
rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);
}
message PlaceOrderRequest {
string customer_id = 1;
repeated OrderItemInput items = 2;
optional string notes = 3; // 新加的 optional,向后兼容
}
message OrderItemInput {
string product_id = 1;
int32 quantity = 2;
double unit_price = 3;
}
message PlaceOrderResponse {
string order_id = 1;
string status = 2;
}
message GetOrderRequest {
string order_id = 1;
}
message GetOrderResponse {
string order_id = 1;
string customer_id = 2;
string status = 3;
repeated OrderItemView items = 4;
}优势:
- 强类型(编译期检查)
- 自动生成多语言客户端
- 字段编号 = 兼容性的核心(不能重用已删除字段的编号)
- 序列化体积小(vs JSON)
场景 2:契约优先开发(API-First)
# openapi.yaml(先写,后实现)
openapi: 3.1.0
info:
title: Order Service
version: 2.1.0
paths:
/orders:
post:
operationId: placeOrder
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOrderRequest'
responses:
'201':
description: Order placed
content:
application/json:
schema:
$ref: '#/components/schemas/PlaceOrderResponse'
'400': { $ref: '#/components/responses/ValidationError' }
'409': { $ref: '#/components/responses/ConflictError' }
'422': { $ref: '#/components/responses/BusinessError' }
components:
schemas:
PlaceOrderRequest:
type: object
required: [customerId, items]
properties:
customerId: { type: string, format: uuid }
items:
type: array
minItems: 1
items: { $ref: '#/components/schemas/OrderItemInput' }契约优先的 4 个好处:
- 并行开发——前后端 / 多端基于契约同时开发
- 代码生成——Server / Client / Mock / Test 全自动
- 文档一致——文档 = 代码生成,永远不脱节
- 契约测试——消费者驱动的契约测试(CDC)
场景 3:契约测试(CDC - Consumer-Driven Contract)
# 消费者写期望:调用 OrderService.placeOrder 时
# 应该返回 orderId 和 status 字段
# 这个契约由消费者定义,生产者验证
# producer-tests/test_contract.py
def test_place_order_contract():
# Pact / Spring Cloud Contract 框架验证
response = order_service.place_order(
customer_id="cus-123",
items=[{"product_id": "SKU-001", "quantity": 2}]
)
# 消费者期望的字段必须有
assert "order_id" in response
assert "status" in response
assert response["status"] in ["pending", "confirmed"]CDC 的 3 个核心角色:
- Consumer 定义契约——"我期望你返回这些字段"
- Producer 验证契约——"我能满足这些期望"
- Broker(Pact 等)——存储契约,CI 流水线验证
为什么 CDC 重要:传统"接口测试"只验证"生产者实现是否正确",CDC 验证"调用方是否能正确使用"——后者才是契约的本质。
场景 4:幂等性与重试语义
service OrderService {
// 显式声明幂等性
rpc PlaceOrder(PlaceOrderRequest) returns (PlaceOrderResponse) {
option idempotency_level = EXACTLY_ONCE; // 至少一次去重
}
rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);
}
message PlaceOrderRequest {
string idempotency_key = 1; // 调用方提供去重 key
string customer_id = 2;
repeated OrderItemInput items = 3;
}幂等性的 3 个层级:
| 层级 | 含义 | 典型场景 |
|---|---|---|
| Idempotent | 重放 = 同一结果 | GET / 查询 |
| At-least-once | 重放 = 至少一次执行 | 创建(带 idempotency_key 去重) |
| Exactly-once | 重放 = 精确一次 | 金融交易(强约束) |
V2 B6 权衡视角:exactly-once 性能差、成本高,绝大部分业务"at-least-once + idempotency_key"就够。
场景 5:版本演化的协调
双版本共存期:
v1 阶段: /v1/orders
v2 阶段: /v2/orders
过渡期: 同时部署两个版本
下架期: v1 监控下降到 0 后删除协调步骤:
- 公告 v2 契约(spec 发布、内部文档)
- 灰度(10% / 50% / 100%)
- 监控旧版本调用量
- 主动通知还没切的调用方
- 倒计时截止日
- 下架 + 保留监控 N 个月
常见误区
- "契约 = 注释"——错。契约是机器可读的 IDL,不是文档注释
- "加字段 = 改字段"——错。只能加字段,改类型必须新版本
- "假设调用方不会用错"——错。Hyrum's Law:足够多的调用方会试探性使用任何可观察的行为
- "生产者说了算"——错。契约是双向协议,消费者应该有发言权(CDC)
- "错误码 = HTTP status"——错。HTTP status 是协议层,业务错误用业务错误码(如
ORDER_INSUFFICIENT_STOCK),HTTP 422 - "重试是调用方的事"——半对。生产者应该显式声明哪些操作可重试,避免不安全的重试
- "契约写完不改"——错。契约必然演化,关键是演化要显式 + 渐进 + 可监控
关键洞察
接口契约 = BC 之间的"宪法"——违约 = 边界崩塌。
一个契约被 100 个调用方依赖时,破坏兼容 = 法律纠纷。
V2 B2 段 4 Hyrum's Law 的工程含义——契约 = 模型,行为 = 现实。 契约的演化必须显式、可监控、可追责——默默改字段 = 制造技术债。
契约优先 vs 代码优先:契约先 = 演化阻力小 = 长期可持续。 契约写完就改 = 演化失控 = 长期技术债。 判断契约健康的标志 = 契约变更后 30 天内,没有一个调用方"突然报错"。
相关链接
- API Design——接口设计的 URL / 资源 / 状态层
- Event Contract——异步事件契约(不同的演化机制)
- Hyrum's Law——契约被试探的现实
- [V3 涌现视域] · 认识自己——契约 = 模型,行为 = 现实
- [V3 涌现之时] · 演化——契约演化的元姿态
- [V3 涌现作为] · 权衡——幂等性的代价权衡