跳到主要内容

真实世界:重构如何在团队里活下来

1. 这一章讲什么

前面十一章的方法都有一个隐含前提:代码归你管、测试网在、随时能提交。真实工程里这些前提各有缺口——代码可能是别的团队的、测试可能是零、改动要过数据库、性能不能不管。原书把这些内容放在第 2 章(原则篇的后半),我们把它们集中到这里:它既是方法书的「工程环境适配层」,也是全书主张压力测试最大的地方。

2. 进度问题:经济账,不是道德账

「重构会拖慢进度」的印象是最大的阻力:「这可能是导致人们没有充分重构的最大阻力所在」1。作者的总回答一句话:「重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值」——所以当真遇到权衡(一个大重构很必要、眼前的功能又很小),先加功能再重构也是合法选项,「做这个决定需要判断力」,他明说无法量化(换算成数字来比较)2

两条经验观察撑住这个回答:行业里「重构不足的情况远多于重构过度的情况」3;而且压制重构的不只是经理——「其实程序员自己也经常这么干」,有时领导反而希望他们重构4。对技术型经理,讲清设计耐久性假说即可;对不懂技术的,作者的建议出了名的直白:「不要告诉经理!」——受进度驱动的经理要的是快,「我认为最快的方式就是重构,所以我就重构」,这是专业者对结果负责的逻辑5

给团队定调时,作者特别警告一种论证方式:拿「整洁的代码」「良好的工程实践」当道德旗帜是「陷阱」——「重构的意义不在于把代码库打磨得闪闪发光,而是纯粹经济角度出发的考量」,「重构应该总是由经济利益驱动」6。这条与我们第 02 章的「银钳子」自我定位一脉相承。

3. 所有权与分支:代码不是你一个人的

已发布接口:一个函数被外部客户使用时,你无法知道谁在用、用得多频繁——它属于「已发布接口」(published interface):使用者与声明者彼此独立,声明者无权修改使用者的代码7。此时改签名只能走第 05 章的迁移式做法:旧声明标记废弃、保留转发、等客户端迁移。「代码所有权的边界会妨碍重构」,但不阻止——代价是接口变复杂8。组织层面的处方是反对细粒度的强代码所有制,推荐团队所有制;跨团队则用类似开源(社区协作、提交审核)的模式(B 团队改 A 团队代码、提交给 A 审核)9

分支:特性分支开得越久,合并回主线越难,而且难点在语义冲突——「现代版本控制系统都能很好地合并程序文本的复杂修改,但对于代码的语义它们一无所知」:你改了函数名,同事在她的分支上新调用了这个函数,文本合并毫无察觉,集成之后代码才坏10。合并难度随分支存活时间「指数上升」11。于是有了持续集成(CI,即基于主干开发):每人每天至少向主线集成一次12。CI 与重构的关系是本章最实的工程结论:重构常对很多地方做小修改(比如给广泛使用的函数改名),「这样的修改尤其容易造成合并时的语义冲突」,见过太多团队因此放弃重构;「CI和重构能够良好配合,所以Kent Beck在极限编程中同时包含了这两个实践」13。作者还引了交付研究:用 CI 的团队「在软件交付上更加高效」14

测试与遗留问题在这里交汇:没有自测试代码,CI 只能捕获文本冲突;反过来,「只使用一组经过验证是安全的重构手法」的流派能在低测试覆盖的大代码库上工作,但手法特定于语言,「行业里对这种方法的介绍和了解都还不足」——作者把它留给自己的网站15

4. 遗留代码与数据库

遗留系统的死结一句话:「整个故事都很棒,但我们绕不开关底的恶龙:遗留系统多半没测试」16。恶龙的解药在书外——作者直接荐书:《修改代码的艺术》,要义是「先找到程序的接缝,在接缝处插入测试,如此将系统置于测试覆盖之下」;而造接缝本身需要危险的、无测试覆盖的重构——「在这种情况下,安全的自动化重构简直就是天赐福音」17。策略上别打大会战:每次触碰一块代码就把它变好一点点,频繁使用的代码回报最厚。

数据库曾经是「重构经常出问题的一个领域」,但渐进式数据库设计改变了局面:结构修改与代码修改绑在同一批迁移脚本里提交,要迁移到某版本就依次执行脚本18。改名一个字段的完整流程是「并行修改」(expand-contract):先新增新字段(暂不使用)→写逻辑双写新旧字段→逐个把读取切到新字段→观察一段时间无 bug→删除旧字段——每一步系统都活着,这正是第 02 章「小步可停」在数据库上的翻译19

5. 性能:先写出可调优的软件

全书的性能口径在第 12 章给全:策略是「除了对性能有严格要求的实时系统」,写快速软件的秘密就是「先写出可调优的软件,然后调优它以获得足够的速度」20。三种方法对照:预算法(每个组件分好时间空间额度,用于心脏起搏器这类系统)、持续关注法(任何人在任何时刻都优化——「通常不会起太大作用」,而且让代码难维护)、以及利用统计的第三法:程序「把大半时间都耗费在一小半代码身上」,「90%的优化工作都是白费劲的」——先度量找热点,再集中优化21

Ron Jeffries 的框注故事是本节最好的一课:克莱斯勒薪资系统太慢,几位高手凭「对系统的全盘了解」推测瓶颈、甚至顺手改进了设计——结果「我一开始所想的可能性竟然全都不是问题肇因」;真正的问题是空日期区间被反复创建,修掉后「系统速度提升了几乎一倍」,耗时「只花了我们大约五分钟」。教训原话:「哪怕你完全了解系统,也请实际度量它的性能,不要臆测。臆测会让你学到一些东西,但十有八九你是错的」22。这解释了第 01 章那个「查找 1 次变 3 次先不管」的底气:先重构,瓶颈若真出现,热点的定位与优化都更容易。

6. 架构与 YAGNI:不为猜想的未来买单

传统做法应对未来靠灵活性机制——预加参数、预留钩子。作者的账:机制不是免费午餐,「我经常会把灵活性机制弄错」,而且「很多时候这些灵活性机制反而拖慢了我响应变化的速度」23。重构给出替代策略:「我更愿意只根据当前的需求来构造软件,同时把软件的设计质量做得很高」,需求理解加深后再重构出灵活性24。决策判据一句话:要不要现在为未来的变化加机制,评估「如果以后再重构有多困难」——只有未来重构会很困难时才现在加25。这套方法的名字是 YAGNI(「你不会需要它」);作者特意防误读:「YAGNI并不是“不做架构性思考”的意思」——它是把架构、设计与开发过程融合的工作方式,「必须有重构作为基础才可靠」26

至此三块基石合拢:自测试代码(第 04 章)、持续集成(本页)、重构,「彼此之间有着很强的协同效应」;它们是极限编程的内核,也是「三大实践」。作者对行业的观察仍然尖锐:大部分「敏捷」项目只是「徒有其名」,真敏捷要求团队在重构上有能力、有热情27

7. 工具:从文本替换到语法树

自动化重构十年间是领域最大变化。能力的分层很清楚:文本操作(查找替换)「太粗糙」,做完必须跑测试;体面的工具「必须操作代码的语法树」(代码结构的内存表示),这也是 IDE 比文本编辑器先进的地方28。静态类型让更多手法变得完全安全——同一门「函数改名」,动态语言里工具分不清同名函数属于哪个类,只能列出所有调用点让人挑;静态类型下工具能精确圈定,改名可以完全信任29。但工具也有闪失:反射调用(运行时按名字找函数的机制,如 Java 的 Method.invoke)会迷惑不够成熟的工具——所以「即便是最安全的重构,也应该经常运行测试套件」30。方向是语言服务器(Language Server):用独立进程生成语法树、给任意编辑器提供 API,让 Emacs 党也能鱼与熊掌兼得31

JS 版与 Java 版的差异,把前言与各章的散点收拢:示例语言从 Java(第 1 版)换成 JavaScript(第 2 版),因为「大多数读者都能看懂」;手法核心跨语言相同。可见的差异都是语言层:JS 无编译期检查(「编译」泛指 Babel 等预处理)、支持嵌套函数(省掉传参)、构造函数不能返回子类(多态必须经工厂函数)、动态类型让自动化重构少了几分把握。原书自己给边界:「这不是一本“用JavaScript进行重构”的书」,JS 特有的重构不在讨论范围32

8. 三个声音与本书的立场

中文版有三个声音,引用时必须分清:作者 Fowler(措辞克制,假说就说假说);中文版编者注(如那句「Web 版表述仅适用英文原版」的说明)33;译者序的熊节导读——它有独立立场:读者提醒「重构」概念在国内「表里分离」,人人挂招牌、实干「刀劈斧砍」;他猜第 2 版会拔高谈架构级重构,「孰料成书令我们跌破眼镜,Fowler先生不仅没有拔高,反而把工夫做得更扎实了」——删掉大型重构章、新增提炼变量/拆分循环等更细的手法,「千里之行积于跬步」34。「十六字心法」(旧的不变/新的创建/一步切换/旧的再见)是熊节在导读中转述其 ThoughtWorks 同事王健的总结,他只指出它与第 2 版保持对象完整的做法暗合;「对变更过程(而非只结果)的设计」才是熊节自己的分析——两者都不是 Fowler 的原话;他把这本书最终归结为软件开发的匠艺——「对“正确的做事方式”的重视」35。这个立场我们认同,但记账在导读名下。

判断(我们的,不是书里的): 全书的地基按可靠性排序是:①手法与步骤(工程验证充分)→ ②「重构改善经济指标」(行业共识、经验证据雄厚但无对照实验)→ ③设计耐久性假说(作者自认未证明)。引用时应分层,别把③当定理用。如果错,会错在: 如果后续出现可信的对照研究推翻内部质量与交付速度的相关性,②③一起松动,但①的手法作为「行为保持的变换」仍然成立——它们的价值不依赖那条经济论证。

9. 边界与局限

  • 「不要告诉经理」是有争议建议,作者自己标明;它预设了专业者的自律,反向滥用(用重构之名行拖延)不在此防御范围内。
  • 遗留代码的「接缝」技术本书不展开,只有荐书;照书操作仍有真实风险(无覆盖下动手术)。
  • CI 的代价原书只点了一句:必须保证主线随时健康、学会把大功能拆小(用特性开关隐藏未完成功能)12
  • 性能三法中「持续关注法」被否定的理由(分散优化伴随对编译器/硬件的误解)在 2018 年成书后依然成立,但 JIT 与硬件演进让具体热点漂移更快——度量更不可省。

10. 可带走的

  1. 向任何人论证重构,只算经济账:加功能更快、修 bug 更快。
  2. 改外部接口:迁移式做法+废弃标记,给下游留迁移期。
  3. 团队所有制优于按人所有;跨团队用开源式审核。
  4. 分支存活时间与合并痛苦成指数关系;重构型团队优先 CI。
  5. 遗留系统:从接缝处插入测试,一次只把触碰到的代码变好一点。
  6. 数据库重构走 expand-contract:双写、切换、观察、删除。
  7. 性能优化前先度量;90% 的优化精力默认会打空。
  8. 灵活性机制的门槛:「以后再重构有多困难」。
  9. 文本替换级工具改完必须跑测试;最安全的自动重构也要常跑测试。
  10. 引用本书时分清三个声音:作者、编者注、熊节导读。

11. 原文地图

主题原书章原文位置
拖慢进度的阻力第2章 §2.5text/10-ch02.txt:324(搜「拖慢新功能的开发进度」) · text/10-ch02.txt:327(搜「用更少的工作量创造更大的价值」)
重构不足远多于过度第2章 §2.5text/10-ch02.txt:339(搜「重构不足的情况远多于」)
程序员自己也压制第2章 §2.5text/10-ch02.txt:344(搜「程序员自己也经常这么干」)
道德论证是陷阱第2章 §2.5text/10-ch02.txt:350(搜「纯粹经济角度」) · text/10-ch02.txt:352(搜「经济利益驱动」)
不要告诉经理第2章 §2.4text/10-ch02.txt:294(搜「不要告诉经理」)
已发布接口第2章 §2.5text/10-ch02.txt:360(搜「提供给客户的API」) · text/10-ch02.txt:365(搜「代码所有权的边界会妨碍重构」)
团队所有制第2章 §2.5text/10-ch02.txt:372(搜「细粒度的强代码所有制」)
语义冲突第2章 §2.5text/10-ch02.txt:400(搜「对于代码的语义它们一无所知」)
合并难度指数上升第2章 §2.5text/10-ch02.txt:404(搜「指数上升」)
CI 定义第2章 §2.5text/10-ch02.txt:407(搜「Continuous Integration」)
CI 与重构配合第2章 §2.5text/10-ch02.txt:417(搜「CI和重构能够良好配合」) · text/10-ch02.txt:424(搜「软件交付上更加高效」)
安全流派第2章 §2.5text/10-ch02.txt:453(搜「经过验证是安全的重构手法」)
遗留系统的恶龙第2章 §2.5text/10-ch02.txt:470(搜「关底的恶龙」)
接缝第2章 §2.5text/10-ch02.txt:479(搜「接缝」) · text/10-ch02.txt:481(搜「天赐福音」)
数据库迁移脚本第2章 §2.5text/10-ch02.txt:494(搜「数据迁移脚本」)
并行修改第2章 §2.5text/10-ch02.txt:512(搜「并行修改」)
灵活性机制第2章 §2.6text/10-ch02.txt:527(搜「灵活性机制」) · text/10-ch02.txt:532(搜「反而拖慢了我响应变化」)
「以后再重构有多困难」第2章 §2.6text/10-ch02.txt:541(搜「如果以后再重构有多困难」)
YAGNI第2章 §2.6text/10-ch02.txt:544(搜「你不会需要它」) · text/10-ch02.txt:545(搜「不做架构性思考」)
三大实践第2章 §2.7text/10-ch02.txt:565(搜「第一块基石」) · text/10-ch02.txt:572(搜「三大实践」)
徒有其名第2章 §2.7text/10-ch02.txt:562(搜「徒有其名」)
性能三法与 90%第2章 §2.8text/10-ch02.txt:596(搜「先写出可调优的软件」) · text/10-ch02.txt:647(搜「白费劲」)
Jeffries 故事第2章 §2.8text/10-ch02.txt:634(搜「提升了几乎一倍」) · text/10-ch02.txt:642(搜「不要臆测」)
语法树第2章 §2.10text/10-ch02.txt:738(搜「必须操作代码的语法树」)
静态类型第2章 §2.10text/10-ch02.txt:747(搜「很多重构手法会更加安全」)
反射与闪失第2章 §2.10text/10-ch02.txt:763(搜「Method.invoke」)
语言服务器第2章 §2.10text/10-ch02.txt:769(搜「语言服务器」)
《修改代码的艺术》第2章 §2.11text/10-ch02.txt:477(搜「修改代码的艺术」)
编者注一例前言text/07-fm.txt:182(搜「编者注」)
熊节导读:更扎实译者序text/05-fm.txt:20(搜「把工夫做得更扎实」) · text/05-fm.txt:27(搜「千里之行积于跬步」) · text/05-fm.txt:54(搜「匠艺」)

Footnotes

  1. 出处:「重构的原则」第 325 段(text/10-ch02.txt:325,搜「最大阻力所在」)。

  2. 出处:「重构的原则」第 327 段(text/10-ch02.txt:327,搜「用更少的工作量创造更大的价值」)与第 330-331 段(text/10-ch02.txt:331,搜「无法量化决定的依据」)。

  3. 出处:「重构的原则」第 339 段(text/10-ch02.txt:339,搜「重构不足的情况远多于」)。

  4. 出处:「重构的原则」第 344 段(text/10-ch02.txt:344,搜「程序员自己也经常这么干」)。

  5. 出处:「重构的原则」第 294 段(text/10-ch02.txt:294,搜「不要告诉经理」)与第 300-301 段(text/10-ch02.txt:301,搜「我就重构」)。

  6. 出处:「重构的原则」第 349-353 段(text/10-ch02.txt:350,搜「纯粹经济角度」)与第 352 段(text/10-ch02.txt:352,搜「经济利益驱动」)。

  7. 出处:「重构的原则」第 360-363 段(text/10-ch02.txt:360,搜「提供给客户的API」)。

  8. 出处:「重构的原则」第 365 段(text/10-ch02.txt:365,搜「代码所有权的边界会妨碍重构」)。

  9. 出处:「重构的原则」第 372 段(text/10-ch02.txt:372,搜「细粒度的强代码所有制」)与第 379-383 段(text/10-ch02.txt:383,搜「类似开源」)。

  10. 出处:「重构的原则」第 399-400 段(text/10-ch02.txt:400,搜「对于代码的语义它们一无所知」)。

  11. 出处:「重构的原则」第 404 段(text/10-ch02.txt:404,搜「指数上升」)。

  12. 出处:「重构的原则」第 407 段(text/10-ch02.txt:407,搜「Continuous Integration」);CI 的代价(特性开关)在第 411-412 段(text/10-ch02.txt:411,搜「特性开关」)。 2

  13. 出处:「重构的原则」第 417-418 段(text/10-ch02.txt:417,搜「CI和重构能够良好配合」)。

  14. 出处:「重构的原则」第 424 段(text/10-ch02.txt:424,搜「软件交付上更加高效」)。出处标注 [Forsgren et al],即《加速》的作者团队研究。

  15. 出处:「重构的原则」第 453 段(text/10-ch02.txt:453,搜「经过验证是安全的重构手法」)与第 456 段(text/10-ch02.txt:456,搜「本书不对其多做介绍」)。

  16. 出处:「重构的原则」第 470 段(text/10-ch02.txt:470,搜「关底的恶龙」)。

  17. 出处:「重构的原则」第 477-481 段(text/10-ch02.txt:479,搜「接缝」)与第 481 段(text/10-ch02.txt:481,搜「天赐福音」)。

  18. 出处:「重构的原则」第 491-495 段(text/10-ch02.txt:494,搜「数据迁移脚本」)。

  19. 出处:「重构的原则」第 497-512 段(text/10-ch02.txt:512,搜「并行修改」)。

  20. 出处:「重构的原则」第 595-596 段(text/10-ch02.txt:596,搜「先写出可调优的软件」)。

  21. 出处:「重构的原则」第 646-647 段(text/10-ch02.txt:647,搜「白费劲」);持续关注法的否定在第 605-609 段(text/10-ch02.txt:605,搜「持续关注法」)。

  22. 出处:「重构的原则」第 620 段(text/10-ch02.txt:620,搜「全都不是问题肇因」)、第 634-635 段(text/10-ch02.txt:635,搜「只花了我们大约五分钟」)与第 642 段(text/10-ch02.txt:642,搜「不要臆测」)。框注署名 Ron Jeffries 在第 644 段(text/10-ch02.txt:644,搜「Ron Jeffries」)。

  23. 出处:「重构的原则」第 527-533 段(text/10-ch02.txt:532,搜「反而拖慢了我响应变化」)。

  24. 出处:「重构的原则」第 536 段(text/10-ch02.txt:536,搜「只根据当前的需求来构造软件」)。

  25. 出处:「重构的原则」第 541 段(text/10-ch02.txt:541,搜「如果以后再重构有多困难」)。

  26. 出处:「重构的原则」第 544-547 段(text/10-ch02.txt:545,搜「不做架构性思考」)。

  27. 出处:「重构的原则」第 565 段(text/10-ch02.txt:565,搜「第一块基石」)、第 572-573 段(text/10-ch02.txt:572,搜「三大实践」)与第 562 段(text/10-ch02.txt:562,搜「徒有其名」)。

  28. 出处:「重构的原则」第 733 段(text/10-ch02.txt:733,搜「文本操作」)与第 738 段(text/10-ch02.txt:738,搜「必须操作代码的语法树」)。

  29. 出处:「重构的原则」第 747 段(text/10-ch02.txt:747,搜「很多重构手法会更加安全」)与第 748-754 段(text/10-ch02.txt:751,搜「手工决定修改哪些调用点」)。

  30. 出处:「重构的原则」第 762-764 段(text/10-ch02.txt:763,搜「Method.invoke」)。

  31. 出处:「重构的原则」第 767-770 段(text/10-ch02.txt:769,搜「语言服务器」)。

  32. 出处:「前言」第 128 段(text/07-fm.txt:128,搜「我选择了用JavaScript」)、第 145 段(text/07-fm.txt:145,搜「用JavaScript进行重构」);「编译」脚注见「重构,第一个示例」第 319 段(text/09-ch01.txt:319,搜「Babel」);构造函数限制见「重构API」第 1076 段(text/19-ch11-11-api.txt:1076,搜「只能返回当前所调用类的实例」)。

  33. 出处:「前言」第 182 段(text/07-fm.txt:182,搜「编者注」)。

  34. 出处:「重读《重构》,呼唤匠艺(译者序)」第 19-20 段(text/05-fm.txt:20,搜「把工夫做得更扎实」)与第 27 段(text/05-fm.txt:27,搜「千里之行积于跬步」);两版对比在第 22-28 段(text/05-fm.txt:23,搜「大型重构」)。

  35. 出处:「重读《重构》,呼唤匠艺(译者序)」第 54 段(text/05-fm.txt:54,搜「匠艺」);十六字心法在第 36-44 段(text/05-fm.txt:36,搜「十六字」);「对变更过程(而非只是结果)的设计」在第 49-50 段(text/05-fm.txt:49,搜「变更过程」)。