接口契约与不变量未通过评审前,禁止写实现。
实现的诱惑永远强于契约的耐心。先写代码 = 把不确定性固化进生产环境。
任何后端接口相关的”开始”动作,必须触发:
按顺序执行,任何一步失败都不能写实现代码:
目的:明确”谁依赖这个契约”。未列出的调用方出现时,说明契约有缺失。
输出格式:YAML 文件,提交到 api/<service>/<version>.yaml
不变量(invariant)= “X 永远不能 Y” 的硬规则。
模板(每个不变量写 1 行):
1
2
3
4
5
6
- id: INV-001
statement: "同一用户在同一分钟内不能重复提交同一订单"
enforcement: "unique index on (user_id, order_id, minute_bucket)"
violation_handling: "返回 409 + 重复订单的 id"
- id: INV-002
statement: "..."
最少 3 个不变量。少于 3 个说明你还没想清楚。
Pre-mortem = 在写实现前先想象 5 类失败,每类先写 1 个失败测试。
5 类必写:
所有 5 个测试必须先红后绿(先失败,再因实现而绿,不是先写实现再补测试)。
| 失败模式 | 表现 | 后果 |
|---|---|---|
| 写完代码补契约 | 实现优先,契约是后补的 | 契约反映实现而非业务 |
| 不变量模糊 | “重复请求应该不重复扣款” | “应该” 不可执行 |
| 幂等键过期 | 24h 后重复请求重新执行 | 副作用 2 次 |
| pre-mortem 走过场 | “应该不会有问题” | 故障模式没被测试覆盖 |
| 错误码语义混乱 | 都返回 200 + msg | 客户端无法程序化处理 |
| 不变量被实现破坏 | 实现偷偷绕过了 invariant | 静默 bug |
| 未枚举的调用方 | “前端用我们的方式调” | 实际调用方按自己的方式用 |
🚩 你(agent)想直接写 def create_user(...)
🚩 你看了一下”差不多就行”
🚩 你跳过 pre-mortem 因为”不复杂”
🚩 你的不变量列表少于 3 条
🚩 你写了测试但测试只是 assert True
🚩 你的幂等键用时间戳但没考虑时钟漂移
🚩 你的错误码只有 200 和 500
| 借口 | 反驳 |
|---|---|
| “先写实现快一点” | 重构契约的成本 » 重构实现的成本 |
| “不变量太理论” | 每一个生产事故都源自一个没被写下来的不变量 |
| “幂等以后再说” | 幂等是设计选择,不是补丁 |
| “pre-mortem 浪费时间” | 5 个失败测试 < 1 次线上事故 |
| “前端会处理的” | 前后端契约错配 = 协作灾难 |
| “我们没有外部调用方” | 内部调用方更复杂(散落在 10 个服务里) |
1
2
3
4
5
6
7
8
9
10
# 1. agent 直接写实现
@app.post("/users")
def create_user(user: User):
db.insert(user)
return user
# 2. 之后补契约
# yaml: ...(事后写,反映实现)
# 3. 没写失败测试
# 4. 重复请求会创建重复用户
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1. 先写 OpenAPI
paths:
/users:
post:
parameters: [...]
responses:
'201': {...}
'409': {description: "用户已存在"}
# 2. 提取不变量
- INV-001: "email 必须全局唯一"
- INV-002: "同一 request_id 不能创建 2 个用户"
- INV-003: "删除用户必须保留审计记录 7 年"
# 3. 5 个 pre-mortem 测试
def test_duplicate_request_returns_same_user(): ...
def test_concurrent_creation_one_wins(): ...
def test_downstream_timeout_degrades_gracefully(): ...
def test_email_service_down_rejects_creation(): ...
def test_user_cannot_create_with_other_user_token(): ...
# 4. 等 5 个测试都红了,才写实现
# 5. 5 个测试都绿了,commit
契约是设计,实现是施工。施工可以返工,设计的债务会随每次部署积累。