Skip to content

Interface Contract(接口契约) ​

解决什么问题:同步 RPC 调用方凭什么相信被调方的承诺—— Interface Contract = 同步接口的"宪法",定义输入输出格式、错误语义、版本、可观测性。任何接口被 100 个调用方依赖时,契约就是"法律"。

它是什么 ​

Interface Contract(接口契约) 是 L2 设计层关于同步接口的强约束契约——比 API Design 更聚焦于"契约层面"而不是"URL 设计层面"。

契约的 4 个核心要素:

  1. 格式契约(Format)——字段名、类型、是否必填、嵌套结构
  2. 语义契约(Semantics)——业务含义、不变量、状态约束
  3. 错误契约(Errors)——错误码、错误消息、重试语义、幂等性
  4. 演化契约(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 ContractEvent 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 作为"代码 + 契约"的双源 ​

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) ​

yaml
# 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 个好处:

  1. 并行开发——前后端 / 多端基于契约同时开发
  2. 代码生成——Server / Client / Mock / Test 全自动
  3. 文档一致——文档 = 代码生成,永远不脱节
  4. 契约测试——消费者驱动的契约测试(CDC)

场景 3:契约测试(CDC - Consumer-Driven Contract) ​

python
# 消费者写期望:调用 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:幂等性与重试语义 ​

protobuf
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 后删除

协调步骤:

  1. 公告 v2 契约(spec 发布、内部文档)
  2. 灰度(10% / 50% / 100%)
  3. 监控旧版本调用量
  4. 主动通知还没切的调用方
  5. 倒计时截止日
  6. 下架 + 保留监控 N 个月

常见误区 ​

  1. "契约 = 注释"——错。契约是机器可读的 IDL,不是文档注释
  2. "加字段 = 改字段"——错。只能加字段,改类型必须新版本
  3. "假设调用方不会用错"——错。Hyrum's Law:足够多的调用方会试探性使用任何可观察的行为
  4. "生产者说了算"——错。契约是双向协议,消费者应该有发言权(CDC)
  5. "错误码 = HTTP status"——错。HTTP status 是协议层,业务错误用业务错误码(如 ORDER_INSUFFICIENT_STOCK),HTTP 422
  6. "重试是调用方的事"——半对。生产者应该显式声明哪些操作可重试,避免不安全的重试
  7. "契约写完不改"——错。契约必然演化,关键是演化要显式 + 渐进 + 可监控

关键洞察 ​

接口契约 = BC 之间的"宪法"——违约 = 边界崩塌。

一个契约被 100 个调用方依赖时,破坏兼容 = 法律纠纷。

V2 B2 段 4 Hyrum's Law 的工程含义——契约 = 模型,行为 = 现实。 契约的演化必须显式、可监控、可追责——默默改字段 = 制造技术债。

契约优先 vs 代码优先:契约先 = 演化阻力小 = 长期可持续。 契约写完就改 = 演化失控 = 长期技术债。 判断契约健康的标志 = 契约变更后 30 天内,没有一个调用方"突然报错"。

相关链接 ​

从名家方法论与工程化思路中蒸馏出自己的工程体系。