核心问题
为什么代码库会自然变乱?
熵增定律说:
封闭系统总是趋向于无序。
映射到软件工程:
如果代码库没有持续整理,它一定会变乱。
这不是团队素质差,也不是某个人不够努力。软件系统只要持续承接需求、修 bug、接入外部系统、应对时间压力,就会自然产生局部妥协。
这些妥协会一点点累积,最后变成技术债。
技术债不是脏代码
很多人把技术债理解成“代码写得烂”。这个理解太浅。
更准确地说:
技术债是当前系统结构和未来变化方向之间的摩擦。
一段代码哪怕写得很漂亮,如果它让下一个需求很难落地,它也是债。
一段代码哪怕有点粗糙,如果它清楚、局部、容易删除,也未必是债。
所以判断技术债,不是看代码丑不丑,而是看:
- 它是否让变化变慢?
- 它是否让错误更容易发生?
- 它是否让新人更难理解系统?
- 它是否把一个领域概念藏进了隐式约定?
- 它是否让多个模块必须一起改?
熵增来自哪里
代码库的混乱通常不是一次大爆炸,而是很多小偏差慢慢叠加。
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。
临时方案不可怕。可怕的是没有清理路径的临时方案。
重构不是重写
很多人一说技术债,就想重写系统。
但重写通常很危险:
- 旧系统里的隐性规则会被漏掉。
- 新系统在完成前没有业务价值。
- 两套系统并行时复杂度翻倍。
- 团队容易低估迁移成本。
更稳的方式是小步重构:
- 先补测试或观察点。
- 把关键规则命名。
- 把重复判断集中。
- 明确模块边界。
- 一次迁移一个调用点。
真正高级的重构不是推倒重来,而是:
在系统还活着的时候,持续替换它的骨架。
熵增与职业信条
工程师的工作不只是交付功能,还包括维护系统的可变性。
一个成熟工程师会在写代码时同时问两件事:
- 这个需求今天怎么上线?
- 这个改动会不会让下一次变化更容易?
如果只回答第一个问题,系统会越来越难改。
如果只回答第二个问题,又容易陷入过度设计。
平衡点是:
为今天写具体代码,为已经出现的变化留下清楚边界。
小结
- 代码库天然会变乱,持续整理是工程工作的组成部分。
- 技术债不是丑代码,而是结构和变化方向之间的摩擦。
- 熵增常来自需求压力、命名漂移、边界侵蚀、派生状态失控和临时方案永久化。
- 最危险的债是语言债:团队用同一个词表达不同概念。
- 对抗熵增靠小步持续重构,而不是动不动重写。
- 每次提交都应该让系统比之前更容易理解一点点。