核心问题

为什么代码库会自然变乱?

熵增定律说:

封闭系统总是趋向于无序。

映射到软件工程:

如果代码库没有持续整理,它一定会变乱。

这不是团队素质差,也不是某个人不够努力。软件系统只要持续承接需求、修 bug、接入外部系统、应对时间压力,就会自然产生局部妥协。

这些妥协会一点点累积,最后变成技术债。

技术债不是脏代码

很多人把技术债理解成“代码写得烂”。这个理解太浅。

更准确地说:

技术债是当前系统结构和未来变化方向之间的摩擦。

一段代码哪怕写得很漂亮,如果它让下一个需求很难落地,它也是债。

一段代码哪怕有点粗糙,如果它清楚、局部、容易删除,也未必是债。

所以判断技术债,不是看代码丑不丑,而是看:

  1. 它是否让变化变慢?
  2. 它是否让错误更容易发生?
  3. 它是否让新人更难理解系统?
  4. 它是否把一个领域概念藏进了隐式约定?
  5. 它是否让多个模块必须一起改?

熵增来自哪里

代码库的混乱通常不是一次大爆炸,而是很多小偏差慢慢叠加。

1. 需求压力

“这个功能今天必须上线。”

于是我们写:

if (user.role === "enterprise_admin") {
  // special case
}

一次可以接受,十次之后系统就变成条件分支森林。

2. 命名漂移

最开始 User 表示登录账号。

后来它又表示:

  • 现实中的人
  • 课程学习者
  • 付费客户
  • 企业成员
  • 管理员

名字没有变,但含义不断扩张。最后所有人都在用同一个词说不同的事。

这类债很隐蔽,因为代码可能还能跑,但团队已经失去共同语言。

3. 边界侵蚀

一个模块为了省事,直接读取另一个模块的内部字段:

if (subscription.status === "active" && subscription.endsAt > now) {
  // grant course access
}

短期看很方便,长期会让 subscription 的内部状态变成全系统共享知识。

更稳的做法是把规则放在边界后面:

subscriptionPolicy.canGrantCourseAccess(subscription, now)

或更直接:

canAccessCourse(accountId, courseId)

4. 派生状态失控

例如同时存:

user.isCustomer
subscription.status
order.status
payment.status

这些字段之间如果没有严格状态机,就会开始不一致。

“他到底是不是客户?”这个问题会变成数据库考古。

5. 临时方案永久化

很多永久复杂性都来自一句话:

先这样,之后再改。

如果没有明确的清理机制,“之后”通常不会来。

技术债的三种类型

1. 局部债

局部函数太长、命名不好、重复代码、条件分支混乱。

这种债比较容易还,因为影响范围小。

2. 结构债

模块边界错了,数据所有权不清,领域概念放错地方。

这种债更贵,因为它影响多个功能。

例如课程访问判断散落在各处:

checkout service
course page
mobile API
admin panel
notification worker

每个地方都自己判断一次 canAccessCourse,规则迟早不一致。

3. 语言债

团队对核心概念没有共同语言。

例如:

  • User 到底是人还是账号?
  • Customer 是付过钱的人,还是当前有有效订阅的人?
  • Active 是账号活跃,订阅有效,还是最近登录?

语言债最危险,因为它会让会议、文档、代码和数据库都开始互相误导。

对抗熵增的基本动作

1. 每次提交都让系统干净一点

不要把重构想成大型项目。

更好的习惯是:

每一次提交,都让系统比之前更容易理解一点。

可以是:

  • 改一个更准确的名字
  • 删除一个死分支
  • 把重复判断收进一个函数
  • 给一个含糊状态补上明确枚举
  • 把散落的业务规则集中到一个地方

这叫持续对抗熵增。

2. 发现第三次重复时,停下来找变化轴

第一次重复,可以复制。

第二次重复,可以继续观察。

第三次重复,应该停下来问:

这里真正重复的是什么?

不要急着抽公共函数。先找变化轴。

如果重复的是课程访问规则,就抽 canAccessCourse

如果重复的是数据加载机制,才抽 repository 或 query helper。

3. 把隐式规则变成显式名字

坏代码:

if (order.status === "paid" && !order.refundedAt) {
  // ...
}

更好的代码:

if (orderPolicy.isRevenueRecognized(order)) {
  // ...
}

或者:

if (isPaidAndNotRefunded(order)) {
  // ...
}

命名不是装饰。命名是把隐式知识变成显式概念。

4. 让模块拥有自己的规则

如果 Subscription 的状态规则到处被别人判断,说明边界正在泄漏。

可以让它变成:

subscription.isActiveAt(now)

或在服务层集中:

subscriptionPolicy.isActive(subscription, now)

关键不是面向对象还是函数式,而是:

规则应该有唯一、清楚、可被信任的位置。

5. 让临时方案带上过期机制

如果必须写临时代码,要让它可以被找回来:

// Temporary until enterprise billing supports seat-level invoices.
// Remove after billing-v2 migration.

更好的是配合 issue、测试或 feature flag。

临时方案不可怕。可怕的是没有清理路径的临时方案。

重构不是重写

很多人一说技术债,就想重写系统。

但重写通常很危险:

  • 旧系统里的隐性规则会被漏掉。
  • 新系统在完成前没有业务价值。
  • 两套系统并行时复杂度翻倍。
  • 团队容易低估迁移成本。

更稳的方式是小步重构:

  1. 先补测试或观察点。
  2. 把关键规则命名。
  3. 把重复判断集中。
  4. 明确模块边界。
  5. 一次迁移一个调用点。

真正高级的重构不是推倒重来,而是:

在系统还活着的时候,持续替换它的骨架。

熵增与职业信条

工程师的工作不只是交付功能,还包括维护系统的可变性。

一个成熟工程师会在写代码时同时问两件事:

  1. 这个需求今天怎么上线?
  2. 这个改动会不会让下一次变化更容易?

如果只回答第一个问题,系统会越来越难改。

如果只回答第二个问题,又容易陷入过度设计。

平衡点是:

为今天写具体代码,为已经出现的变化留下清楚边界。

小结

  1. 代码库天然会变乱,持续整理是工程工作的组成部分。
  2. 技术债不是丑代码,而是结构和变化方向之间的摩擦。
  3. 熵增常来自需求压力、命名漂移、边界侵蚀、派生状态失控和临时方案永久化。
  4. 最危险的债是语言债:团队用同一个词表达不同概念。
  5. 对抗熵增靠小步持续重构,而不是动不动重写。
  6. 每次提交都应该让系统比之前更容易理解一点点。