在我编程的每个小时,我都会做重构。有几种方式可以把重构融入我的工作过程里。
三次法则
> Don Roberts给了我一条准则:第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是可以去做;第三次再做类似的事,你就应该重构。
>
> 正如老话说的:事不过三,三则重构。
### 预备性重构:让添加新功能更容易
重构的最佳时机就在添加新功能之前。在动手添加新功能之前,我会看看现有的代码库,此时经常会发现:如果对代码结构做一点微调,我的工作会容易得多。也许已经有个函数提供了我需要的大部分功能,但有几个字面量的值与我的需要略有冲突。如果不做重构,我可能会把整个函数复制过来,修改这几个值,但这就会导致重复代码——如果将来我需要做修改,就必须同时修改两处(更麻烦的是,我得先找到这两处)。而且,如果将来我还需要一个类似又略有不同的功能,就只能再复制粘贴一次,这可不是个好主意。所以我戴上重构的帽子,使用函数参数化(310)。做完这件事以后,接下来我就只需要调用这个函数,传入我需要的参数。
> 这就好像我要往东去100公里。我不会往东一头把车开进树林,而是先往北开20公里上高速,然后再向东开100公里。后者的速度比前者要快上3倍。如果有人催着你“赶快直接去那儿”,有时你需要说:“等等,我要先看看地图,找出最快的路径。”这就是预备性重构于我的意义。
>
> ——Jessica Kerr
修复bug时的情况也是一样。在寻找问题根因时,我可能会发现:如果把3段一模一样且都会导致错误的代码合并到一处,问题修复起来会容易得多。或者,如果把某些更新数据的逻辑与查询逻辑分开,会更容易避免造成错误的逻辑纠缠。用重构改善这些情况,在同样场合再次出现同样bug的概率也会降低。
### 帮助理解的重构:使代码更易懂
我需要先理解代码在做什么,然后才能着手修改。这段代码可能是我写的,也可能是别人写的。一旦我需要思考“这段代码到底在做什么”,我就会自问:能不能重构这段代码,令其一目了然?我可能看见了一段结构糟糕的条件逻辑,也可能希望复用一个函数,但花费了几分钟才弄懂它到底在做什么,因为它的函数命名实在是太糟糕了。这些都是重构的机会。
看代码时,我会在脑海里形成一些理解,但我的记性不好,记不住那么多细节。正如Ward Cunningham所说,通过重构,我就把脑子里的理解转移到了代码本身。随后我运行这个软件,看它是否正常工作,来检查这些理解是否正确。如果把对代码的理解植入代码中,这份知识会保存得更久,并且我的同事也能看到。
重构带来的帮助不仅发生在将来——常常是立竿见影。我会先在一些小细节上使用重构来帮助理解,给一两个变量改名,让它们更清楚地表达意图,以方便理解,或是将一个长函数拆成几个小函数。当代码变得更清晰一些时,我就会看见之前看不见的设计问题。如果不做前面的重构,我可能永远都看不见这些设计问题,因为我不够聪明,无法在脑海中推演所有这些变化。Ralph Johnson说,这些初步的重构就像扫去窗上的尘埃,使我们得以看到窗外的风景。在研读代码时,重构会引领我获得更高层面的理解,如果只是阅读代码很难有此领悟。有些人以为这些重构只是毫无意义地把玩代码,他们没有意识到,缺少了这些细微的整理,他们就无法看到隐藏在一片混乱背后的机遇。
### 捡垃圾式重构
帮助理解的重构还有一个变体:我已经理解代码在做什么,但发现它做得不好,例如逻辑不必要地迂回复杂,或者两个函数几乎完全相同,可以用一个参数化的函数取而代之。这里有一个取舍:我不想从眼下正要完成的任务上跑题太多,但我也不想把垃圾留在原地,给将来的修改增加麻烦。如果我发现的垃圾很容易重构,我会马上重构它;如果重构需要花一些精力,我可能会拿一张便笺纸把它记下来,完成当下的任务再回来重构它。
当然,有时这样的垃圾需要好几个小时才能解决,而我又有更紧急的事要完成。不过即便如此,稍微花一点工夫做一点儿清理,通常都是值得的。正如野营者的老话所说:至少要让营地比你到达时更干净。如果每次经过这段代码时都把它变好一点点,积少成多,垃圾总会被处理干净。重构的妙处就在于,每个小步骤都不会破坏代码——所以,有时一块垃圾在好几个月之后才终于清理干净,但即便每次清理并不完整,代码也不会被破坏。
### 有计划的重构和见机行事的重构
上面的例子——预备性重构、帮助理解的重构、捡垃圾式重构——都是见机行事的:我并不专门安排一段时间来重构,而是在添加功能或修复bug的同时顺便重构。这是我自然的编程流的一部分。不管是要添加功能还是修复bug,重构对我当下的任务有帮助,而且让我未来的工作更轻松。这是一件很重要而又常被误解的事:重构不是与编程割裂的行为。你不会专门安排时间重构,正如你不会专门安排时间写`if`语句。我的项目计划上没有专门留给重构的时间,绝大多数重构都在我做其他事的过程中自然发生。
> ![](https://box.kancloud.cn/9cf522e33e311401bf0d755d003df8ea_19x20.jpeg) 肮脏的代码必须重构,但漂亮的代码也需要很多重构。
还有一种常见的误解认为,重构就是人们弥补过去的错误或者清理肮脏的代码。当然,如果遇上了肮脏的代码,你必须重构,但漂亮的代码也需要很多重构。在写代码时,我会做出很多权衡取舍:参数化需要做到什么程度?函数之间的边界应该划在哪里?对于昨天的功能完全合理的权衡,在今天要添加新功能时可能就不再合理。好在,当我需要改变这些权衡以反映现实情况的变化时,整洁的代码重构起来会更容易。
> 每次要修改时,首先令修改很容易(警告:这件事有时会很难),然后再进行这次容易的修改。
>
> ——Kent Beck
长久以来,人们认为编写软件是一个累加的过程:要添加新功能,我们就应该增加新代码。但优秀的程序员知道,添加新功能最快的方法往往是先修改现有的代码,使新功能容易被加入。所以,软件永远不应该被视为“完成”。每当需要新能力时,软件就应该做出相应的改变。越是在已有代码中,这样的改变就越显重要。
不过,说了这么多,并不表示有计划的重构总是错的。如果团队过去忽视了重构,那么常常会需要专门花一些时间来优化代码库,以便更容易添加新功能。在重构上花一个星期的时间,会在未来几个月里发挥价值。有时,即便团队做了日常的重构,还是会有问题在某个区域逐渐累积长大,最终需要专门花些时间来解决。但这种有计划的重构应该很少,大部分重构应该是不起眼的、见机行事的。
我听过的一条建议是:将重构与添加新功能在版本控制的提交中分开。这样做的一大好处是可以各自独立地审阅和批准这些提交。但我并不认同这种做法。重构常常与新添功能紧密交织,不值得花工夫把它们分开。并且这样做也使重构脱离了上下文,使人看不出这些“重构提交”的价值。每个团队应该尝试并找出适合自己的工作方式,只是要记住:分离重构提交并不是毋庸置疑的原则,只有当你真的感到有益时,才值得这样做。
### 长期重构
大多数重构可以在几分钟——最多几小时——内完成。但有一些大型的重构可能要花上几个星期,例如要替换一个正在使用的库,或者将整块代码抽取到一个组件中并共享给另一支团队使用,再或者要处理一大堆混乱的依赖关系,等等。
即便在这样的情况下,我仍然不愿让一支团队专门做重构。可以让整个团队达成共识,在未来几周时间里逐步解决这个问题,这经常是一个有效的策略。每当有人靠近“重构区”的代码,就把它朝想要改进的方向推动一点。这个策略的好处在于,重构不会破坏代码——每次小改动之后,整个系统仍然照常工作。例如,如果想替换掉一个正在使用的库,可以先引入一层新的抽象,使其兼容新旧两个库的接口。一旦调用方已经完全改为使用这层抽象,替换下面的库就会容易得多。(这个策略叫作Branch By Abstraction\[mf-bba\]。)
### 复审代码时重构
一些公司会做常规的代码复审(code review),因为这种活动可以改善开发状况。代码复审有助于在开发团队中传播知识,也有助于让较有经验的开发者把知识传递给比较欠缺经验的人,并帮助更多人理解大型软件系统中的更多部分。代码复审对于编写清晰代码也很重要。我的代码也许对我自己来说很清晰,对他人则不然。这是无法避免的,因为要让开发者设身处地为那些不熟悉自己所作所为的人着想,实在太困难了。代码复审也让更多人有机会提出有用的建议,毕竟我在一个星期之内能够想出的好点子很有限。如果能得到别人的帮助,我的生活会滋润得多,所以我总是期待更多复审。
我发现,重构可以帮助我复审别人的代码。开始重构前我可以先阅读代码,得到一定程度的理解,并提出一些建议。一旦想到一些点子,我就会考虑是否可以通过重构立即轻松地实现它们。如果可以,我就会动手。这样做了几次以后,我可以更清楚地看到,当我的建议被实施以后,代码会是什么样。我不必想象代码应该是什么样,我可以真实看见。于是我可以获得更高层次的认识。如果不进行重构,我永远无法得到这样的认识。
重构还可以帮助代码复审工作得到更具体的结果。不仅获得建议,而且其中许多建议能够立刻实现。最终你将从实践中得到比以往多得多的成就感。
至于如何在代码复审的过程中加入重构,这要取决于复审的形式。在常见的pull request模式下,复审者独自浏览代码,代码的作者不在旁边,此时进行重构效果并不好。如果代码的原作者在旁边会好很多,因为作者能提供关于代码的上下文信息,并且充分认同复审者进行修改的意图。对我个人而言,与原作者肩并肩坐在一起,一边浏览代码一边重构,体验是最佳的。这种工作方式很自然地导向结对编程:在编程的过程中持续不断地进行代码复审。
### 怎么对经理说
“该怎么跟经理说重构的事?”这是我最常被问到的一个问题。毋庸讳言,我见过一些场合,“重构”被视为一个脏词——经理(和客户)认为重构要么是在弥补过去犯下的错误,要么是不增加价值的无用功。如果团队又计划了几周时间专门做重构,情况就更糟糕了——如果他们做的其实还不是重构,而是不加小心的结构调整,然后又对代码库造成了破坏,那可就真是糟透了。
如果这位经理懂技术,能理解“设计耐久性假说”,那么向他说明重构的意义应该不会很困难。这样的经理应该会鼓励日常的重构,并主动寻找团队日常重构做得不够的征兆。虽然“团队做了太多重构”的情况确实也发生过,但比起做得不够的情况要罕见得多了。
当然,很多经理和客户不具备这样的技术意识,他们不理解代码库的健康对生产率的影响。这种情况下我会给团队一个较有争议的建议:不要告诉经理!
这是在搞破坏吗?我不这样想。软件开发者都是专业人士。我们的工作就是尽可能快速创造出高效软件。我的经验告诉我,对于快速创造软件,重构可带来巨大帮助。如果需要添加新功能,而原本设计却又使我无法方便地修改,我发现先重构再添加新功能会更快些。如果要修补错误,就得先理解软件的工作方式,而我发现重构是理解软件的最快方式。受进度驱动的经理要我尽可能快速完成任务,至于怎么完成,那就是我的事了。我领这份工资,是因为我擅长快速实现新功能;我认为最快的方式就是重构,所以我就重构。
### 何时不应该重构
听起来好像我一直在提倡重构,但确实有一些不值得重构的情况。
如果我看见一块凌乱的代码,但并不需要修改它,那么我就不需要重构它。如果丑陋的代码能被隐藏在一个API之下,我就可以容忍它继续保持丑陋。只有当我需要理解其工作原理时,对其进行重构才有价值。
另一种情况是,如果重写比重构还容易,就别重构了。这是个困难的决定。如果不花一点儿时间尝试,往往很难真实了解重构一块代码的难度。决定到底应该重构还是重写,需要良好的判断力与丰富的经验,我无法给出一条简单的建议。
- 第1章 重构,第一个示例
- 1.1 起点
- 1.2 对此起始程序的评价
- 1.3 重构的第一步
- 1.4 分解statement函数
- 1.5 进展:大量嵌套函数
- 1.6 拆分计算阶段与格式化阶段
- 1.7 进展:分离到两个文件(和两个阶段)
- 1.8 按类型重组计算过程
- 1.9 进展:使用多态计算器来提供数据
- 1.10 结语
- 第2章 重构的原则
- 2.1 何谓重构
- 2.2 两顶帽子
- 2.3 为何重构
- 2.4 何时重构
- 2.5 重构的挑战
- 2.6 重构、架构和YAGNI
- 2.7 重构与软件开发过程
- 2.8 重构与性能
- 2.9 重构起源何处
- 2.10 自动化重构
- 2.11 延展阅读
- 第3章 代码的坏味道
- 3.1 神秘命名(Mysterious Name)
- 3.2 重复代码(Duplicated Code)
- 3.3 过长函数(Long Function)
- 3.4 过长参数列表(Long Parameter List)
- 3.5 全局数据(Global Data)
- 3.6 可变数据(Mutable Data)
- 3.7 发散式变化(Divergent Change)
- 3.8 霰弹式修改(Shotgun Surgery)
- 3.9 依恋情结(Feature Envy)
- 3.10 数据泥团(Data Clumps)
- 3.11 基本类型偏执(Primitive Obsession)
- 3.12 重复的switch (Repeated Switches)
- 3.13 循环语句(Loops)
- 3.14 冗赘的元素(Lazy Element)
- 3.15 夸夸其谈通用性(Speculative Generality)
- 3.16 临时字段(Temporary Field)
- 3.17 过长的消息链(Message Chains)
- 3.18 中间人(Middle Man)
- 3.19 内幕交易(Insider Trading)
- 3.20 过大的类(Large Class)
- 3.21 异曲同工的类(Alternative Classes with Different Interfaces)
- 3.22 纯数据类(Data Class)
- 3.23 被拒绝的遗赠(Refused Bequest)
- 3.24 注释(Comments)
- 第4章 构筑测试体系
- 4.1 自测试代码的价值
- 4.2 待测试的示例代码
- 4.3 第一个测试
- 4.4 再添加一个测试
- 4.5 修改测试夹具
- 4.6 探测边界条件
- 4.7 测试远不止如此
- 第5章 介绍重构名录
- 5.1 重构的记录格式
- 5.2 挑选重构的依据
- 第6章 第一组重构
- 6.1 提炼函数(Extract Function)
- 6.2 内联函数(Inline Function)
- 6.3 提炼变量(Extract Variable)
- 6.4 内联变量(Inline Variable)
- 6.5 改变函数声明(Change Function Declaration)
- 6.6 封装变量(Encapsulate Variable)
- 6.7 变量改名(Rename Variable)
- 6.8 引入参数对象(Introduce Parameter Object)
- 6.9 函数组合成类(Combine Functions into Class)
- 6.10 函数组合成变换(Combine Functions into Transform)
- 6.11 拆分阶段(Split Phase)
- 第7章 封装
- 7.1 封装记录(Encapsulate Record)
- 7.2 封装集合(Encapsulate Collection)
- 7.3 以对象取代基本类型(Replace Primitive with Object)
- 7.4 以查询取代临时变量(Replace Temp with Query)
- 7.5 提炼类(Extract Class)
- 7.6 内联类(Inline Class)
- 7.7 隐藏委托关系(Hide Delegate)
- 7.8 移除中间人(Remove Middle Man)
- 7.9 替换算法(Substitute Algorithm)
- 第8章 搬移特性
- 8.1 搬移函数(Move Function)
- 8.2 搬移字段(Move Field)
- 8.3 搬移语句到函数(Move Statements into Function)
- 8.4 搬移语句到调用者(Move Statements to Callers)
- 8.5 以函数调用取代内联代码(Replace Inline Code with Function Call)
- 8.6 移动语句(Slide Statements)
- 8.7 拆分循环(Split Loop)
- 8.8 以管道取代循环(Replace Loop with Pipeline)
- 8.9 移除死代码(Remove Dead Code)
- 第9章 重新组织数据
- 9.1 拆分变量(Split Variable)
- 9.2 字段改名(Rename Field)
- 9.3 以查询取代派生变量(Replace Derived Variable with Query)
- 9.4 将引用对象改为值对象(Change Reference to Value)
- 9.5 将值对象改为引用对象(Change Value to Reference)
- 第10章 简化条件逻辑
- 10.1 分解条件表达式(Decompose Conditional)
- 10.2 合并条件表达式(Consolidate Conditional Expression)
- 10.3 以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)
- 10.4 以多态取代条件表达式(Replace Conditional with Polymorphism)
- 10.5 引入特例(Introduce Special Case)
- 10.6 引入断言(Introduce Assertion)
- 第11章 重构API
- 11.1 将查询函数和修改函数分离(Separate Query from Modifier)
- 11.2 函数参数化(Parameterize Function)
- 11.3 移除标记参数(Remove Flag Argument)
- 11.4 保持对象完整(Preserve Whole Object)
- 11.5 以查询取代参数(Replace Parameter with Query)
- 11.6 以参数取代查询(Replace Query with Parameter)
- 11.7 移除设值函数(Remove Setting Method)
- 11.8 以工厂函数取代构造函数(Replace Constructor with Factory Function)
- 11.9 以命令取代函数(Replace Function with Command)
- 11.10 以函数取代命令(Replace Command with Function)
- 第12章 处理继承关系
- 12.1 函数上移(Pull Up Method)
- 12.2 字段上移(Pull Up Field)
- 12.3 构造函数本体上移(Pull Up Constructor Body)
- 12.4 函数下移(Push Down Method)
- 12.5 字段下移(Push Down Field)
- 12.6 以子类取代类型码(Replace Type Code with Subclasses)
- 12.7 移除子类(Remove Subclass)
- 12.8 提炼超类(Extract Superclass)
- 12.9 折叠继承体系(Collapse Hierarchy)
- 12.10 以委托取代子类(Replace Subclass with Delegate)
- 12.11 以委托取代超类(Replace Superclass with Delegate)
- 参考文献
- 重构列表
- 坏味道与重构手法速查表