跳到主要内容

接手别人的代码

这一章讲三件事: 为什么所有代码库都是乱的;「技术债」这个词到底指什么、怎么谈; 以及在别人的代码上安全修改的一整套手艺。 读完你会知道:接手一个旧代码库,第一步不是重写,而是立栅栏。

1. 这一章讲什么

新工程师的第一批真活,几乎总是在一个已有的代码库上做。原书开场拿法国阿尔勒的古罗马竞技场打比方:罗马灭亡后,一座小镇直接建在竞技场里——有墙、有排水,住起来很方便,但后来的居民大概会抱怨当初的建筑师让这地方难以改造成城镇。原书的结论是:代码库就像那座竞技场,一代人写几层、改几层,「与代码打交道很艰难,但也是你首先必须要做的几件事情之一」1

本章在全书链条里的位置:第 01 章讲的是你怎么学、怎么问;这一章往下走一层,讲你面对的第一个实物——别人写的、正在运行的代码。改它的手法(先测试后变更)是第 05 章测试的直接前置。

2. 顶层全景

你打开一个旧代码库

├─ 为什么乱? 软件的熵(变化的自然副作用,不是前人的罪)
│ └─ 技术债:熵的主要来源(有本金、有利息、分四种)

├─ 怎么改? 五步法:定义变更点 → 找测试点 → 打破依赖 → 写测试 → 动手改
│ └─ 日常纪律:童子军原则 + 代码异味随手清 + 小步提交

└─ 什么时候大动? 很少:十倍好门槛、创新代币、重写的真实代价

图说:全章是一条「先解释乱、再教改法、最后劝阻大改」的链条。

3. 核心原理

3.1 软件的熵:乱是常态,不是罪证

先看现象。你打开团队的代码,很快会发现缺陷:风格不统一、有奇怪的补丁、有没人敢动的模块。第一反应往往是「写这代码的人不行」。

原书的说法正相反:混乱的代码是变化的自然副作用,不要把它归咎于开发者2。需求在变、技术在变、修 bug 和赶性能的改动都会引入复杂性——这些东西加在一起,让代码自发地从有序走向无序。物理里把这种趋势叫熵;软件的熵就是代码库走向混乱的趋势3

这个说法有两个直接推论。第一,对旧代码保持平常心:你看到的乱,大概率不是谁的过失,而是这代码活了很久的证据。第二,熵可以管理:代码风格和缺陷检测工具、代码评审、持续的小幅重构都是压熵的手段——分别对应本书的第 05、06 章和本章 3.3 节。

3.2 技术债:有本金,有利息,还分四种

技术债(为了快点交付,明知代码有不足而欠下的未来工作)是熵的一个主要来源。原书用金融做类比:本金是当初那笔没做干净的活;利息是之后每次绕着它写变通代码所付的代价——变通办法复制、巩固,复杂的东西越缠越多,最后长出 bug4

有两个边界要立刻钉住,防止这个词被用坏:

  • 你不同意的技术决策不是债,你不喜欢的代码也不是。 要够得上「债」,必须是你被迫持续付利息(绕着它写变通)或它随时可能引爆严重问题5
  • 乱用「技术债」会稀释它:喊得多了,真正要紧的债反而排不进优先级6

原书引用了软件界名人马丁·福勒的 2×2 矩阵,把债按「有意/无意」和「鲁莽/谨慎」分成四种7:

鲁莽的谨慎的
有意的「没时间设计,先这样」「先发布,之后处理」
无意的「什么是分层?」(不知道自己不知道)「现在才知道当时该怎么做」

矩阵里最值得看的是右列。谨慎的有意是债的正当形态:在已知不足和交付速度之间做交易,只要团队有计划地还,这就是好债。谨慎的无意则是成长的自然产物——有些教训只有事后才知道,健康的团队靠项目回顾来发现这种债8

矩阵还带来一个反直觉的结论,值得单独抄下来:技术债不可避免,它甚至可能是成功的标志——项目只有活了足够久,才会变得无序9

主走查:一笔真实的债是怎么被提议偿还的

光有矩阵还不够——债要有人还,得先说服团队。原书给了一个五步模板:按事实陈述情况;描述风险和成本;提出方案;讨论备选方案(不行动也是备选);权衡利弊10。我们拿原书自带的例子走一遍全程:工程师约翰娜认为该把公司那个既管认证又管授权的登录服务拆成两个服务,她写了一封邮件11

第 1 步 事实:登录服务的不稳定占我们 On-Call(值班,见第 08 章)问题
的 30% 以上;不稳定主要来自认证与授权逻辑交织。
第 2 步 风险与成本:安全相关特性难以测试;给客户的数据安全承诺
越来越难兑现;下次合规审计可能被点名。
第 3 步 方案:拆分服务,把授权代码移出去——并承认这是大工程。
第 4 步 备选:改用后端团队的授权服务——她自己论证了为什么不合适
(对方是系统间授权,我们是面向用户的授权),但留了余地:
「也许会有更好的方法同时满足两种场景」。
第 5 步 权衡:大工程 vs 稳定性 + 正确性,她认为值得。

注意她没有说的东西:邮件里没有「这代码又老又难看」这类价值判断——原书在模板旁边特意警告,呼吁要建立在成本与收益上,不要建立在审美上,而且要白纸黑字写出来,别只在会上喊12。这封邮件是本章的锚:后面第 09 章(技术设计文档)讲的其实是同一件事的加长版——当债务提案大到需要换架构时,它就升级成了一份设计文档。

3.3 安全变更五步:先立栅栏,再动土

在有行为的旧代码上改代码,和在新代码库里写代码是两种手艺——你必须在不破坏现有行为的前提下改。原书引用了迈克尔·费瑟斯《修改代码的艺术》的五步13:

1 定义变更点(要改哪几行)
2 寻找测试点(从哪里能观察到这段代码的行为)
3 打破依赖关系(必要时调整代码结构,让测试变得可能)
4 编写测试(钉住现有行为——这是「栅栏」)
5 进行修改和重构(栅栏立好后,才动手改)

原书配了个园艺比喻:第 5 步是播种,前 4 步是清理空地、立栅栏——栅栏没立好之前,野生动物会闯进来挖走你的作物14。这里有个词要当场讲清:重构指在不改变软件外在行为的前提下改进代码内部结构;它通常发生在加新特性之前(让特性好加)或之后(把改动理顺)15

五步里风险最大的是第 3 步。这里的「依赖」不是指你引用了哪个类库,而是指测试你的代码时需要的对象和方法——比如一段代码直接读系统时钟,你就没法测试「未来某个时刻会发生什么」。打破依赖的手段包括:把大方法拆成小方法分别测;引入一个接口(一组约定好的方法签名)给测试提供简单替身;注入明确的控制点(比如把「现在几点」变成可以由外部传入的参数(把写死的值改成能从外面递进来的值))16。有一条禁令要划出来:不要为了测试方便把私有方法改成公开的——那等于为了浇花拆掉花房的墙,封装(对象自己管理内部细节、外部只通过接口交互的机制)一旦破坏,你要保证「行为没变」的面积反而变大了17

3.4 日常纪律:过手的代码要比之前更干净

五步法管一次大改动,日常还有一条更轻的纪律。原书引用童子军的原则「住过的营地要比住之前更干净」,并把它搬到代码上:每修一个 bug、加一个特性,顺手清理与本次改动相关的乱代码,不要满仓库找脏东西——「要随缘一些」18

配套两条操作规则。其一,清理代码的提交和改变行为的提交要分开——这样回滚某个行为变更时不会把清理一起丢掉,而且小提交更容易评审19。其二,清理时对代码异味(不是 bug、但采用了已知会招致麻烦的写法的代码)保持嗅觉。原书给了一个具体例子,值得拿真实的数走一遍:

if (a < b)
a += 1;

这段代码本身完全正确——Java 允许 if 后面跟单条语句。但它是异味:如果后人顺手在下面加一行 a = a * 2;,Java 靠大括号而不是缩进分组,无论条件是否成立,a 都会被翻倍。拿具体的数看:设 a=3、b=5,原意是「a<b 时 a 变 4」;加了那行之后,不管 a、b 是多少,a 都会变成 8。如果当初写了大括号,这个手误就很难发生20。过长的方法、重复代码、过多的参数,都是同类异味,代码检查工具(俗称 linter,静态扫描代码的工具)能自动抓出大部分。

提交信息也有纪律。 开发中要尽早、频繁提交(把代码改动记入版本控制系统,即 VCS),但对外呈现的提交信息要压缩、重写成一条清楚的:开发过程中的「哎呀」「修复测试错误」对别人没有价值21。原书转述了克里斯·比姆斯的建议,核心几条:标题与正文用空行分开;标题不超过 50 个字符、用命令式语气(「修复登录超时」,不是「修复了」);正文解释改了什么、为什么改,不解释怎么改22。把提交信息和工作单号关联(如 [MYPROJ-123] 前缀),还能让脚本和工具自动把代码和需求串起来。

3.5 避坑:什么时候值得大动

最后是刹车。新工程师最常见的冲动有两种:引入新技术栈、重写旧系统。原书给了四道闸门。

第一道:十倍好。 原书引本·霍洛维茨《创业维艰》:新产品至少要比现有流行方式好十倍,人们才会真的迁移——两三倍的改进不足以让任何人承担切换成本23。这个判据同样适用于你重构别人代码的冲动:小收益不值得付出大代价。

第二道:创新代币。 每引入一项新技术,就花掉一枚公司级别稀缺的「创新代币」——花在新技术上的精力,本可以花在新特性上24。花在哪有讲究:要花在服务公司核心竞争力的技术上。比例也很悬殊:引入一个新框架或数据库花一枚,引入一门新语言可能要花三枚——整个构建、测试、IDE、运维链条都得跟着它走25。原书还补了一句很硬的观察:价值数十亿美元的公司,很多建立在「成熟但有些无聊的编程语言」之上;语言的年龄和不够时髦,几乎从来不是反对它的理由26

第三道:故障模式的可预测性。 旧东西以可预测的方式坏,新东西以令人惊讶的方式坏——缺乏成熟度意味着更小的社区、更少的文档、更差的兼容性27

第四道:第二系统综合征。 原书引《人月神话》:第一个系统完成了工作但笨拙有限;有经验的人看清了所有问题,于是用全部聪明才智造第二个系统——每个东西都可配置、可注入——结果第二个系统通常是臃肿的烂摊子28

重写的真实代价,原书给了账单:Twitter 的 A/B 测试(把用户分组、各看一版、比较效果)工具 Duck Duck Goose 重写,预计一个季度建成、第二个季度淘汰旧系统;实际一年才让新版稳定、再花六个月退役旧版,资深工程师和管理者预估的 6 个月最终膨胀到 18 个月,超出预算超过 100 万美元29。结论写得很直白:把重构当最后的手段,对自己的重构欲望要诚实。

4. 作者的判断与证据

  • 「技术债不可避免、可能是成功的标志」:作者判断,论证来自定义本身(无意之债无法预防),不是数据。

  • 保守选型:主要证据是丹·麦金利的演讲「选择保守的技术」(故障模式好理解)加两条亲历故事——Scala/SBT 在 LinkedIn 的水土不服(构建系统互不兼容、版本二进制不兼容,克里斯称 Scala 后来成了他的「眼中钉」)30;以及 10 人 iOS 游戏公司不该上前沿机器学习(让计算机从数据里学规律的技术)算法的假想例。

  • DDG 重写账单:亲历故事,有具体数字(6 个月变 18 个月、超 100 万美元),但样本量(可统计的例子数)是 1。

  • 五步变更法:转述费瑟斯《修改代码的艺术》,属于业界成熟实践,不是本书原创。

  • 童子军原则:业界流传的做法,原书明确注明「互联网上的编程传说经常引用」——来源不可考,原书取其有用。

5. 边界与局限

  • 「十倍好」的出处是创业产品语境(霍洛维茨谈的是创业公司的产品),原书把它平移到内部代码重构上,是作者的类比引申,不是原论证的适用范围。
  • 风险不对称没有量化:本章讲了「重写常超支」,但没讲「不重写、继续背债」的成本如何度量;什么时候该忍、什么时候该动手,书里只有定性判据。
  • 创新代币没有定价机制:一枚代币对应多少工时、由谁批准,书里没说,实际执行全看团队文化。
  • 2×2 矩阵里的「谨慎的无意」依赖团队有回顾文化;没有回顾机制的公司,这类债基本只能靠事故暴露。

6. 可带走的

  1. 看到乱代码先默念:熵是变化的副作用,不是前人的罪;但熵要管,放任会变成本章的债;
  2. 「技术债」三个字要说得出口就得有本金和利息——不喜欢的代码不是债;
  3. 谈债用五步模板:事实→风险成本→方案→备选→权衡,写成文字,别喊口号;
  4. 改旧代码先立栅栏:变更点→测试点→打破依赖→钉住现状的测试→再动手;
  5. 永远不为测试把私有改成公开——那是拆墙浇花;
  6. 过手的代码要比之前更干净,但只清相关的;清理提交与行为提交分开;
  7. 缺大括号这类异味用具体的数想一遍(a=3 时它到底变成几),就知道它为什么危险;
  8. 引入新技术前问:这要花几枚创新代币?公司核心竞争力在哪?语言的话乘以三;
  9. 重写之前,先把 DDG 那张账单(6 个月变 18 个月)在心里过一遍。

7. 原文地图

主题原书章原文位置
竞技场开场第3章 玩转代码text/12-ch03.txt:3(搜「阿尔勒」)
软件的熵第3章 玩转代码text/12-ch03.txt:22(搜「软件的熵」)
本金与利息第3章 玩转代码text/12-ch03.txt:36(搜「本金」)
滥用「技术债」第3章 玩转代码text/12-ch03.txt:44(搜「滥用这个专有名词」)
福勒矩阵第3章 玩转代码text/12-ch03.txt:51(搜「技术债矩阵」) · text/12-ch03.txt:57(搜「现在我们知道了当时应该怎么做」)
债不可避免/成功标志第3章 玩转代码text/12-ch03.txt:80(搜「技术债总是不可避免」)
债务提案五步第3章 玩转代码text/12-ch03.txt:97(搜「按事实陈述情况」)
约翰娜邮件 30%第3章 玩转代码text/12-ch03.txt:111(搜「30%」) · text/12-ch03.txt:134(搜「约翰娜」)
费瑟斯五步第3章 玩转代码text/12-ch03.txt:152(搜「修改代码的艺术」) · text/12-ch03.txt:156(搜「定义变更点」)
栅栏比喻第3章 玩转代码text/12-ch03.txt:162(搜「栅栏」)
打破依赖三法第3章 玩转代码text/12-ch03.txt:158(搜「打破依赖关系」) · text/12-ch03.txt:192(搜「注入明确的控制点」)
不要破坏封装第3章 玩转代码text/12-ch03.txt:195(搜「封装」)
童子军原则第3章 玩转代码text/12-ch03.txt:210(搜「住过的营地」) · text/12-ch03.txt:217(搜「随缘」)
分开提交第3章 玩转代码text/12-ch03.txt:218(搜「分开」)
缺大括号异味第3章 玩转代码text/12-ch03.txt:224(搜「代码清单 3-1」) · text/12-ch03.txt:251(搜「大括号」)
代码异味定义第3章 玩转代码text/12-ch03.txt:222(搜「代码异味」)
提交信息七条第3章 玩转代码text/12-ch03.txt:324(搜「空行」) · text/12-ch03.txt:328(搜「命令式语气」)
十倍好第3章 玩转代码text/12-ch03.txt:347(搜「好十倍」)
创新代币第3章 玩转代码text/12-ch03.txt:377(搜「创新代币」) · text/12-ch03.txt:395(搜「3 枚」)
无聊的语言第3章 玩转代码text/12-ch03.txt:403(搜「无聊的编程语言」)
Scala 眼中钉第3章 玩转代码text/12-ch03.txt:431(搜「眼中钉」)
特立独行与分叉第3章 玩转代码text/12-ch03.txt:441(搜「特立独行」) · text/12-ch03.txt:465(搜「分叉」)
第二系统综合征第3章 玩转代码text/12-ch03.txt:497(搜「第二系统综合征」)
DDG 账单第3章 玩转代码text/12-ch03.txt:508(搜「Duck Duck」) · text/12-ch03.txt:527(搜「100 万美元」)

Footnotes

  1. 出处:「第3章 玩转代码」第 3 段(text/12-ch03.txt:3,搜「阿尔勒」)与第 3 段(text/12-ch03.txt:3,搜「竞技剧场」)。原文:代码库就像阿尔勒的那个圆形剧场。

  2. 出处:「第3章 玩转代码」第 21 段(text/12-ch03.txt:21,搜「归咎于开发者」)。

  3. 出处:「第3章 玩转代码」第 22 段(text/12-ch03.txt:22,搜「软件的熵」)。

  4. 出处:「第3章 玩转代码」第 36 段(text/12-ch03.txt:36,搜「本金」)。原文:利息是随着代码的发展没有解决的潜在不足,因为实施了越来越复杂的变通方法。

  5. 出处:「第3章 玩转代码」第 42 段(text/12-ch03.txt:42,搜「迫使团队」)与第 44 段(text/12-ch03.txt:44,搜「滥用这个专有名词」)。

  6. 出处:「第3章 玩转代码」第 44 段(text/12-ch03.txt:44,搜「滥用这个专有名词」)。

  7. 出处:「第3章 玩转代码」第 555 段(text/12-ch03.txt:555,搜「马丁·福勒」)与第 51 段(text/12-ch03.txt:51,搜「技术债矩阵」)。

  8. 出处:「第3章 玩转代码」第 79 段(text/12-ch03.txt:79,搜「项目回顾」)。原文:健康的团队使用诸如项目回顾等做法来发现无心之债。

  9. 出处:「第3章 玩转代码」第 80 段(text/12-ch03.txt:80,搜「技术债总是不可避免」)与第 82 段(text/12-ch03.txt:82,搜「变得无序」)。

  10. 出处:「第3章 玩转代码」第 97 段(text/12-ch03.txt:97,搜「按事实陈述情况」)。

  11. 出处:「第3章 玩转代码」第 109 段(text/12-ch03.txt:109,搜「登录服务」)与第 111 段(text/12-ch03.txt:111,搜「30%」)。邮件落款为约翰娜,见第 134 段(text/12-ch03.txt:134,搜「约翰娜」)。

  12. 出处:「第3章 玩转代码」第 102 段(text/12-ch03.txt:102,搜「价值判断」)与第 102 段(text/12-ch03.txt:102,搜「以书面形式」)。

  13. 出处:「第3章 玩转代码」第 152 段(text/12-ch03.txt:152,搜「修改代码的艺术」)与第 156 段(text/12-ch03.txt:156,搜「定义变更点」)。

  14. 出处:「第3章 玩转代码」第 161 段(text/12-ch03.txt:161,搜「播撒种子」)与第 162 段(text/12-ch03.txt:162,搜「栅栏」)。

  15. 出处:「第3章 玩转代码」第 144 段(text/12-ch03.txt:144,搜「不改变软件行为」)。

  16. 出处:「第3章 玩转代码」第 187 段(text/12-ch03.txt:187,搜「技术手段可以打破依赖关系」)与第 192 段(text/12-ch03.txt:192,搜「注入明确的控制点」)。

  17. 出处:「第3章 玩转代码」第 195 段(text/12-ch03.txt:195,搜「封装」)。原文:将私有方法和变量公开以让测试用例访问代码,但同时也破坏了封装,这是一种糟糕的方式。

  18. 出处:「第3章 玩转代码」第 210 段(text/12-ch03.txt:210,搜「住过的营地」)与第 217 段(text/12-ch03.txt:217,搜「随缘」)。

  19. 出处:「第3章 玩转代码」第 218 段(text/12-ch03.txt:218,搜「分开」)。原文:尽量将清理代码的提交和改变行为的提交各自分开。

  20. 出处:「第3章 玩转代码」第 224 段(text/12-ch03.txt:224,搜「代码清单 3-1」)与第 251 段(text/12-ch03.txt:251,搜「大括号」)。原文:与 Python 不同,Java 忽略了缩进,而是依靠大括号来分组语句。

  21. 出处:「第3章 玩转代码」第 313 段(text/12-ch03.txt:313,搜「哎呀」)。原文:频繁地提交往往会导致无意义的信息,如「哎呀」或「修复测试错误」;提交前应重置分支、压缩提交、写清楚的提交信息。

  22. 出处:「第3章 玩转代码」第 324 段(text/12-ch03.txt:324,搜「空行」)与第 328 段(text/12-ch03.txt:328,搜「命令式语气」)。这是克里斯·比姆斯的建议,原书全文转述了七条。

  23. 出处:「第3章 玩转代码」第 347 段(text/12-ch03.txt:347,搜「好十倍」)。

  24. 出处:「第3章 玩转代码」第 377 段(text/12-ch03.txt:377,搜「创新代币」)。

  25. 出处:「第3章 玩转代码」第 390 段(text/12-ch03.txt:390,搜「一种新的语言」)与第 395 段(text/12-ch03.txt:395,搜「3 枚」)。

  26. 出处:「第3章 玩转代码」第 403 段(text/12-ch03.txt:403,搜「无聊的编程语言」)。

  27. 出处:「第3章 玩转代码」第 365 段(text/12-ch03.txt:365,搜「保守技术上出现的故障模式很好理解」)。

  28. 出处:「第3章 玩转代码」第 497 段(text/12-ch03.txt:497,搜「第二系统综合征」)。

  29. 出处:「第3章 玩转代码」第 526 段(text/12-ch03.txt:526,搜「18」)与第 527 段(text/12-ch03.txt:527,搜「100 万美元」)。

  30. 出处:「第3章 玩转代码」第 431 段(text/12-ch03.txt:431,搜「眼中钉」)与第 425 段(text/12-ch03.txt:425,搜「SBT」)。