
文档不会主动说谎,但它有一种天然的腐烂倾向。
它在被创建的那一刻是真实的。然后代码改了,需求变了,架构调整了,系统长出了新的形状——而文档就停在那里,面带微笑,继续用它写成那天的信息告诉后来的人"这就是事情的真相"。
这才是最危险的谎言:不是恶意伪造,而是无声的失真。你甚至不知道该信任它到什么程度,因为它永远看起来很正式、很权威,标着版本号,盖着作者的名字。
注释里的谎言
注释是距离代码最近的文档,理论上也应该是最准确的。但恰恰是这种近距离,让它的谎言更具迷惑性。
最经典的场景: 函数名改了,注释没改。
// 返回激活状态的用户列表
public List<User> getUsers() {
return userRepository.findAll(); // 三个月前改成返回所有用户了
}
你看到这个注释,相信了它,然后花了两个小时排查"为什么返回的用户里有未激活的",最后发现注释是谎言。这两个小时,是文档腐烂的直接账单。
但更隐蔽的是另一种:注释描述的是"做了什么",而不是"为什么这么做"。
# 将结果乘以 0.85
price = base_price * 0.85
这行注释告诉你的信息,你盯着代码本身三秒钟也能看出来,它什么都没有说。真正有价值的是:这个 0.85 是什么?是税率?是折扣?是某个合同里写死的系数?是三年前某个业务逻辑的残留?
没有人知道。写注释的人走了,文档只剩下一个数字和一个完全无用的动词。
还有一种注释,是被注释掉的代码。
// const result = legacyCalculate(input);
const result = newCalculate(input);
// TODO: 确认新旧方法在边界情况下的差异
这段 TODO 是什么时候写的?那个"边界情况"确认了吗?legacyCalculate 是否还有某些地方依赖它?没有人敢删掉那行注释的代码,因为没有人知道删了会发生什么。于是它就这样活在那里,像一块已经腐烂但没有人敢移走的砖。
接口文档里的陷阱
接口文档是我见过谎言密度最高的文档类型之一,因为接口改动频繁,而维护文档的人和改代码的人经常不是同一个人。
字段存在,但行为不符合描述。
文档写着:status 字段,可能的值为 active、inactive、pending。
你调了接口,拿到了一个值是 SUSPENDED。
打开 Slack 找后端,对方说:哦,这个是上季度加的,文档还没更新。
这种情况,在任何一个活跃开发了超过一年的系统里,每周都在发生。文档说有四个状态,实际上有六个。文档说某个字段是必填,实际上在某些场景下它会被省略。文档说返回的是 UTC 时间,实际上是服务器本地时间——这个问题在系统从一个时区迁移到另一个时区之后尤为突出,而通常没有人会去更新文档。
最要命的是错误码文档。
错误码:4001
描述:参数校验失败
触发条件呢?哪些参数会导致这个错误?4001 和 4002 有什么区别?文档没说。你在线上遇到了这个错误,翻文档,文档只告诉你"参数校验失败"——恭喜,这和你从错误本身读出来的信息是完全一样的,文档在这里提供的信息量是零。
更糟糕的是,有些错误码文档完全是错的。接口重构之后,旧的错误码被复用了,但语义变了,文档没有跟着改。你按照文档处理了 4001,然后发现你的处理逻辑完全不对,因为你在处理的其实是另一种错误。
版本控制问题:文档版本和接口版本对不上。
文档上写着 v2,系统已经在跑 v3,但 v3 的变更记录没有人整理,或者整理了但藏在某个没人找得到的角落。调用方永远不知道自己对着哪一版的文档在开发,只能靠反复试错来验证文档里的哪些内容还是真的。
架构文档里的化石
架构文档是最容易变成化石的文档,因为它看起来最稳定,改动成本最高,于是就最容易被遗忘。
经典陷阱:画的是理想状态,运行的是现实状态。
架构图上,服务 A 直接调用服务 B,走的是标准的 REST API。实际上,某次性能优化之后,有一部分请求走了消息队列,还有另一部分走了缓存层,而这些变更都是临时方案,"等有时间再重构",然后永远没有等到那个时间。
架构文档还停在那张图上。新工程师看了图,以为系统是这样的,然后试图用这个理解去调试一个和消息队列相关的问题——找不到根因,因为他的心智模型是错的,而错误的来源是那张正式、美观、加了版本号的架构图。
另一个陷阱:文档说"这里用了 X 技术",但 X 早就被替换了。
文档写着"消息中间件使用 RabbitMQ",两年前迁移到了 Kafka,但架构文档没有更新。你在排查消息积压问题,按照文档去找 RabbitMQ 的管理控制台,当然什么都找不到。问了三个人之后,终于有人说:哦,我们用 Kafka,RabbitMQ 是老系统的东西了。
这种信息,在文档里留了两年。两年里有多少工程师被它误导过,没有人统计。
ADR 写了,但决策已经被推翻了,文档没有更新。
某个 ADR 记录了"决定使用单体架构,理由是团队规模小,维护成本低"。三年后团队扩大了三倍,系统已经被拆成了微服务,但那份 ADR 还在,还是当时的结论,没有任何标注说明这个决策已经作废。
新工程师看到这份 ADR,以为这是有意为之的设计原则,于是在新功能上也按照单体的思路在设计,然后和整体架构方向越来越冲突。
需求文档里的幽灵
需求文档的谎言通常不是写错了什么,而是没有写出来的那些东西,以及留下来的那些已经过期的东西。
场景一:需求文档版本混战。
v1 的需求文档说,用户可以删除自己的账号。v2 说,增加了删除冷却期,七天内可以撤销。v3 说,企业账号不允许删除,只能停用。
这三份文档都在 Confluence 上,标着不同的日期。但没有哪一份说"本版本废弃了之前的哪些描述"。开发按照最新版本实现,测试按照 v1 在写用例,结果 code review 的时候才发现双方对功能的理解相差甚远。
场景二:需求里的"临时方案"永久化了。
需求文档里有一行小字:"该功能暂时不支持批量操作,后续版本补充。"这行字写于两年前的某个版本,在那之后这个功能迭代了五次,批量操作从来没有被加进来,也从来没有人把那行"暂时"改成"不支持"或者删掉。
现在有人问:批量操作是不支持,还是在规划中?没有人能给出确定的答案,因为文档只说了"暂时"。
场景三:业务逻辑只在需求文档里,不在代码里,也没有测试覆盖。
某个复杂的计费逻辑,在需求文档里用了三页纸描述了各种计算规则和边界情况。这份文档是真实的——在它写成的那天是真实的。然后在实现过程中,业务方又来了几轮修改,有些在文档里更新了,有些在会议纪要里记了一下,有些就存在于开发和产品经理的那几次口头沟通里,没有落到任何文档上。
两年后,有人要重构这个计费模块。他找到了那份三页纸的需求文档,以为这就是完整的规则,按照它重构了。上线之后,发现了七个和现有行为不一致的地方——这七个地方,是那些没有被文档记录的口头修改的结果。
项目文档里的安慰剂
项目管理类文档——进度表、风险登记册、里程碑计划——有一种特殊的危险性:它们的读者是管理层,而管理层的决策依赖于它们。
进度表变成了愿望清单。
项目启动时,PM 用甘特图画了一条漂亮的时间线:需求分析两周,开发六周,测试三周,上线。每个阶段都有负责人,每个节点都有日期。
三周后,开发已经落后两周了。但进度表没有更新,因为更新进度表意味着要和管理层解释为什么落后,而"暂时先不更新"更容易。
管理层看着那张进度表,以为项目在按计划推进。他们在这个基础上做了关于发布时间的对外承诺。
又三周后,现实无法再被隐藏,延期的消息才被披露。但这时外部承诺已经作出,损失已经不可逆。
进度表没有说谎,它只是停止了说话。但停止说话,和说谎的效果是一样的。
风险登记册变成了形式主义。
每个季度要更新风险登记册,于是每个季度有人把上个季度的文档拷贝一份,改一下日期,把状态从"未发生"改成"未发生",提交上去。
真正的风险——某个核心工程师在和竞争对手面试,某个依赖的开源库已经停止维护,某个关键的业务假设在最近的用户调研里被推翻了——这些东西不在登记册上。登记册上是那些人人都能看出来但永远不会发生的风险,写在那里让人感觉"风险是被管理的"。
文档腐烂的根因
这些谎言和陷阱,根源上都是同一件事:文档没有保鲜机制。
代码有编译器,写错了会报错。代码有测试,逻辑变了测试会失败,提醒你去更新相关的部分。代码有 lint,风格不一致会被标出来。
文档什么都没有。你写了一段和现实不符的内容,没有任何系统会告诉你。它就这样静静地存在着,等待着下一个相信它的人踩进去。
有人会说,文档维护是工程师的职责,应该养成好习惯。这是对的,但这不够——依赖个人自律来维护一个系统级的一致性,是一个注定会失败的设计。
真正有效的做法,是让文档的维护成本尽可能低,让文档和代码尽可能近,让不一致尽可能早地被发现。接口文档用代码生成,而不是手写;架构决策记录在代码仓库里,和代码一起被版本控制;测试本身就是一种活的文档——测试失败意味着文档(即测试的断言)和实现之间出现了分歧,必须有人来决定哪一个是对的。
写文档不难。让文档保持真实,才是真正的难题。
如果你觉得这篇文章对你有帮助,欢迎转发点赞收藏。
转载请注明出处,谢谢。
夜雨聆风