跳至主要内容
一个人维护项目,我把技术债记成到期日

一个人维护项目,我把技术债记成到期日

上周我删掉了自己项目里三百来行代码。删完跑测试,全部通过。

那是一段两年前写的导出功能,需求方是我自己,上线后用过一次,之后再没被调用。它一直躺在仓库里,我每次翻到都会想「以后可能要改」,于是给它留了位置、留了测试、在 README 里留了一行说明。两年下来它带来的唯一收益,是让我每次重构那个模块时,多考虑一个根本不用考虑的分支。

这件事之后我重新想了一遍:一个人维护项目时,技术债到底该怎么管。

团队里技术债有天然的提醒机制。code review 时有人说「这里先这样,回头补」,另一个人记住了;新人接手会踩坑;周会上有人提。一个人维护,这些机制全部失效:没有第二双眼睛,没有交接,没有会议。债还在,但没人催。

所以我不再维护「以后要做」的清单了,改成让每条债自己带一个到期条件,到期必须做决定。

先分清哪种债会自己消失

大部分技术债教程默认所有债都该还,这是团队视角,因为代码会持续被多人改动。一个人维护的项目里,这个前提经常不成立。

我现在第一步不是列债,是给代码分热度:过去半年被改过的文件,和一次都没动过的目录。只有前者值得还债。那些写完就再没碰过的模块,要么删掉,要么就让它放着,因为维护它的成本是零,只要它不参与构建、不拖慢测试、不在每次重命名时冒出来烦你。

按这个标准过一遍,我项目里真正需要处理的债少了一大半。剩下的问题就变成:怎么记住它们。

让每条 TODO 带上到期条件

我以前的 TODO 长这样:

PYTHON1 行
1
# TODO: 这里以后改成增量更新

写了等于没写。半年后打开,我不知道当时想改成什么、为什么没改、现在还需不需要改。更糟的是这种注释会繁殖,一个文件里堆七八条,最后我干脆全部无视。

现在的格式是固定的:

PYTHON1 行
1
# DEBT(2026-03): 现在每次全量重算,数据过万条就得改成按 updated_at 增量拉

规则有两条。第一,括号里是月份,不是精确日期,因为我只需要它在我扫的时候能被比对出来。第二,冒号后面必须写清「现在是什么样、什么条件下必须动」,不允许出现「优化」「改进」「重构」这类无法判断是否完成的词。

配套一个十几行的脚本,放在仓库根目录:

PYTHON12 行
1
2
3
4
5
6
7
8
9
10
11
12
import re, pathlib, datetime

pat = re.compile(r'DEBT\((\d{4}-\d{2})\):\s*(.+)')
now = datetime.date.today().strftime('%Y-%m')

for p in pathlib.Path('.').rglob('*.py'):
if '.venv' in str(p):
continue
for i, line in enumerate(p.read_text().splitlines(), 1):
m = pat.search(line)
if m and m.group(1) <= now:
print(f'{p}:{i} {m.group(2)}')

我把它挂在提交前的检查里,过期条目会打印出来,但不阻断提交。这一点我犹豫过:阻断提交更严格,可一个人写代码的时候,被自己的脚本拦住很容易直接加 --no-verify 绕过,反而养成坏习惯。打印出来让我看见就够了,看见之后我会做一个决定。

决定只有三种:现在做;把月份往后推,并在注释里补一句为什么推;或者直接删掉代码。不允许的是无声地改日期,那和没写一样。

还债的动作要小到一顿饭能做完

一个人的项目,最大的开销不是写代码,是重新进入上下文。我的经验是,任何预估超过半天的事,我都会一直往后推,推到它变成一条两年前的 TODO。所以还债必须切碎。

我实际做过的、一小时内能收尾的动作大概是这些:把一个二百行的函数拆成三个,行为一点不改;给一条没有日志的关键路径加三行日志;把写死的路径挪进配置文件;删掉一个永远为 true 的分支;给一个没有测试的纯函数补三个用例。

反面同样重要:不在同一次提交里既重构又改行为。一个人维护没人兜底,混在一起的提交出了问题,二分定位会很痛苦。我的规矩是一个 commit 只做一件事,哪怕它只有五行。

依赖升级也按这个粒度处理。我大概每月挑一天,只升 patch 版本,跑一遍测试;minor 版本看一眼 changelog 再决定;major 单独排期。如果一个依赖已经落后三个大版本以上,我的选择通常是不追了,等那个模块下次真要改的时候连着代码一起重写。硬追一个落后两年的版本,花掉的时间经常比重写那块功能还多,而且重写至少产出的是我自己看得懂的代码。

有些债我是故意欠的

一个人维护时,「下次回来还能看懂」的权重远高于扩展性。所以我主动欠着这些债:不上微服务,就一个进程;不上消息队列,同步调用,失败了重试加报警;数据库只有一个,不拆;抽象层级压到最低,三段相似的代码就让它相似着,不抽基类。

这些做法在团队里大概会被 review 拦下来。一个人维护时它们反而省心,因为抽象带来的收益是多人和并行开发,而这两个前提在我这里不存在,剩下的只有成本:多一层间接,多一个需要记住的约定。

但故意欠债有个前提:得写下来。我在 README 里有一节叫「为什么不」,记着为什么用同步不用队列、为什么没有缓存层、为什么所有定时任务都在一个进程里。写下来的作用不是给别人看,是防止半年后的自己兴冲冲地把它「改好」,然后再花两个周末把它改回来。

这周我把脚本跑了一遍,跳出来三条已经过期一年以上的记录。两条我当场补了,剩下一条打开一看,那段功能的入口早就被另一条路径取代,代码直接删了,测试也跟着删。删完之后仓库小了,测试快了几秒,没有别的影响。

下次再往代码里写 DEBT 的时候,我会先问自己一句:这条记录,什么情况下必须做决定。

关于本文

由 Lu's Blog 撰写,采用 CC BY-NC 4.0 许可协议。

#工程实践 #技术债 #独立开发

评论