重构(refactoring)是现代软件工程的核心理念之一。它更是一种普适的方法论,特别是对于惯于宏大叙事但往往流于空泛的中国式思维,是一个非常有益且行之有效的互补。马丁·福勒(Martin Fowler)是现代软件工程的大师之一。
在过去十年中,马丁·福勒在商业化信息系统开发领域倡导了许多新的软件开发技术。他在许多领域的工作都为世人所瞩目,包括:面向对象的分析与设计,软件模式,统一建模语言,敏捷软件开发(特别是极限编程),以及重构。著名的出版社艾迪森维斯理(AddisonWesley)曾出版过若干本他的著作,如《分析模式》(1996年10月),《重构》(1999年6月,与肯特·贝克合著),《UML精粹》(1999年8月,与肯德尔·斯考特合著),《规划极限编程》(2000年10月,与肯特·贝克合著),以及即将出版的《企业级应用架构模式》(2002年11月)。
这次采访分为六个部分。福勒谈到了他对许多话题的看法,包括重构,设计,测试,和极限编程。在第一部分,福勒探讨了重构和测试在开发过程中扮演的角色,并描述了重构、设计、以及可靠性之间的相互作用。
比尔:请给出重构的定义。
马丁:重构就是对代码本身做出修改,以改善它的内部结构,但又不改变它的外部表现。
比尔:如果重构既不添加新的功能也不消除已有的漏洞,那它的商业目的是什么?你是怎么看待重构的?
马丁:重构改善了设计。而一个良好的设计,其商业目的何在?我认为,它使你能在未来更容易地对软件作出改动。
重构实际上是在说,“来吧,让我们把系统结构重新调整一下,好让将来的任何改动都更容易些。”其潜台词是,如果你不会改动你的系统,那么也就没有必要做重构,因为不会有任何回报。但如果你将要对你的系统作出改动——不管是消除漏洞也好,还是添加新功能也好——那么,一个好的或更好的系统结构,会使你在做修改时有所受益。
重构与团队
比尔:一个团队中,各人有各人的编程习惯。我的一个朋友在一家公司做事,公司进了个新人,那个人对于什么是好的命名规则有自己的一套想法:他喜欢在成员名字的前面加上“m_”前缀、在词与词之间加上下划线,等等。于是他做了一些简单的重构,就像你推荐的那样,但这些重构造成了一些问题。
比如,由于授权许可的限制,公司的一些代码是保密的,这个新人无权访问。结果他的重构造成了代码不一致,我的朋友不得不修补这个问题。而当我的朋友需要修补在多个版本中都存在的漏洞时,问题又来了:重构导致了在不同的版本中命名规则是不一样的,这无疑给不同版本的漏洞修补工作增添了难度。此外,公司里像我朋友那样的旧员工,已经习惯了旧的命名规则。有一次,我的朋友怎么也找不到一个方法函数,因为它的名字被改了。诸如此类的问题,都是因为那个新人所做的重构。
那么,在一个团队里,应该由谁来决定命名规则?又应该由谁来决定何时以及如何做重构?
马丁:重构并非让你变态地去重命名一大堆东西,而理由仅仅是你觉得那样好。重构必须要有收益。假设你在做重命名,那么你应该查找那些名字和意义不符的方法,并且其他使用该方法的人也觉得应该改名才行。说到命名规则,一个团队必须要有一个都能接受的命名规则。如果你是新加入的,那你必须了解这套命名规则。假设我作为一个咨询师进入团队,那我不会把我的命名规则强加给团队。我会询问团队的命名规则是什么,熟悉它们,使用它们。当然,从另一方面说,我坚决反对像“x374”这样的命名,因为它不能表达任何意义。
至于说保密的代码与其他部分代码不一致的问题,这正说明了他们的测试不是很严格。测试对于重构来说,是非常重要的支撑。
重构与测试
比尔:你在《重构》这本书里写道:“如果你打算重构,那么最基本的前提是有完善的测试。”这是不是说,如果没有测试,就不要重构?
马丁:没有测试支撑的重构,就如同不系安全带走钢丝。如果你很擅长走钢丝,而且钢丝又不是悬得很高的话,那不妨试试。但如果你从没走过钢丝,而钢丝又是悬在尼亚加拉瀑布上空,那你最好还是有个保靠的安全带。
比尔:如果你目前没有任何测试,那么还有必要追加测试么?
马丁:还是那句话,代码的一致性可能会因为某人的修改而被破坏,你绝对不希望会有这样的“惊喜”。而类似JUnit的测试的最大好处就是让你能通过运行它们看看是否有什么东西被破坏了。如果你不打算碰你的代码,那当然平安无事;但只要你加新的功能或是修补漏洞,那么你就有可能破坏某些东西。你的测试越完善,你对能做的改动就越有信心。最终,你能实现比较高的可靠度。
以测试为基础的可靠性是极限编程中不大被人们注意的要点之一。人们在谈论极限编程的时候,谈得最多的是它的快速反应力以及轻量化的开发过程,但往往不提可靠性。而我听到越来越多的故事,都表明极限编程有非常高的可靠性。两周前,我跟以前在 C3 项目的老同事里奇(RichGarzaniti)聊了聊。克莱斯勒的 C3项目通常被认为是极限编程的摇篮。在那里,肯特第一次把各种实践有机地结合在一起。里奇谈到了他借助极限编程以及测试和重构所开发的项目。整个系统彻底贯彻了极限编程的精要。今年到目前为止,他只发现了一处漏洞。
测试与效率
比尔:在《重构》这本书中,你写道:“作为一个程序员,我写测试是为了提高了自己的工作效率。”那么,测试是否能提高鲁棒性、质量和可靠性呢?
马丁:会的,这些都是测试带来的好处。我认为,程序中的缺陷影响了我们的效率,因为我们不得不花时间去修正它们。如果我能减少一个缺陷,我的效率就得到了提高。至于说我得到了更鲁棒、更可靠的软件,这都是很有价值的副产品。最基本的一点,还是在于我能节省下跟踪和修补漏洞的时间,从而编写出更多的功能。
比尔:也就是说,你花在写测试上的时间,可以因为不用修补漏洞而补回来。
马丁:我能在一天之内就把时间补回来,因为花在跟踪调试上的时间大大减少了。花在测试上的成本很快就能收回。随之而来的还有其它好处。
比尔:假如一段代码根本没有测试,那么写测试的成本还能很快收回么?
马丁:这种情况需要较长的时间。我不建议你花上整整两个月的时间,就是用来写测试。但我认为,通过添加一些测试,你能很快获得回报,因为你开始发现问题了。如果你把测试集中在你需要做改动的代码部分,那么当你犯错误的时候,测试会告知你这些错误。显然,全面综合的测试会使你受益最大;但是,就算只写几个测试,你也会从中受益。
重构与效率
比尔:你说过单元测试可以帮助你提高效率,你还说过重构的好处之一就是能够更快地编程。那么,重构是如何帮助你提高编程速度的呢?
马丁:因为一个设计良好的程序,修改起来会更容易。程序的设计越好,修改起来就越容易,从而提高了效率。
重构与性能优化
比尔:你在书里写道,你之所以做重构,并不是出于好玩,而是因为“有些与程序有关的事情,如果不做重构,就无法做到;只有做了重构,才能办到。”那么,你所指的这些事情,除了对程序进行改动,还有哪些事情呢?
马丁:对程序进行改动是主要的动因。我们不得不经常对软件作出一些改动——只要这个软件还在使用。重构是用来改善设计的。我们需要一个好的设计以使任何改动都更容易些。重构跟性能优化有些类似,都是在行为不变性(behavior-preserving)前提下的改进。不过,性能优化的步骤有异于重构,整个过程也有所不同,因为性能优化的驱动要素是性能分析(profiling)。
比尔:也就是说,重构所作的改动,不会增加或改变功能,而是为了程序更清晰,这样,将来就可以容易地做出改动。而性能调优的相似之处在于,其所作的改动除了缩短执行时间以外,也不会改变程序的功能。
马丁:对。它们有相似之处。不过,我一般还是把它们看成是两个很不一样的概念。
重构与设计
比尔:您在《重构》一书中写道:“重构可以帮助我们改进软件的设计。”那么,重构是如何改进设计的呢?
马丁:我无法笼统地回答这个问题。不过你可以分析单个的重构方法,来看看它是如何改进设计的。比如,提取方法(Extract Method)通过把一个很长的、令人费解的方法拆分成一些小方法来改进设计。改进后的方法读起来就像是一份文档——一张调用那些小方法的列表。
每种重构方法都会对针对某些特定的设计元素做出改进。应用的时候要具体情况具体分析。很多重构方法都能找到相对立的另一个重构方法。比如,如果一个方法,除了方法本身的代码所表达的意义之外,没有任何附加的含义,那么你可能会内联它。内联方法(InlineMethod)与提取方法就是对立的。很多时候,到底应用哪种方法取决于具体情况。
重构与查错
比尔:你在《重构》一书中还声称,重构可以帮助你发现漏洞。重构是如何做到这一点的?
马丁:我想,重构可以从几方面帮助你发现漏洞。有时候,当你让程序变得易懂时,漏洞自然而然地就显现在你面前。你一边做着重构,一边对自己说,“等等,假如给定某种条件,结果会是什么呢?”我就是这样发现了很多漏洞,还有很多其他人也都如此。重构使得事情一瞬间清晰起来,因而你能够一眼就看出漏洞所在。
当你在修补一个漏洞而代码又晦涩难懂时,重构也能够帮你发现问题所在。通常,当你在调试一段代码的时候,对之进行重构也相对容易些。重构后,你就可以轻易地发现漏洞在哪里。这种清理使得漏洞更容易被暴露出来。
针对包含漏洞的代码段编写单元测试,是一种很好的调试技术。其带来的好处,不仅仅是让你对代码的理解更深刻,还让你建立起测试库,从而意味着将来不会有问题发生。
重构与重写
比尔:在《重构》一书中,你列举了重构可能会遇到的几个问题,其中有一个是说,在某些情况下不应该做重构。那么,如何判断什么时候应该彻底抛弃现有代码从头开始,而不是选择重构?
马丁:答案是——我也不知道。如果你有一堆乱七八糟的代码且又没有测试,那么你最好是扔掉它们从头开始,否则你就得重新做所有的测试。反之,如果你有一堆乱七八糟的代码同时还有很多测试的话,情况就不一样了。假如代码中满是漏洞,那么在行为不变性下,不管怎么变换,那些漏洞都会被保留下来。这时,是否重构就是一个值得争论的问题。我想,这个问题的答案也会随着你对重构熟悉程度的深入而改变。随着对重构越来越有信心,你可能会对以前想要重写的一些东西改用重构,因为你有更强的重构能力了。
比尔:我曾经有过一次经历,和另外一个咨询师一起努力使一个有问题的应用运转起来。我决定把我所负责的那段代码彻底丢弃,然后从头开始。而另一位咨询师试图重构他所负责的代码。但结果却是,他那部分代码最终也未能稳定运行。最后,我接手了他的工作,把原来的代码丢掉,从零开始重写了一遍。这次经历说明,在某些时刻,如果代码完全没有结构,那么重写是比重构更有效的一种方法。
马丁:在决定重写代码之前,也许值得花些时间在重构上,来看看能做多少改进。