Skip to content

技术债务治理 ​

“技术债”可能是被滥用最多的词:任何看着不顺眼的旧代码都被叫做技术债,“还债”变成了“我想重写”的理由。结果是:

  • 花三个月重写了一个模块,新代码又带来了一批新 bug,旧功能还漏了几个;
  • 代码里有几百个 TODO,最早的一个是五年前写的;
  • 某个依赖三年没升级,一次安全漏洞通报后,升级它要连带改动几十处;
  • 向业务方申请时间“还技术债”,被一句“那这段时间不交付功能了?”挡了回来。

这篇讲清楚什么是真正的技术债,以及怎样用最小的代价、持续地治理它。

读完这篇你会

  • 区分真正的技术债和“看不顺眼的旧代码”
  • 选择顺手还、专项还,或者干脆不还
  • 用渐进替换代替大重写,并防止新债不断产生

1. 原理:债是要付利息的 ​

技术债和金融借款很像:为了更快上线(借钱),做了一些不完美的取舍(借款),之后每次修改这块代码都要多花时间、多冒风险(利息)。

判断某块代码是不是技术债,关键不是它“好不好看”,而是它现在是否在持续产生利息:

是技术债(在付利息)不是技术债
每加一个功能都要改十个地方代码风格老旧,但运行稳定、没人需要改
没有测试,每次改动都提心吊胆,经常引入 bug用了老技术栈,但满足需求、没有安全问题
依赖有已知安全漏洞,且已停止维护你个人不喜欢的写法
只有一个人看得懂,他请假时没人敢动一个已经很久没人碰、也不需要碰的模块

一个运行良好、没人需要修改的老模块,哪怕写得再丑,也不需要“还债”,因为它没有在产生利息。

2. 债的几种类型 ​

类型表现典型处理
设计债模块耦合严重、职责混乱渐进重构,引入清晰的边界
测试债关键逻辑没有测试改动前先补测试
依赖债依赖老旧、有漏洞、停止维护定期小步升级
知识债没有文档,只有某个人知道为什么这么写补充决策记录和关键注释

还有一个重要区分:有意的债(当时权衡过、有记录)和无意的债(当时不知道这样有问题)。有意的债不可怕,可怕的是没有记录的债,三个月后没人知道这段奇怪的代码为什么存在,也不敢动。

3. 让债被看见:记录它的“利息” ​

记录技术债时,重点不是“这里很烂”,而是它造成了什么具体成本:

text
标题:订单状态判断散落在 12 个文件中
影响:每次新增订单状态,需要修改 12 处,平均多花 2 天,最近 3 个月因此产生 2 个线上 bug
建议:把状态流转规则集中到一个模块
预估:3 天

有了这样的记录,就能比较不同债务的优先级,也能和业务方讨论。

4. 坑一:大重写,最后两头都顾不上 ​

现象:决定“推倒重来”,用新技术重写整个系统。半年过去了,新系统还没完成;老系统还在持续接需求,新功能要在两边各做一遍;上线时发现旧系统里有很多没人记得的特殊逻辑没有迁移过去。

原因:重写意味着在很长一段时间里同时维护两套系统,而旧系统里沉淀的大量隐性规则,往往只有在出问题时才会被发现。

修复:用绞杀者模式(Strangler Fig)渐进替换。这个名字来自一种榕树:它沿着老树生长,逐渐包裹住老树,最终取而代之,而老树在整个过程中一直“活着”。

text
1. 在旧模块前面加一层统一的入口(接口、路由或适配层)
2. 新功能直接在新模块里实现
3. 每次挑一小块旧功能迁移到新模块,入口把这部分请求转给新模块
4. 每一步都可以上线、可以验证、可以回退
5. 旧模块的功能被逐步“掏空”,最后安全删除

系统在整个过程中一直可用,每一步的风险都很小。

5. 坑二:代码里几百个 TODO,没人管 ​

现象:// TODO: 这里以后要优化 写了几百个,没有人知道哪些重要、哪些已经过时,也没有人去处理。

修复:TODO 必须关联一个可追踪的任务,并写清楚影响:

js
// TODO(#1287): 优惠计算未处理“满减 + 折扣”叠加的情况,目前通过限制叠加规避,见任务说明

可以配置 lint 规则,要求每个 TODO 都附带任务编号;定期清理已经完成或不再需要的 TODO。

6. 坑三:依赖多年不升级,最后升不动了 ​

现象:某个框架三年没升级,跨了好几个大版本。一次严重安全漏洞通报后,必须马上升级,结果牵一发动全身,改了两周。

原因:依赖升级的成本,会随着拖延时间非线性增长。每次落后一个小版本,升级只需要几分钟;落后好几个大版本,就要同时处理所有累积的不兼容变化。

修复:

  • 用 Dependabot、Renovate 等工具自动提醒和创建升级请求,小步、定期地升级;
  • 有安全漏洞的依赖优先处理,不能拖;
  • 每次升级前确保关键流程有测试保护,升级后跑一遍。

真实的漏洞应急和老项目升级过程,可以参考 PHP 严重漏洞处置与升级。

7. 坑四:一边还旧债,一边欠新债 ​

现象:专门安排了一个月还技术债,三个月后,新的问题又堆了一大堆。

修复:防止新增比偿还更重要:

  • 代码评审时识别新债:如果因为时间紧迫必须妥协,就明确记录下来(为什么、影响是什么、计划什么时候处理);
  • 用工具守住底线:依赖方向、分层规则、禁止的写法,写成 lint 规则或架构检查,在 CI 里自动执行;
  • 童子军法则:每次修改一块代码时,顺手让它比你来之前更干净一点(补一个测试、改一个命名、删一段死代码),但不要借机做大范围重构。

8. 坑五:和业务方说不清为什么要还债 ​

现象:

text
❌ “我们需要停下来两个月还技术债。”
   业务:“那这两个月不交付功能了?”

修复:把技术债翻译成业务能理解的语言:速度、稳定性、风险。

text
✅ “现在每次调整订单规则,都要多花 3 天,最近还因此出了 2 次线上事故。
   花 2 周把订单规则集中起来后,同类需求预计 1 天就能完成,
   这个季度还有 6 个订单相关的需求,节省的时间够多做 2 个功能。”

还债的理由来自它对未来交付速度和稳定性的改善,而不是“代码应该干净”。

9. 什么时候还、怎么还 ​

债的情况策略
有安全漏洞立即处理
频繁导致 bug、严重拖慢开发专项偿还,明确目标和完成标准
偶尔不方便,但能绕过记录下来,在修改相关代码时顺手偿还
很少有人碰、运行稳定不还,等它自然被替换或淘汰

检查清单 ​

  • [ ] 所说的技术债能说出具体的“利息”(时间、bug、风险)
  • [ ] 债务被记录在可追踪的任务里,而不只是存在于某个人的脑子里
  • [ ] 每个 TODO 都关联了任务编号
  • [ ] 用渐进替换代替大重写,每一步都能上线和回退
  • [ ] 依赖小步定期升级,安全漏洞优先处理
  • [ ] 有 lint 和架构检查防止新债,妥协时有记录
  • [ ] 和业务沟通时,用时间、稳定性和风险来说明价值

延伸阅读 ​

为复用而记录,为理解而整理。