refactoring Patterns:第一部分 | ![]() | ![]() | ||
![]() | ||||
![]() |
![]() |
石一楹 (shiyiying@hotmail.com) 这是关于refactoring思考的第一部分内容。本文将介绍refactoring的基本概念、定义,同时解释正确、安全进行refactoring需要坚持的几个原则。 介绍 这样的代码难以理解,更不要说对它加以修改。如果你关心系统体系结构、设计,或者是一个好程序,你的第一反应就是拒绝工作于这样的代码。你会说:"这么烂的代码,让我修改,还不如重写。"然而,你不大可能完全重写已经能够甚至是正在运作的系统,你不能保证新的系统能够实现全部的原有功能。更何况,你不是生活在真空,还有更多的投资、交付、竞争压力。 于是你使用一种quick-and-dirty的方法,如果系统有问题,那么就直接找到这个问题,便当地修改它。如果要增加一个新功能,你会从原来的系统中找到一块相近的代码,拷出来,作些修改。对于原来的系统,你想,既然我不能重头写过,而且它们已经在运作,让它去吧。然后,你增加的代码变成了下一个程序员咒骂的对象。系统越来越难以理解,维护越来越困难、越来越昂贵。系统变成了一个十足的大泥球。 这种情况是每一个人都不愿意碰到的,但是奇怪的是,这样的情景一次又一次出现在大多数人的编程生涯中。这是因为我们不知道该如何解决。 解决这个问题的最好办法当然是让它不要发生。然而,要阻止代码的腐化,你需要付出额外的代价。每次在修改或增加代码之前,你都要看一看手上的这些代码。如果它有很好的味道,那么你应该能够很方便地加入新的功能。如果你需要花很长的时间去理解原来的代码,花更长的时间去增加和修改代码。那么,先放下手里的活,让我们来做Refactoring。 什么是Refactoring? Martin Fowler[Fowler]把Refactoring定义为两部分,一部分为名词形式: 另一部分则是动词形式: 软件结构可以因为各种各样的原因而被改变,如进行打印美化、性能优化等等,但只有出于可理解性、可修改、可维护目的的改变才是Refactoring。这种改变必须保持可观察的行为,按照Martin的话来说,就是Refactoring之前软件实现什么功能,之后照样实现什么功能。任何用户,不管是终端用户还是其他的程序员,都不需要知道某些东西发生了变化。 Two Hats(两顶帽子) 在一个软件的开发过程中,你可能频繁地交换这两顶帽子。你开始增加一个新功能,这时你认识到,如果原来的代码结构更好一点,新功能就能够更方便地加入。因此,你脱下增加功能的帽子,换上refactoring的帽子。一会儿,代码结构变好了,你脱下refactoring的帽子,戴上增加功能的帽子。增加了新功能以后,你可能发现你的代码使得程序的结构难以理解,这时你又交换帽子。 关于两顶帽子交换的故事不断地发生在你的日常开发中,但是不管你带着哪一定帽子,一定要记住带一定帽子只做一件事情。 Unit Test 但是,要从理论上完全证明系统的可观察行为保持不变,虽然不是说不可能,也是十分困难的。工具也有自己的缺陷。首先,目前对于Refactoring的理论研究并非十分成熟,某些曾经被证明安全的Refactoring最近被发现在特定的场合下并不安全。其次,目前的工具不能很好地支持"非正式"的Refactoring操作,如果你发现一种新的Refactoring技巧,工具不能立即让这种refactoring为你所用。 自动化的测试是检验Refactoring安全性非常方便而且有效的方法。虽然我们不能穷尽整个系统中所有的测试,但如果在Refactoring之前成功的测试现在失败了,我们就会知道刚刚做的Refactoring破坏了系统的可观察行为。自动化测试能够在程序员不进行人工干预的情况下自动检测到这样的行为破坏。 自动化测试中最实用的工具是XUnit系列单元测试框架,该框架最初由Kent Beck和Eric Gamma为Smalltalk社团而开发。 Eric Gamma对测试的重要性曾经有过这样的话: 下面的片断来自Javaworld,两个Sun开发者展示了它们对单元测试的狂热以及展示了它们扩展单元测试来检查象EJB这样的分布式控件: 我曾经认为自己是很好的程序员。认为自己的代码几乎不可能出错。但事实上,我没有任何证据可以证明这一点,同样我也没有信心我的代码就一定不会出错,或者当我增加一项新功能时,原先的行为一定没有遭到破坏。另一方面,我认为太多的测试于事无补,测试只能停留在理论之上,或只有那些实力强劲的大公司才能做到。 这个观点在1999年我看到Kent Beck和Gamma的Junit测试框架之后被完全推翻了。JUnit是XP的重要工具之一。XP提倡一个规则叫做test-first design。采用Test First Design方法,你在编写一个新功能前先写一个单元测试,用它来测试实现新功能需要但可能会出错的代码。这意味着,测试首先是失败的,写代码的目的就是为了让这些测试能够成功运行。 JUnit的简单、易用和强大的功能几乎让我立刻接纳了单元测试的思想,不但因为它可以让我有证据表明我的代码是正确的,更重要的是在我每次对代码进行修改的同时,我有信心所有的变化都不会影响原有的功能。测试已经成为我所有代码的一部分。关于这一点,Kent Beck在它的《Extreme Programming Explained》中指出: 单元测试的基本过程如下:
在编写测试的时候,要注意对测试的内容加以考虑,并不是测试越多越好.Kent Beck说: 另一位作者Eric Gamma说: 你可能会认为单元测试虽然好,但是它会增加你的编程负担,而别人花钱是请你来写代码,而不是来写测试的。但是WILLAM WAKE说: 你还会认为单元测试可能增加你的维护量,因为如果代码发生了改变,相应的测试也需要做出改变。事实上,测试只会让你的维护更快,因为它们让你对你所做出的改变更有信心,如果你做错了一件事,测试同时也会提醒你。如果接口发生了改变,你当然需要改变你的接口,但这一点并非太难。 单元测试是程序的一部分,而不是独立的测试部门所应完成的任务。这就是所谓的自测试代码。程序员可能花费一些时间在编写代码,花费一些时间在理解别人的代码,花费一些时间在做设计,但他们最多的时间是在做调试。任何一个人都有这样一种遭遇,一个小小的问题可能花费你一个下午、一天,甚至是几天的时间来调试。要改正一个bug往往很简单,但是要找到这样的bug却是一个大问题。如果你的代码能够带有自动化的自测试,那么一旦你加入一个新的功能,旧的测试会告诉你那些原来的代码存在着bug,而新加入的测试则告诉哪些新加入的代码引入了bug。 Small step 如果你一次做了太多的修改,那么就有可能介入很多的bug,代码将难以调试。如果你发现修改并不正确,要想返回到原来的状态也十分困难。 这些细小的步骤包括:
要求使用小步骤渐进地Refactoring并不完全出于对实践易行的考虑。 Ralph Johnson在伊利诺斯州立大学领导的一个研究小组是Refactoring理论的引导者和最重要的理论研究团体。其中William Opdyke 1992年的博士论文《Refactoring Object-Oriented Framework》是公认的Refactoring第一位正式提出者。在那篇论文中,Opdyk描述他对refactoring重构层次的看法: 为了实现这样的intermediate level操作,Opdyke提出了原子atomic refactoring的概念,他指出: 论文中,Opdyke 首先证明在一定的前提之下,这些原子refactoring将不会改变程序的Observable behaviour。更高层的refactoring可以通过分解为这些原子的refactoring加以证明。Opdyke也证明了他所提出的高层refactoring如何在每一步原子atomic之后都符合后续原子atomic所需要的前提。 小步前进使得对每一步进行证明成为可能,最终通过组合这些证明,可以从更高层次上来证明这些refactoring的安全性和正确性。 Refactoring工具依赖于这些理论研究进行Refactoring。如果每个人能够按照这样的一小步一小步进行Refactoring,那么极有希望他的refactoring能够被正确地记录下来,为整个面向对象社团所用。同时,对他理论正确性地证明可以促使refactoring工具得到进一步的发展。 也许你会认为,随着工具的发展,程序员将变成Refactoring机器人。这样的看法是不正确的。 虽然使用一个refactoring工具能够避免介入使用手工方式可能产生的各种各样bug,减少编译、测试和code review。但是正如Smalltalk Refactory Browser的作者Don Roberts所说,Refactoring工具不打算用来代替程序员,程序员需要自己来决定什么地方需要refactoring,做什么样的refactoring。而在这一点上,经验是不可代替的。 Code Review和Pair Programming Code Review原先一般都在一些大公司实行,他们可能聘请专家对项目进行Code Review,以发现代码中存在的问题,改良系统的设计,提高程序员的水平。 同样在refactoring过程中,我们也可以使用Code Review的方法。问题是,我们是否有足够的精力和人员配备来进行这样的Review呢? XP成功经验表明,Code Review不应当是只有大公司才能做的。甚之,XP中的Pair Programming其实就是对Code Review的极端化,它也更加适合于表达Code review在refactoring过程中所能起到的作用。Kent Beck说: 在每一对中有两个角色。一个合作者,把持键盘和鼠标,正在考虑该处所实现方法的最佳途径,。另一个合作者,则更多考虑策略性方面的问题:
使用这种方法进行refactoring,可以在一个程序员没有想到一个应当有的单元测试时,当一个程序员无法找到合适的Refactoring方法或者当一个程序员没有按照正确的方法进行refactoring时,另外一个程序员可以提出自己的观点和建议。甚至在极端情况下,当拥有键盘的程序员对如何完成这个refactoring没有概念时,另外一个程序员可以接过键盘,直接往下做。 XPChina的notyy认为Code Review不应当属于refactoring的原则之一。严格来你可以在不实行Pair Programming或者Code Review的情况下进行refactoring.但是由于refactoring的特殊性,它不是增加新的代码,而是修改已经存在、很可能已经被其他许多模块依赖的代码,所以Pair Programming在这里比一般的新代码更重要。从另一个方面来讲,如果你正在做big refactory,如refactor to Design pattern,此时Pair Programming更有助于交流双方对于被修整代码将refactor成为何种设计模式的意见。 因此,尽管这不是一条必要的原则,我还是把它作为原则之一进行描述。 The Rule of Three 第一次做某件事,你直接做就是了。第二次你做某件事,看到重复,你有些退缩,但不管怎样,你重复就是了。第三次你做类似的事情,你refactor。
|