第二章 重构的原则

2.1 何谓重构

"重构"这个词既可以用作名词也可以用作动词。名词形式的定义是:

重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。这个定义适用于我在前面的例子中提到的那些有名字的重构,例如提炼函数(106)和以多态取代条件表达式(272)。

重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构

所以,我可能会花一两个小时进行重构(动词),其间我会使用几十个不同的重构(名词)。

重构的关键在于运用大量微小且保持软件行为的步骤,一步步达成大规模的修改。每个单独的重构要么很小,要么由若干小步骤组合而成。因此,在重构的过程中,我的代码很少进入不可工作的状态,即便重构没有完成,我也可以在任何时刻停下来。

如果有人说他们的代码在重构过程中有一两天时间不可用,基本上可以确定,他们在做的事不是重构。

在上述定义中,我用了"可观察行为"的说法。它的意思是,整体而言,经过重构之后的代码所做的事应该与重构之前大致一样。这个说法并非完全严格,并且我是故意保留这点儿空间的:重构之后的代码不一定与重构前行为完全一致。比如说,提炼函数(106)会改变函数调用栈,因此程序的性能就会有所改变;改变函数声明(124)和搬移函数(198)等重构经常会改变模块的接口。不过就用户应该关心的行为而言,不应该有任何改变。++如果我在重构过程中发现了任何++ ++bug++ ++,重构完成后同样的++ ++bug++ ++应该仍然存在++(不过,如果潜在的bug还没有被任何人发现,也可以当即把它改掉)。

重构与性能优化有很多相似之处:两者都需要修改代码,并且两者都不会改变程序的整体功能。两者的差别在于其目的:重构是为了让代码"更容易理解,更易于修改"。这可能使程序运行得更快,也可能使程序运行得更慢。在性能优化时,我只关心让程序运行得更快,最终得到的代码有可能更难理解和维护,对此我有心理准备。

2.2 两顶帽子

Kent Beck提出了"两顶帽子"的比喻,即:添加新功能和重构。

添加新功能时,我不应该修改既有代码,只管添加新功能。通过添加测试并让测试正常运行,我可以衡量自己的工作进度。重构时我就不能再添加功能,只管调整代码的结构。

软件开发过程中,我可能会发现自己经常变换帽子。首先我会尝试添加新功能,然后会意识到:如果把程序结构改一下,功能的添加会容易得多。于是我换一顶帽子,做一会儿重构工作。程序结构调整好后,我又换上原先的帽子,继续添加新功能。

2.3 为何重构

2.3.1 重构改进软件的设计

如果没有重构,程序的内部设计(或者叫架构)会逐渐腐败变质,于是代码逐渐失去了自己的结构。程序员越来越难通过阅读源码来理解原来的设计。代码结构的流失有累积效应。越难看出代码所代表的设计意图,就越难保护其设计,于是设计就腐败得越快。经常性的重构有助于代码维持自己该有的形态。

重构的一个重要方向是消除重复代码,代码量减少将使未来可能的程序修改动作容易得多。

2.3.2 重构使软件更容易理解

重构可以帮我让代码更易读。

2.3.3 重构帮助找到bug

对代码的理解,可以帮我找到bug。如果对代码进行重构,我就可以深入理解代码的所作所为,并立即把新的理解反映在代码当中。搞清楚程序结构的同时,我也验证了自己所做的一些假设,于是想不把bug揪出来都难。

重构能够帮助我更有效地写出健壮的代码。

重构提高编程速度

重构帮我更快速地开发程序。改善设计、提升可读性、减少bug,这些都能提高质量。但花在重构上的时间,难道不是在降低开发速度吗?否则,一开始他们进展很快,但如今想要添加一个新功能需要的时间就要长得多,bug修改也越来越慢。

两种团队的区别就在于软件的内部质量。需要添加新功能时,内部质量良好的软件让我可以很容易找到在哪里修改、如何修改。良好的模块划分使我只需要理解代码库的一小部分,就可以做出修改。如果代码很清晰,我引入bug的可能性就会变小,即使引入了bug,调试也会容易得多。

通过投入精力改善内部设计,我们增加了软件的耐久性,从而可以更长时间地保持开发的快速。

2.4 何时重构

Don Roberts给了我一条准则:第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是可以去做;第三次再做类似的事,你就应该重构。

正如老话说的:事不过三,三则重构。

2.4.1 预备性重构:让添加新功能更容易

重构的最佳时机就在添加新功能之前。在动手添加新功能之前。

如果对代码结构做一点微调,我的工作会容易得多。也许已经有个函数提供了我需要的大部分功能,但有几个字面量的值与我的需要略有冲突。如果不做重构,我可能会把整个函数复制过来,修改这几个值,但这就会导致重复代码------如果将来我需要做修改,就必须同时修改两处(更麻烦的是,我得先找到这两处)。如果把某些更新数据的逻辑与查询逻辑分开,会更容易避免造成错误的逻辑纠缠。

2.4.2 帮助理解的重构:使代码更易懂

需要先理解代码在做什么,然后才能着手修改。在一些小细节上使用重构来帮助理解,给一两个变量改名,让它们更清楚地表达意图,以方便理解,或是将一个长函数拆成几个小函数。

重构就像扫去窗上的尘埃,使我们得以看到窗外的风景。重构会引领我获得更高层面的理解,如果只是阅读代码很难有此领悟。

2.4.3 捡垃圾式重构

我已经理解代码在做什么,但发现它做得不好。例如,逻辑不必要地迂回复杂,或者两个函数几乎完全相同,可以用一个参数化的函数取而代之。

2.4.4 有计划的重构和见机行事的重构

预备性重构、帮助理解的重构、捡垃圾式重构------都是见机行事的。

肮脏的代码必须重构,但漂亮的代码也需要很多重构。重构就是人们弥补过去的错误或者清理肮脏的代码。当然,如果遇上了肮脏的代码,你必须重构,但漂亮的代码也需要很多重构。对于昨天的功能完全合理的权衡,在今天要添加新功能时可能就不再合理。

如果团队过去忽视了重构,那么常常会需要专门花一些时间来优化代码库,以便更容易添加新功能。有时,即便团队做了日常的重构,还是会有问题在某个区域逐渐累积长大,最终需要专门花些时间来解决。但这种有计划的重构应该很少,大部分重构应该是不起眼的、见机行事的。

2.2.5 长期重构

有一些大型的重构可能要花上几个星期,即便在这样的情况下,我仍然不愿让一支团队专门

做重构。可以让整个团队达成共识,在未来几周时间里逐步解决这个问题,这经常是一个有效的策略。每当有人靠近"重构区"的代码,就把它朝想要改进的方向推动一点。这个策略的好处在于,重构不会破坏代码------每次小改动之后,整个系统仍然照常工作。

Improvement task.

2.4.6 Code review是重构

代码复审有助于在开发团队中传播知识,也有助于让较有经验的开发者把知识传递给比较欠缺经验的人,并帮助更多人理解大型软件系统中的更多部分。

代码复审也让更多人有机会提出有用的建议,毕竟我在一个星期之内能够想出的好点子很有限。如果能得到别人的帮助,我的生活会滋润得多,所以我总是期待更多复审。如果代码的原作者在旁边会好很多,因为作者能提供关于代码的上下文信息,并且充分认同复审者进行修改的意图。

Review meeting, requirement_id in MR

2.4.7 怎么跟经理说?

软件开发者都是专业人士。我们的工作就是尽可能快速创造出高效软件。我的经验告诉我,对于快速创造软件,重构可带来巨大帮助。------能够带来经济效益。

2.4.8 何时不应该重构

一块凌乱的代码,但并不需要修改它,那么我就不需要重构它。例如,丑陋的代码能被隐藏在一

个API之下,我就可以容忍它继续保持丑陋。只有当我需要理解其工作原理时,对其进行重构才有价值。

另一种情况是,如果重写比重构还容易,就别重构了。

2.5 重构的挑战

2.5.1 延缓新功能开发

重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值。

重构很有必要进行,而马上要添加的功能非常小,这时我会更愿意先把新功能加上,然后再做这次大规模重构。在我们这个行业里,重构不足的情况远多于重构过度的情况。

2.5.2 代码所有权

很多重构手法不仅会影响一个模块内部,还会影响该模块与系统其他部分的关系。

2.5.3 分支

每个团队成员各自在代码库的一条分支上工作,进行相当大量的开发之后,才把各自的修改合并回主线分支。

2.5.4 测试

不会改变程序可观察的行为,这是重构的一个重要特征。如果没有自测试的代码,这种担忧就是完全合理的,这也是为什么我如此重视可靠的测试。

2.5.5 遗留代码

遗留代码往往很复杂,测试又不足,而且最关键的是,是别人写的(瑟瑟发抖)。重构可以很好地帮助我们理解遗留系统。

2.6 重构、架构和YAGNI

在任何人开始写代码之前,必须先完成软件的设计和架构。一旦代码写出来,架构就固定了,只会因为程序员的草率对待而逐渐腐败。

重构改变了这种观点。有了重构技术,即便是已经在生产环境中运行了多年的软件,我们也有能力大幅度修改其架构。

重构对架构最大的影响在于,通过重构,我们能得到一个设计良好的代码库,使其能够优雅地应对不断变化的需求。

如果以后再重构有多困难。只有当未来重构会很困难时,我才考虑现在就添加灵活性机

制。我发现这是一个很有用的决策方法。这种设计方法有很多名字:简单设计、增量式设计

或者YAGNI------"你不会需要它"(you arenʼt going to need it)的缩写。

采用YAGNI并不表示完全不用预先考虑架构。有一些时候,如果缺少预先的思考,重构会难以开展。但两者之间的平衡点已经发生了很大的改变:如今我更倾向于等一等,待到对问题理解更充分,再来着手解决。

2.7 重构与软件开发过程

重构起初是作为极限编程(XP)[mf-xp]的一部分被人们采用的。极限编程是最早的敏捷软件开发方法之一。

如果一支团队想要重构,那么每个团队成员都需要掌握重构技能,能在需要时开展重构,而不会干扰其他人的工作。这也是我鼓励持续集成的原因:有了CI,每个成员的重构都能快速分享给其他同事,不会发生这边在调用一个接口那边却已把这个接口删掉的情况;如果一次重构会影响别人的工作,我们很快就会知道。自测试的代码也是持续集成的关键环节,所以这三大实践------自测试代码、持续集成、重构。

2.8 重构与性能

为了让软件易于理解,我常会做出一些使程序运行变慢的修改。这是一个重要的问题。我并不赞成为了提高设计的纯洁性而忽视性能。虽然重构可能使软件运行更慢[也可能是软件运行的更快],但它也使软件的性能优化更容易。

关于性能,一件很有趣的事情是:如果你对大多数程序进行分析,就会发现它把大半时间都耗费在一小半代码身上。如果你一视同仁地优化所有代码,90%的优化工作都是白费劲的,因为被你优化的代码大多很少被执行。你花时间做优化是为了让程序运行更快,但如果因为缺乏对程序的清楚认识而花费时间,那些时间就都被浪费掉了。

在性能优化时,找出性能热点所在的一小段代码。然后我应该集中关注这些性能热点,并使用持续关注法中的优化手段来优化它们。由于把注意力都集中在热点上,较少的工作量便可显现较好的成果。

重构可以帮助我写出更快的软件。短期看来,重构的确可能使软件变慢,但它使优化阶段的软件性能调优更容易,最终还是会得到好的效果。

性能优化找出瓶颈所在代码优化;