1、"先这样,回头再改"——那个回头永远没来

为了赶上线,你把一段逻辑硬编码进去,心里想:"先能跑,回头再改。" 三个月后,这段代码扩散成了 5 个地方,没人敢改、没人记得为什么这么写,而"回头"始终没来。

这就是技术债(Technical Debt)——一个由 Ward Cunningham 提出的绝妙比喻:为了短期速度而做的妥协,会在未来收取"利息"。 本课讲:什么是技术债、怎么识别、怎么还,以及重构的正确姿势。

2、技术债:一个借贷的比喻

借债:现在拿到快(提前上线)
利息:以后每次改动都更慢、更容易出错
本金:最终需要花时间做的重构/补齐

关键洞察:技术债不全是坏的。

类型例子评价
审慎的(Deliberate)"我知道这里简化了,为了抢市场先发"✅ 有意识、有计划地借
无意的(Inadvertent)"当时不懂,这么写其实有坑"⚠️ 学习成本,可接受
鲁莽的(Reckless)"不懂也不问,乱写一通"❌ 危险
借债本身不是问题,"借了不还"和"不知道自己借了"才是问题。
有时故意借债是正确决策——比如为了验证 MVP 快速上线。关键是:记账、有计划地还。

3、技术债都有哪些"面"

技术债不只是"代码写得烂":

类型表现
代码债重复代码、超长函数、魔法数字、命名混乱
设计/架构债循环依赖、耦合过重、抽象层级混乱
测试债没有自动化测试、测试脆弱、覆盖率低
文档债没 README、没接口文档、注释过时
依赖债依赖版本老旧、用了停止维护的库
基础设施债手工部署、环境不一致、无监控
知识债只有一个人懂某模块("巴士因子 = 1")

识别信号(出现这些,说明债在收利息了):

- 改一个小功能要动 10 个文件
- 没人敢碰某个模块
- 每次发布都心惊胆战
- 新人上手要三个月
- 同样的问题反复出现
- 测试跑得又慢又经常"意外红"

4、"技术债台账":把债显性化

态度:不追求零技术债(不可能),而是让它可见、可控、可规划。

做法:建一个技术债台账(Debt Register / Backlog):

| ID | 位置 | 描述 | 影响 | 利息 | 估计修复 | 优先级 |
|----|------|------|------|------|----------|--------|
| D1 | order.py | 结算逻辑硬编码税率 | 新地区无法上线 | 高 | 2 天 | P1 |
| D2 | api/ | 无接口测试 | 改动易回归 | 中 | 5 天 | P1 |
| D3 | utils.py | 重复的日期处理 | 维护成本 | 低 | 0.5 天 | P3 |

什么时候还债:

  • 顺手还:碰到就改一点(童子军规则:离开时比来时更干净)。
  • 专项还:每个迭代留固定比例(如 20%)用于还债。
  • 不还:低利息、无人碰、即将退役的模块——主动决定"不还",比假装没债强。

5、重构:改变结构,不改变行为

重构(Refactoring) 的严格定义:

在不改变代码外部行为的前提下,改善其内部结构。

铁律:行为不变。 重构不是"顺便加个功能",也不是"顺便修个 bug"——那是另外的事。混在一起,测试就失去了判定基准,风险陡增。

重构的保障:自动化测试。没有测试的重构,等于闭着眼睛拆炸弹。所以:

安全重构的前提顺序:
  ① 先给要重构的代码补测试(哪怕先是"特征测试"characterization test)
  ② 小步重构,每步跑测试
  ③ 常用 IDE 的自动重构(改名、提函数、内联)

常见重构手法(Martin Fowler《重构》的名录):

坏味道(Smell)重构手法
重复代码提炼函数 / 上移方法
超长函数提炼函数、以查询取代临时变量
超长参数列表引入参数对象
嵌套过深卫语句(提前 return)、提炼函数
魔法数字用常量/枚举命名
发散式变化拆分职责(单一职责)
霰弹式修改内聚相关代码
依恋情结搬移方法
# 重构前:嵌套深、魔法数字、职责混杂
def calc(order):
    total = 0
    for i in order.items:
        if i.type == 1:                     # 1 是什么?
            if i.qty > 10:
                total += i.price * i.qty * 0.9   # 0.9 是什么?
            else:
                total += i.price * i.qty
        else:
            total += i.price * i.qty
    return total

# 重构后:有名有姓、职责清晰
VIP_TYPE = 1
VIP_BULK_THRESHOLD = 10
VIP_BULK_DISCOUNT = 0.9

def item_price(item):
    base = item.price * item.qty
    if item.type == VIP_TYPE and item.qty > VIP_BULK_THRESHOLD:
        return base * VIP_BULK_DISCOUNT
    return base

def calc(order):
    return sum(item_price(i) for i in order.items)

6、"重构" vs "重写":别动不动推倒

重写(Rewrite) 是从头换一套实现。它听起来很爽,实际极其危险:

重写吞噬项目(Second System Effect):
  - 老系统所有隐藏的边界处理、兼容逻辑会"丢失"
  - 新系统要重新踩所有旧坑
  - 期间老系统还得维护(双倍成本)
  - 需求还在变,目标不断移动
  - 常见结局:重写永远不会"完成"

更稳的路径(第 29 课讲过):绞杀者模式 + 渐进重构——旧系统不动,新功能新写法,一块块蚕食替换。

Joel Spolsky 的名言:把重写当成"打扫",是这个行业最容易犯的严重错误之一。"代码是资产,重写往往把它当负债扔掉。"

7、把还债纳入正常节奏

别把还债当"额外负担"。健康的做法:

  • 定义完成的含金量(Definition of Done):包含"测试通过、无新增明显坏味道"。
  • 每次迭代留容量(如 20%)给重构和债。
  • 代码评审拦债:不让明显坏味道轻易合入。
  • 自动化护栏:Lint、格式、覆盖率门禁,让"变烂"没那么容易。
  • 技术债可见:进 backlog,和产品需求一起排优先级(而非永远垫底)。

8、常见误区

误区问题正解
技术债 = 全是坏事因噎废食,不敢快速迭代审慎借债可接受,关键在记账与还
重构时顺手改功能测试基准混乱、风险高行为不变;改功能另开一次
没测试就重构等于赌博先补测试(特征测试)
一出问题就重写丢知识、无限延期渐进重构 + 绞杀者
债永远垫底利息滚雪球固定容量还债 + 进 backlog
追求零技术债不现实、浪费可见、可控、按优先级还

9、动手实验 🎯

任务:为贯穿项目「团队任务清单」做一次技术债清理。

  1. 建台账:通读你的项目,列出 ≥8 条技术债,填入台账(含影响、利息、估计修复、优先级)。
  2. 补测试:选一个"没测试、不敢改"的函数,先写特征测试(哪怕它行为不完美,先固化现状)。
  3. 小步重构:用"提炼函数 + 命名常量 + 卫语句",改进一段嵌套深/魔法数字多的代码,每步跑测试。
  4. 验证行为不变:重构前后,用同一组输入对比输出,确认完全一致。
  5. 写一份"债务计划":未来 3 个迭代,每期还哪些债(用 20% 容量)。
  6. 识别一个"巴士因子=1"的模块,想想怎么降低知识债(结对、文档)。
  7. 反思:你项目里的债,哪些是"审慎借的",哪些是"无意欠下的"?处置方式该有什么不同?

下一课:数据迁移与数据库演进。

10、今天你带走的三句话

  1. 技术债是借贷:审慎借债可接受,关键是记账、有计划地还,不是追求零债务。
  2. 重构 = 不改行为、只改结构;前提是有测试,且小步走。
  3. 别动不动重写:用渐进重构 / 绞杀者模式,把还债纳入每个迭代的固定节奏。

下一课:数据迁移与数据库演进。

标签: 软件工程, 项目管理, 开发流程

添加新评论