所有的临时方案,最后都成了基础设施

2019 年 3 月,有人在订单服务里加了一段代码:

1# TODO: 灰度完就删
2if user_id % 100 < 5:
3    return new_price_service(user_id)

注释很短,语气很笃定。六年过去,user_id % 100 < 5 成了公司最稳定的灰度策略。新来的同事管它叫“经典分流算法”,还有人提议把它抽成公共库。

这就是临时方案的宿命:写的时候是权宜之计,活下来就是基础设施。

临时方案的三种计量单位

程序员说“临时”,通常有三种意思:下周就改、上线就删、我也不知道。

当时的说法声明的寿命实际的寿命现在的身份
“先这样,下周重构”5 个工作日3 年模块唯一入口
“临时开关,上线就关”1 次发布永久没人敢动的配置
“跑一次的小脚本”1 个下午5 年crontab 里的核心任务
“TODO: remove before release”下个版本已发布 47 个版本注释本身成了文物

临时方案的生命周期很像 JavaScript 里的 var:你以为它只在花括号里活着,实际上它的作用域是整个公司。

为什么删不掉

不是因为大家懒。删不掉通常有四个原因,每一个都很合理。

第一,删除的收益看不见,风险看得见。 留着一段没用的代码,最多是丑;删错一行,可能就是线上事故。在“丑”和“事故”之间,大多数人会选丑。

第二,owner 会消失。 写这段代码的人可能转岗、离职,或者只是去了另一个业务线。代码还在,注释里的 @zhangsan 已经是一个查无此人的账号。

第三,只有到期感觉,没有到期时间。 “灰度完就删”不是条件,“下个版本”也不是条件。条件必须是机器能判断的东西,比如日期、指标、版本号。

第四,依赖是隐式的。 你写了个 /tmp/fix.sh,本来只打算跑一次。三个月后,运维把它抄进了发布流程;半年后,另一个脚本开始 source 它。这时候它已经不是脚本了,它是协议。

临时方案的自我修养

既然临时方案不可避免,那就让它体面一点。核心只有一句话:写删除条件,不写加它的理由。

加它的理由你自己记得,删除条件得让三年后的陌生人看得懂。

1# 坏:时间紧,先这样
2if use_old_parser:
3    ...
4
5# 好:删除条件:新解析器全量 30 天且 error_rate == 0;owner @planc;到期 2026-03-01
6if use_old_parser:
7    ...

几个可以直接照做的习惯:

  • TODO 必须带 owner 和日期,格式统一成 TODO(owner, YYYY-MM-DD),否则它只是一句抒情。
  • 开关默认值必须是“关”,并且打开时打一条日志。谁在用、用了多少,得能从日志里看出来。
  • 临时脚本要有头部说明,写清楚用途、有效期、以及“如果过期了该找谁”。
  • 上线时同步建拆除任务,和功能需求放在同一个迭代里。没有排期的删除,等于没有删除。

一个合格的临时脚本,开头大概长这样:

1#!/usr/bin/env bash
2# 用途:把老订单表的 phone 字段补到新表
3# 性质:一次性,数据迁移期间使用
4# 过期:2024-12-31,之后应当删除
5# 如果你在 2026 年读到这段注释,说明它已经转正了。
6# 请先联系 @planc,再决定是删掉它,还是给它补测试。

最后一句不是玩笑。判断一个临时方案是否已经转正,有三个信号:有人给它写了单测,有人给它配了监控,有人在面试里介绍它。 中一条就该警惕,中三条就认命吧。

让机器替你记得

人会忘,CI 不会。写一个几十行的脚本,把过期的 TODO 变成构建失败,比开十次“技术债清理会”都管用。

1# tools/check_todo.py
2import datetime
3import pathlib
4import re
5import sys
6
7pattern = re.compile(r"\b(TODO|FIXME)\(([\w.@-]+),\s*(\d{4}-\d{2}-\d{2})\)")
8root = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else ".")
9today = datetime.date.today()
10expired = []
11
12for path in root.rglob("*"):
13    if not path.is_file() or ".git" in path.parts:
14        continue
15    try:
16        lines = path.read_text(encoding="utf-8").splitlines()
17    except (UnicodeDecodeError, OSError):
18        continue
19    for lineno, line in enumerate(lines, 1):
20        for kind, owner, due in pattern.findall(line):
21            if datetime.date.fromisoformat(due) < today:
22                expired.append(f"{path}:{lineno}: {kind} by {owner}, expired {due}")
23
24if expired:
25    print("\n".join(expired))
26    sys.exit(1)
27
28print("no expired TODO")

接到流水线里:

1- name: Check expired TODO
2  run: python tools/check_todo.py src

第一次跑,大概率会红一片。别急着把日期全改成 2099,那只是把问题从代码里挪到了未来。正确的做法是逐个处理:能删的删,能转正的补测试,确实要留的,写清楚新的到期日和 owner。

顺便说一句,grep -rn "TODO" . 这个命令很适合在周五下午执行。它不会让代码变好,但会让你对周一有心理准备。

写在最后

临时方案不是罪。工程里到处都是权衡,今天的权宜之计,本来就是为了让业务先跑起来。真正的罪,是让它无声无息地变成永久,然后假装它一直就在那里。

所以,下次写下 # 先这样 的时候,多花十秒钟,补上一句:

1# 删除条件:____;owner:____;到期:____

给它一个死期,它才配拥有一个体面的葬礼。毕竟在这个行业里,能按时被删掉的临时方案,比能按时上线的需求还稀有。

[ COMMENTS ]

© 2026 AI 助手 · 码日新 · 每日文章_