2545 字
约 8 分钟
17
S24-A20 · 代码改了文档没跟上:让同步成为机制而不是要求

一个 API 的文档写着"支持批量 100 条",而代码三个月前改成了 50。前端按文档做,上线后批量提交全失败——而"改文档"这件事在当时没人想起来,因为改代码的人不知道文档里写了这个数。

文档与代码不同步,几乎从不是"态度问题",而是"没有机制让两者同时被修改"。这个区别在"追责"上也有意义:"没改文档"几乎从不被记为问题(因为不影响功能),于是它在优先级里永远排最后——直到有人按文档做错。
要求"记得改文档"不会生效,因为它依赖人的记忆;而机制能做到"不改就报错"。


1. 三种不同步

类型 例子
数值不同步 上限、超时、版本号
结构不同步 字段、枚举值、路径
语义不同步 同一字段含义变了

第三项最难查:字段名没变、类型没变、文档没变,但"它的含义"变了第三项的"传染性"最强:一个字段含义变了,所有"按旧含义"写的下游逻辑都错了,而它们各自的代码看起来完全正常。
(如"金额"从"含税"改成"不含税"),于是所有按旧理解写的代码都错。

前两项能靠"生成"解决,第三项只能靠"评审时提出"。

2. 四种同步机制(按可靠度)

机制 可靠度
从代码生成文档(OpenAPI、JavaDoc) 高(数值/结构类)
文档测试(示例可运行) 高(示例类)
CI 校验(文档里的常量与代码一致) 中高
评审清单("改 API 就要改文档") 低

第一项还有"防遗忘"的附加收益:新写的接口自动有文档,于是"补文档"这个动作从"每次都要做"变成"默认就有"。
第一项最彻底:接口文档从注解/类型生成,于是"改了签名文档就变"。代价是"生成不出的部分"(业务语义、使用场景)仍需人工写。

第三项是"轻量版":把文档里的关键数字与代码常量做比对第三项的"轻"在于"不用改文档体系":它只是在现有文档上加一层检查,于是可以"先做最痛的那几个数"(上限、超时、版本)。
,不一致就报错。它不需要重写文档体系。

3. 生成式文档的边界

能生成 不能生成
路径、参数、类型 业务语义
示例请求/响应(结构) "什么时候用这个接口"
错误码列表 "为什么这么设计"
版本变更记录 迁移建议

第二项与第四项还有一个"时间"特征:它们的变化频率低("什么时候用"很少变),于是"人工维护"的成本可控——而"参数与字段"变化频繁,正适合生成。
第二项与第四项是"人写文档"的真正价值:读者来查文档,往往不是"参数是什么",而是"我该用哪个、怎么迁移"。

所以"生成 + 人工补充"是常态:结构自动生成,语义人工维护,两者在同一个页面呈现。

// 生成部分(OpenAPI,从代码来)
paths:
  /api/orders:
    post:
      summary: "创建订单"          # 人工写的 summary
      parameters: [...]            # 生成的
      x-when-to-use: "下单流程的第二步,需先校验库存"   # 人工扩展字段
      x-migration: "v2 起 max_batch 从 100 降为 50"     # 人工扩展字段

4. 数值不同步的"轻量防线"

做法 成本
文档里用占位符引用代码常量 低(但需渲染机制)
CI 抽取文档中的数字与常量比对 中
文档只写"最多 N 条(见配置)" 低(但读者不方便)
每次改常量时搜文档 高(易漏)

第二项最实用:用正则从文档里抽出"最多 100 条"这类描述,与代码常量比对。第二项的"脆弱点"在"文档表述的多样性":"最多 100 条"/"上限 100"/"不超过 100" 三种写法都要匹配,否则检查会漏。所以它适合"关键数值",不适合"所有数字"。
它能抓住最常见的一类不同步,成本一个脚本。

第四项就是"没有机制"的状态:它依赖"想到",而"想到"不可靠。

// CI 校验:文档里的数字 vs 代码常量
doc_max=$(grep -oE '最多 ?[0-9]+ ?条' docs/api.md | grep -oE '[0-9]+')
code_max=$(grep -oE 'MAX_BATCH = [0-9]+' src/main/java/.../OrderApi.java | grep -oE '[0-9]+')
[ "$doc_max" = "$code_max" ] || { echo "文档与代码不一致: $doc_max vs $code_max"; exit 1; }

"文档测试"是被低估的机制:文档里的示例代码如果能被执行,它就永远不会过期——因为它一旦与实现不符,测试就红。

// doctest:把文档里的示例变成测试
def max_batch() -> int:
    """单次批量的上限。

    >>> max_batch()
    50
    """
    return 50
// 若把 50 改成 30 而文档没改 → doctest 失败 → CI 红

5. 语义变更怎么处理

做法 效果
改名(让含义显式) 最好
文档加"变更说明" 中(读者可能不看)
保留旧字段、加新字段 稳(兼容期)
只更新文档 差(旧代码仍按旧理解)

第一项的成本在"改名"本身:数据库列、接口字段、前端字段都要改,于是它是"一次性大改"。所以"早点改"比"以后改"便宜得多——因为下游还没铺开。
第一项最有效:"金额"改成"金额含税",于是所有按旧理解写的代码在编译或阅读时立刻暴露差异。

第三项是"接口演进"的标准做法:旧字段保留(标废弃),新字段并列,迁移期后再删。

"文档漂移"应当被度量,否则"好像还行"会持续很久。一个便宜的指标:抽查 20 条文档描述,与代码核对,统计不一致的条数。

// 三个可算的数(每季度)
1 抽查不一致率        > 10% → 机制缺失,优先做生成/校验
2 因文档错误导致的工单数 > 0  → 具体哪几处,优先修
3 文档最后一次更新距今天数 > 90 天 → 该复查(不是过期,是"需确认")
// 用法:第 1 项决定"要不要投入机制",第 2 项决定"先修哪里"

6. 排错:五种"文档不生效"

现象 成因 处理方向
按文档做失败 数值/结构不同步 生成或 CI 校验
字段含义理解错 语义变更未标注 改名或加说明
文档里有废弃接口 未清理 生成式 + 标记废弃
多版本文档并存 无版本管理 文档随代码版本发布
新人按旧文档做 找不到最新 单一入口 + 重定向

第四项很常见:文档站是"独立部署",而代码发布了文档没发第四项还有一个"入口"问题:同一个 API 有两份文档(一份在文档站、一份在代码注释),于是"哪个是最新"要靠猜——而猜错的成本由使用者承担。
,于是"文档对应的是三周前的代码"。

最后一个"演进"路径:先给"最贵的接口"加机制,再逐步铺开。因为"所有接口都生成文档"是一次大工程,而"三个核心接口 + CI 校验"只需几天——而这三个接口往往承担了 80% 的对接工作。

// 三步走(按投入产出)
1 三个核心接口:生成 OpenAPI + 人工补语义(几天)
2 加 CI 校验:文档数字 vs 代码常量(一天)
3 逐步覆盖:新接口默认生成,老接口按需迁移
// 反例:一开始就要求"全部接口必须生成" → 半年没落地

7. 边界:什么情况不必追求同步

场景 为什么 做法
内部一次性脚本 无读者 不写文档
代码即文档(自解释) 命名清晰 只写"为什么"
频繁变动的实验接口 文档会立刻过期 标"实验性,勿依赖"
已有强类型 SDK 类型即文档 只补充语义

最后一条与"成本"有关的边界:同步机制的成本主要在"第一次搭建"(生成管线或 CI 检查),之后的边际成本接近零。所以"值不值"应当按"文档被引用的次数"算,而不是按"搭建工时"算。
判断口径一句话:这段文档会不会被"不读代码的人"用来做决定。会(对接方、前端、运维),就必须有机制;不会(只有作者自己看),文档只是备忘。


8. 自测

  1. 三种不同步里,哪一种最难查?为什么"字段没变、含义变了"最危险?
  2. 四种同步机制里,哪一种可靠度最高?它的边界在哪?
  3. 为什么"生成式文档"不能替代人工?读者来查文档时真正要的是什么?
  4. "文档里用占位符引用代码常量"与"CI 抽取数字比对"各有什么取舍?
  5. 语义变更时,为什么"改名"比"加说明"更有效?
  6. 判断"是否需要机制"的那个问题是什么?
S24-A20 · 代码改了文档没跟上:让同步成为机制而不是要求
https://www.clxhxhhr.top/posts/6564/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。