2.19 重构与技术债务
概述与动机
重构是在不改变代码对外行为的前提下,改变其内部结构。你重命名一个变量、拆分一个过长的函数、提取一个类、把一团缠绕的条件语句收拢成读者能够看懂的样子,而程序的行为与之前完全一致。最后这一点,正是这门学科的全部要义。按定义,重构是保持行为不变的,一旦你同时改变了行为,你就不再是在重构,而是在同时做两件有风险的事,并且用其中一件掩盖了另一件。本章有意将两者视为不同的行为,因为把它们混为一谈,正是大多数重构出问题的根源。
在大型团队中,这一点比对单打独斗的开发者更为重要,因为你正在清理的代码,是成百上千其他人阅读、依赖、并且不敢轻易触碰的代码。重构,是让一个共享代码库在多年时间和人员更替中依然保持可居住状态的方式。它与软件构建(第 2.9 章)直接相关,日常编码质量正是在那里确立的;它也与测试策略(第 2.4 章)相关,正是这张安全网让重构变得可行。它同样牵涉到一个更棘手的问题:技术债务()那些走捷径、设计老化、以及被推迟的清理工作所累积起来的成本,它们会让未来的每一次改动都变得更慢。重构是偿还这笔债务的主要方式,所以这两个主题理应放在同一章里。
在企业和政府机构的场景中,风险会上升。这些系统往往长期存在,常常已经运行了几十年,而且经常处于审计和变更管控制度之下,任何代码改动都被当作一次受治理的事件来对待。你不能在一个长周末里直接重写一个公民福利系统;你要以小步、可逆、有证据支撑的方式对它进行现代化改造,而这正是有纪律的重构所能提供的。在众多团队和长期存在的系统之间协调这项工作(第 10.4 章),是大规模工程中最具代表性的挑战之一,而一旦做错,组织就会陷入僵局,无法改动自己已经不再理解的软件。
关键原则
- 重构保持行为不变;如果你在改变代码的行为,那是一次独立的改动,应单独进行。
- 一套值得信赖的测试套件,是安全重构的前提条件,而不是可有可无的附加项。
- 以小步、有命名、可逆的方式推进,并确保每一步之后代码都能正常工作。
- 让技术债务可见并被追踪,把偿还债务当作稳定的常规产能来投入,而不是靠英雄式冲刺。
- 重构你已经在改动的那部分代码,在那里清理才划算。
- 并非所有债务都值得偿还;稳定的、很少被触碰的、或即将退役的代码可以放任不管。
- 度量内部质量是为了辅助判断,绝不能把它当作一个被人钻空子的目标。
建议
严格区分重构和行为变更
在开始之前就决定好你要做的是哪一种,绝不要在同一次提交中把两者混为一谈。当你重构时,之前通过的测试在之后也必须原样通过,因为可观察的行为并没有发生变化。当你改变行为时,把它作为一次独立的提交,配上它自己的测试。原因很实际:如果一次混合改动导致出了问题,你无法判断究竟是你的结构调整引入了缺陷,还是你的行为变更引入了缺陷;在代码评审(第 2.5 章)中,评审者也无法清晰地分别推理这两部分。行之有效的习惯,是 Martin Fowler 提出的”两顶帽子”规则:你随时都戴着”重构帽”或”功能帽”中的一顶,你清楚自己戴的是哪一顶,并有意识地切换。分开提交还能让版本控制历史保持可读,这样一位在排查故障时使用二分查找的工程师,就能放心地跳过那些纯重构的提交。
在动手重构之前,先建立起可信赖的安全网
没有测试的重构,只是编辑加祈祷。在重构任何有分量的东西之前,你需要一套你信得过的测试套件,能在你引入行为变更时把它捕获出来,这正是测试策略(第 2.4 章)的核心论点。对于已经有良好覆盖率的代码,先运行测试,以小步重构,每一步之后再次运行测试。对于没有测试的遗留代码,诚实的做法是先编写特征化测试(characterization test)。特征化测试不断言代码应该做什么,而是记录代码当下实际的行为,包括它的种种怪癖,这样任何行为上的变化都会表现为一次失败的测试。Michael Feathers 让这种方法广为人知,而它所针对的正是大型组织普遍面临的处境:代码能运行、至关重要,却没有测试。一旦当前行为被牢牢固定下来,你就可以在其下方安全地重构,然后才在其上改变行为。
学会识别代码异味,并应用小型的、有命名的重构手法
代码异味是一种表面迹象,提示底下可能有需要关注的东西:一个变得过长的函数、一个知道得太多的类、重复的逻辑、一份过长的参数列表、名不副实的命名。异味是一种提示,而不是判决,所以你应该去调查,而不是盲目服从它。应对方式是采用 Fowler 目录中一种小型的、有命名的重构手法:提取函数(Extract Function)、重命名变量(Rename Variable)、移动方法(Move Method)、用多态取代条件表达式(Replace Conditional with Polymorphism),还有另外几十种。使用这些有命名手法的价值在于,每一种都很小、易于理解、机械可靠地安全,而且往往直接得到你的 IDE 支持。你用许多微小而可靠的步骤组合出重大的改进,全程保持代码处于可运行状态,而不是冒险迈出一大步、却无法验证结果。
优先选择机会式重构,把大规模行动留给真正的结构性需求
大多数重构应该是机会式的,融入你已经在做的工作之中。“童子军规则”很好地概括了这一点:让代码比你发现它时更干净一点。当你为了添加功能或修复缺陷而触碰一个文件时,你已经理解了那个角落,在那里做一点小小的清理,会随时间不断累积,而无需征得任何人的许可或申请单独的预算。有计划的重构行动()团队暂停功能开发、去重构一大片区域()有时确有必要,但代价高昂、很难在产品压力下排出档期,而且如果那片区域测试不充分,风险还很大。把这类大规模行动留给机会式清理触及不到的结构性问题,并用你正为之付出的变更成本证据来支撑这个决定。优先选择细水长流式的小规模清理;它比偶尔一次的英雄式重写更持久。
对大规模结构性变更使用绞杀者无花果模式
当整个子系统都需要替换时,不要尝试一次持续一年、最后合并的大爆炸式重写;这正是许多现代化项目走向失败的方式。使用绞杀者无花果模式(strangler fig pattern),这个名字由 Martin Fowler 借用那种缠绕树木生长、并逐渐取而代之的藤蔓命名而来。你在旧系统前面放一个门面(facade),每次把一小片功能路由到门面背后的新代码上,在生产环境中验证它,如此重复,直到旧系统被完全包围,可以被移除为止。每一片都很小、可交付、可回退,因此风险始终受控,而价值持续不断地到来。一个近亲手法叫”抽象分支”(branch by abstraction),在单一代码库内部实现相同效果:你在想要替换的东西之上引入一层抽象,在两者共存期间于其背后构建新实现,逐步将调用方切换过去,一旦没有任何东西依赖旧实现,就将其删除。这两种方法都能让一个遗留系统在保持存活的同时不断演进,而这也是大多数大型组织真正负担得起的唯一一种现代化方式。
把技术债务当作一个投资组合来对待,并让它可见
沃德·坎宁安(Ward Cunningham)提出的债务隐喻,区分了两样东西:本金(那段混乱的代码或那次走的捷径本身)和利息(此后每一次改动因此而多付出的额外努力)。并非所有债务都是等同的。Fowler 的象限把债务沿两条轴线分类:有意为之与无心造成,审慎与鲁莽。审慎且有意为之的债务(“我们现在先交付,下个冲刺再清理,而且我们清楚这么做的代价”)是一项合理的业务决策。鲁莽且无心造成的债务(“什么是设计模式?“)则纯粹是损害。管理层的工作()它与决策制定和治理(第 1.5 章)以及把债务当作投资组合来对待的思路相关()是让债务变得可见,以便被理性讨论:在工作追踪系统中记录重要的债务事项、给代码打上标签,并记录你正在为其支付的利息,这样偿还债务才能凭证据而不是凭谁抱怨得最响来争取产能。你看不见的债务,就无法管理。
把偿还债务当作稳定的常规产能来投入,而不是靠英雄式冲刺
失败的模式是把清理工作当作”等事情忙完了再说”的事,而这一天永远不会到来。持久有效的模式是为偿还债务设立一份固定的、受保护的产能:每个周期中明确划出一部分,或者达成一项常设约定,让清理工作与同一区域的功能开发同步进行。行不通的做法是周期性的英雄式冲刺,靠某个人牺牲一个周末把所有问题都修完,因为这种方式不可持续、缺乏评审,而且往往会自我瓦解。稳定的产能能压低利息支出,避免债务不断累积直到危机逼出一次昂贵重写的大起大落循环。这既是一项管理层面的承诺,也是一项工程实践,理应纳入你在系统整个生命周期中规划软件维护(第 3.7 章)的方式。
度量内部质量,但不要让度量指标本身变成目标
你可以用圈复杂度(一个函数内独立路径数量的度量)、重复度、测试覆盖率、变更失败率,以及你怀疑存在问题的区域中变更所需的时间等信号来度量内部质量。这些数字有助于发现债务集中在何处,也有助于观察长期趋势。危险在于古德哈特定律(Goodhart’s law):一旦某个度量指标变成了目标,它就不再衡量任何真实的东西。强制要求一个覆盖率数字,你得到的就是什么都不断言的测试;奖励低复杂度分数,你得到的就是把逻辑涂抹到更多函数中以规避这个指标。把度量指标用来开启讨论、定位热点,绝不要把质量指标与一个人们有动机去钻空子的关卡绑定在一起。
知道什么时候不该重构
重构是一种投资,而有些代码永远不会带来回报。如果一个模块很稳定、很少被触碰,并且在你偶尔必须改动它的那些场合已经被理解得足够透彻,清理它就是把精力花在你本来不用支付的利息上。如果某段代码即将退役,重构它就是在打磨一件你马上要扔掉的东西。这门学科的纪律,是把你的清理预算花在改动频繁又痛苦的地方()那里降低利息才能真正产生复利效应()而放过那些安静的角落。
权衡:优点与缺点
| 方法 | 优点 | 缺点 |
|---|---|---|
| 机会式重构(童子军规则) | 成本低、持续进行、无需单独预算、随时间产生复利 | 覆盖不均;常改动的文件在改善,冷门文件却在腐坏 |
| 有计划的重构行动 | 能解决机会式清理触及不到的结构性问题 | 代价高昂;与功能开发争夺资源;测试不充分时风险大 |
| 绞杀者无花果模式/抽象分支 | 增量式、可回退、系统保持在线、风险受控 | 从纸面上看比重写慢;需要纪律才能真正完成 |
| 大爆炸式重写 | 从零开始;没有遗留约束 | 失败率高;见效时间长;存在行为差异 |
| 有意为之的审慎债务 | 现在就交付价值;有明确、有计划的偿还安排 | 一旦偿还计划迟迟未被排期,就会变成鲁莽债务 |
| 用指标卡关的质量管控 | 客观、可见、能及早发现偏离 | 容易被钻空子;会惩罚细微差别;可能拉低真实质量 |
核心张力在于当下的速度与未来的可改动性之间,这是真实存在的。当截止日期确实紧迫、而债务是审慎且被追踪的,交付一个捷径可能就是正确的选择。错误的做法,是假装这笔债务没有代价,或者任其在不知不觉中累积,直到系统的改动成本高到无法承受。解决办法是每一次都把这笔交易讲清楚:给这笔债务命名、估算利息、有意识地做出决定,并把这个决定记录下来,这样偿还工作才能被排期,而不是被遗忘。一个知情借债、并稳步偿还的团队,能多年保持高速;一个盲目借债的团队,最终会陷入停滞。
与团队讨论的问题
我们如何防止重构和行为变更混入同一次提交?我们的评审真的在强制执行这一点吗? 这是整章内容的根本纪律,也是在截止日期压力下最常被违反的一条,因为”既然都改到这里了,顺手清理一下”感觉上很高效,于是把两者一起交付了。代价会在之后显现:当一次混合提交搞坏了生产环境时,没人说得清究竟是结构调整还是功能改动造成的,通过历史记录做二分查找也不再可信。找出最近的几个拉取请求,诚实地检查有多少混用了这两顶帽子。与之相对的考量是摩擦成本,因为把工作拆成独立提交,前期要多花一点力气。这个答案应该塑造你们的提交规范和评审清单。
我们的技术债务在哪里?我们正在为它支付多少利息?谁来决定偿还哪些债务? 大多数团队都答不上这个问题,而这恰恰就是真正的问题所在,因为你看不见的债务,最终会由嗓门最大的人来决定如何处理,而不是依据成本真正所在之处来处理。让它可见,意味着追踪重要的债务事项、给代码打标签,并收集证据,说明哪些区域让改动变得缓慢且容易出错。与之相对的拉力是,花在偿还债务上的每一个小时,都是没有花在功能开发上的一个小时,所以这必须是一个与领导层共同做出的组合投资决策,这与你如何治理工程工作(第 1.5 章)相关联。带上你们的变更失败率数据,以及那份”大家都害怕触碰”的文件清单。这个答案应该转化为一份受保护的、稳定的偿债产能,而不是一句”等忙完了再清理”的模糊意向。
我们代码库中的哪些部分应该有意识地不去重构?我们又如何判断? 把一切都重构,和什么都不重构一样,都是一种失败,因为把精力花在清理稳定、很少被触碰、或即将退役的代码上,等于是在偿还一笔你本来不欠的贷款的利息。这个判断是真实存在的:一个模块看起来很丑,但如果从没人改动它,投入其中仍然是选错了地方。把你们的改动频率数据和复杂度信号放在一起看,因为高变动率和高复杂度的交集,正是清理能产生复利的地方,而低变动率的代码通常最好放任不管。与之相对的风险是,“我们就放着不管”会变成永远不去碰任何困难之处的借口。这个答案应该给你一份明确的、值得投入的热点候选清单,以及可以忽略那些安静角落的许可。
我们真的信任测试套件到足以重构那些我们最需要改动的代码吗?我们又该在哪些地方先写特征化测试? 一张你信不过的安全网,会把重构变成编辑加祈祷,而在一个大团队里,最让人害怕的代码通常也是测试最少的代码,而这恰恰是清理最能带来回报的地方。带上你们热点区域的覆盖率和变更失败率数据,并诚实地评估:如果一次结构调整改变了行为,哪些关键模块会完全不给你任何警示。与之相对的考量是,为遗留代码编写特征化测试是一项缓慢、不起眼、又不能交付任何功能的工作,因此很容易被无限期推迟。在处于审计和变更管控之下的企业和政府系统中,这些被固定下来的测试同时也是”该变更保持了行为不变”的证据,所以为它们投入资金,既是一项安全措施,也是一项合规措施;这个答案应该指明,在任何人动手之前,哪些区域必须先建立测试脚手架。
当一个子系统确实需要被替换时,我们如何在渐进式的绞杀者无花果方案和重写之间做决定?谁有权否决重写? 大爆炸式重写是摆在桌面上最诱人、也最容易失败的选项,因为从零开始在纸面上看,总是比继续忍受旧约束显得更便宜。对大型组织而言,渐进式路径(一个门面,每次一小片,在生产环境中验证)能让系统保持存活并把风险控制在范围内,但速度更慢,需要有纪律才能真正完成,而且要与”重新开始”的冲动相抗衡。带上该子系统的改动频率地图、对一次重写在产生价值之前要运行多久的诚实估计,以及一次并行重写必须弥合的行为差异。在政府和受监管的环境中,一次持续多年、最后才合并的重写在审计之下几乎难以幸存,所以这个答案应该默认选择绞杀者无花果模式或抽象分支,并把任何重写都当作一种例外,必须用证据去论证。
我们如何利用内部质量指标找出债务集中的地方,同时不让某个数字变成人们钻空子的目标? 复杂度、重复度、覆盖率、变更失败率等指标,是大型组织唯一能够纵览那些没有任何一个人能通读的代码的方式,然而一旦某个指标被绑定到一个关卡或绩效评审上,古德哈特定律就会起作用,这个数字就不再衡量任何真实的东西。带上一些已经在用某个指标驱动行为的例子,并问一问:它究竟是在开启讨论,还是在悄悄奖励那些什么都不断言的测试,以及被涂抹到各个函数中以规避某个阈值的逻辑。与之相对的拉力是,领导层想要一个简单的仪表盘数字,而”请运用判断力”比一条绿色进度条更难被接受。在指标会反馈到治理报告中的企业和政府场景中,要明确说明质量信号是用来指导投资、定位热点的,绝不能用来给个人设关卡;这个答案应该在”为了学习而度量”和”为了评判而度量”之间划出一条清晰的界线。
行业视角
初创企业。 只有寥寥几名工程师,也没有多余的跑道,因此只做机会式重构:每次提交只戴一顶帽子,让历史保持可二分查找,并保留一份简短诚实的清单,记录你们故意走过的那些捷径。不要发起清理行动,也不要去打磨稳定的模块;把稀缺的注意力花在那一个人人都害怕的文件上,只在某个改动真正让你担心时才去写特征化测试。在这个阶段,刻意且可见的债务是可以接受的;鲁莽且不可见的债务才会要了你的命。
小型企业。 没有专职的平台或工具专家,预算也很紧张,因此要依靠你们的 IDE 和语言生态系统免费提供的东西:自动化的重命名和提取操作、一个代码检查工具(linter),以及一个基本的覆盖率信号。把大多数债务当作日常工作中就能处理的事情,而不是要请顾问来解决的问题,并且优先购买维护良好的库,而不是自己构建、之后再不得不重构自己的代码。把那笔难得的付费投入,留给唯一那个因为运行缓慢而正在直接让你流失客户的系统。
企业。 在众多团队之间,问题在于投资组合治理:一份共享的债务登记册、按改动频率和复杂度对热点进行一致的标注,以及为每个团队的产能预留一份受保护的偿债份额,让清理工作不再默认输给功能开发。将”两顶帽子”纪律和特征化测试的实践标准化,这样任何在团队之间流动的工程师都能遇到同一套规则,并对跨团队协调的结构性变更使用绞杀者无花果模式和抽象分支。让质量指标保持在提供信息的层面,这样它们既能定位债务,又不会在绩效评审中被钻空子。
政府机构。 处于严格审计和变更管控之下的长期存在的系统,让有纪律的重构不仅是一项工程资产,也是一项合规资产:把结构调整与行为变更严格分开,能让审计人员清楚看出哪些提交改变了行为、哪些只是做了整理。采购和透明度方面的规则,更青睐小步、可逆、有证据支撑的改动,而不是大爆炸式重写,所以要默认采用绞杀者无花果模式,并用特征化测试记录行为得到了保留。把债务登记册及其偿还计划纳入系统的维护记录,让监督机构获得他们所要求的可追溯性。
示例
初创企业。 一家六人规模的初创公司交付速度很快,也清楚自己正在承担债务,因此把两件成本低廉的事做得很扎实。每一个拉取请求只戴一顶帽子:重构提交与功能提交是分开的,这让他们的历史即便在高速迭代下也依然可以二分查找。他们还保留着一份简短诚实的清单,记录着自己故意走过的捷径,并为每一条附上一句关于其利息成本的说明。当某个支付模块变成人人害怕的文件时,这份清单加上他们的变更失败历史,就足以为花两天时间提取出一个更干净的边界提供依据。他们编写特征化测试来固定当前行为,借助 IDE 的重命名和提取操作在其下方重构,并且从不去碰那些没人改动的稳定模块。他们承担的债务是刻意为之、也是可见的,所以它从未演变成鲁莽的那种。
企业。 一家全球物流公司运行着一个已有十五年历史的订单系统,许多团队每周都要改动它。他们没有选择重写,而是采用了绞杀者无花果模式:一个门面被放在这个单体系统前面,每次把一项有明确边界的能力路由到门面背后的新服务上,在下一片开始之前先在生产环境中验证。跨团队和一个长期存在的系统之间协调这项工作(第 10.4 章)是其中最难的部分,因此他们维护着一份共享的债务登记册,按改动频率和复杂度标注热点,并为每个团队的产能预留固定的一份用于偿还债务。内部质量指标用来指导应该关注哪里,但绝不会成为任何人绩效评审的关卡,这让这些数字保持诚实。两年下来,这个单体系统在稳步缩小,而从没有哪一次改动会让整个系统承担风险。
政府机构。 一家国家税务机关必须在严格的审计和变更管控规则下,对一个已有几十年历史的评估平台进行现代化改造,在这里,每一次代码改动都是一次受治理、有证据支撑的事件。大爆炸式重写是不可能的,所以他们使用抽象分支:在遗留的计算引擎之上引入一层抽象,在其背后构建新的实现,调用方每次迁移一条税务规则,每一次迁移都被记录为一次小型、可逆的改动,并配有特征化测试证明行为没有改变。由于重构被严格地与任何立法层面的行为变更分开,审计人员能够清楚看出哪些提交改变了行为、哪些只是做了结构调整。债务登记册及其偿还计划,成为该系统维护记录(第 3.7 章)的一部分,为监督机构提供了他们所要求的可追溯性。
业务论证:动机、投资回报率与总拥有成本
重构和偿还债务带来的回报,是以低成本持续改动软件的能力,而对大多数系统来说,生命周期成本的大头都是维护,所以总拥有成本在很大程度上正是由此决定的。技术债务的利息,是以领导层已经在追踪的货币来支付的:更慢的交付速度、更高的变更失败率、从事故中恢复所需的更长时间,以及工程师对最令人害怕的代码避而远之。当你让债务可见、并稳定地为偿还提供资金时,你就降低了最重要的那些区域中每一次未来改动的成本,也避免了那种因债务被忽视而最终逼出一次昂贵紧急重写的大起大落模式。
采纳这套做法的成本不高,而且大多是文化层面的:建立”两顶帽子”纪律、在需要重构的地方搭建安全网、维护一份债务登记册,并为偿还债务保护一份稳定的产能份额。而忽视它的代价则会悄悄复利累积。利息会附加在每一次改动上,直到速度崩溃,组织发现自己陷入僵局,无法安全地修改一个它已经不再理解的系统,而这正是所有结果中代价最高昂的一种。要向领导层证明这一点,就把债务直接与他们已经关心的交付指标联系起来,并把偿还债务的工作,包装成一项有可衡量回报的投资组合决策,而不是工程师要求腾出时间来整理东西。
反模式与陷阱
- 把重构和行为变更混在一起: 一次提交同时做了两件事,导致一旦出问题就无法归因,历史记录也变得不可信。
- 在没有安全网的情况下重构: 对未经测试的代码进行结构调整,只能靠祈祷,这是凭信念编辑代码。
- 大爆炸式重写: 一次性替换掉一个正在运行的系统,这种模式失败率高,见效时间也长。
- 把重构当成一次英雄式的周末冲刺: 未经评审、不可持续的清理,最终会自我瓦解,而不是变成稳定的产能。
- 不可见的债务: 没有人追踪的捷径,导致偿还工作是被抱怨的音量驱动,而不是被真实的成本驱动。
- 钻质量指标的空子: 达成了覆盖率或复杂度的目标,真实质量却在下降,因为度量指标本身变成了目标。
- 重构错了代码: 打磨稳定或即将退役的模块,而真正的热点却在持续消耗成本。
- 无休止的重构: 永无止境的结构调整,从不交付价值,这是”从不清理”的另一种镜像失败。
成熟度模型
- 第 1 级,初始阶段: 重构是临时的、被动反应式的,经常与行为变更混在同一次提交中。没有可信赖的安全网,技术债务不可见且未被追踪,清理工作只在偶尔的英雄式冲刺中发生,或者根本不发生。
- 第 2 级,发展阶段: 一些团队会把重构和行为变更分开,并在有测试的地方依靠测试;有命名的重构手法和特征化测试出现在一些局部区域。各团队之间的实践并不一致,债务被讨论、有时也被记录,但偿还工作要临时与功能开发争抢资源,而且通常会输。
- 第 3 级,标准化阶段: “两顶帽子”纪律、针对遗留代码的特征化测试,以及小型的、有命名的重构手法,已被记录下来,并在整个组织范围内被期待遵循。债务被记录在一份共享登记册中,本金和利息被区分开来,每个周期都会规划一份受保护的偿债产能,并在评审中得到执行。
- 第 4 级,管理阶段: 债务和清理工作依据基准被度量和控制。你们追踪改动频率和复杂度以定位热点,关注已重构区域的变更失败率和变更交付周期,并记录每一项重要债务所付出的利息,这样偿还决策就建立在证据之上,是否投入的取舍也基于趋势而非抱怨的音量。质量信号为投资提供依据,而不会被绑定到人们可以钻空子的关卡上。
- 第 5 级,编排阶段: 债务被当作一个持续再平衡的投资组合来管理,并在整个组织范围内与产品和维护规划相整合。结构性变更常规性地使用跨团队协调的绞杀者无花果模式和抽象分支,偿还工作持续不断,并与改动频繁且痛苦的地方相匹配,这套实践也会随着系统及其风险状况的变化而不断调整,让长期存在的代码在数十年间保持可改动性。
讨论想法
- 你们团队用来保持重构与行为变更分离的规则,实际执行情况如何?它在截止日期压力下会在哪里失效?
- 你们如何依据证据来决定哪些代码值得清理、哪些最好放任不管?
- 在哪些地方,特征化测试能让你安全地重构一个你目前一直回避的遗留区域?
- 对于你们下一次重大的现代化改造,一种绞杀者无花果方案会是什么样子?你们会先引入哪个门面或抽象层?
- 谁负责技术债务登记册?偿还债务实际上是如何在与功能开发争抢产能时胜出的?
关键要点
- 重构保持行为不变;把它与行为变更严格分开,放在不同的提交中。
- 一套值得信赖的测试套件是安全重构的前提条件,而特征化测试能为遗留代码提供这样一套测试。
- 以小步、有命名、可逆的方式推进,优先选择机会式清理,对大型结构性变更使用绞杀者无花果模式或抽象分支。
- 让技术债务可见,把本金和利息区分开,把偿还债务当作稳定的常规产能来投入,而不是靠英雄式冲刺。
- 度量内部质量是为了辅助判断,绝不能当作被钻空子的目标,也不要重构那些稳定或即将退役的代码。
参考文献与延伸阅读
- Martin Fowler, Refactoring: Improving the Design of Existing Code, second edition
- Michael Feathers, Working Effectively with Legacy Code
- Ward Cunningham, The WyCash Portfolio Management System (OOPSLA 1992 experience report, origin of the debt metaphor)
- Martin Fowler, “TechnicalDebtQuadrant” and “StranglerFigApplication” (martinfowler.com)
- Kent Beck, Tidy First? A Personal Exercise in Empirical Software Design
- Steve McConnell, Code Complete: A Practical Handbook of Software Construction
- Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship