真实世界:重构如何在团队里活下来
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 与硬件演进让具体热点漂移更快——度量更不可省。