虽然refactoring几乎可以随时进行,然而,按照我们关于两顶帽子的原则,在某些场合下 ,refactoring的介入显得更加实际、有意义、富有成效。
另外,在最后进入Refactoring实践之前,我把Kent Beck和Martin Fowler给我们的忠告和建议放在这里。这些内容,特别是Code Smell和命名规则不但对我们进行refactoring具有很强的实践意义。同时,他们也促使我们对很多OO设计和编码的原则进行更多的思考。
Refactoring 应用的场合
增加功能
如果你的代码不需要增加新的功能,那么你几乎没有必要对他进行Refactoring。增加功能是进行Refactoring最常见的起因。通常,refactoring的第一个目标就是为了让代码更容易理解。Refactoring不仅仅可以使难以理解的代码更加清晰,他也可以使你对它的理解呈现一种交互状态,你和代码的交互。理解了,新的功能就很容易加入。
另一个原因是原来代码的结构很难直接扩展,可能是因为一个类和另外一个类的绑定太紧,或者是他使用了大量的case语句,或者是有一些重复的代码让你觉得加入你的代码会使结构更差。总而言之,直接加入新功能不是太方便。这时候,你就需要Refactoring,使得旧的代码在能让旧的test case通过的情况下,让新的代码加入更加方便、快捷。
重用
重用可以说是增加功能的一个特例。因为这是OO增加功能最主要的方法。但这里有一些差别。
如果你的代码是一个框架,那么当把代码交给其他人使用时,你必须重点考虑框架的重用方法。譬如,你想在框架之上增加一层facade,这时候,你可能需要refactoring一些内部类,让这个Facade的结构更容易加入。
对框架Refactoring可能需要是一个反馈的过程。因为框架一旦稳定就可能长期存在,一般你不会直接对这个框架增加功能。这时,不同应用程序对框架使用经验的积累可能使得你的Refactoring具有更加明确的目标,更加深层次的抽象。
修正bug
bug产生的原因有很多。可能是你对解决的问题理解不够,也可能是程序的代码过于纷乱。当你的代码结构不是很清晰时,bug往往隐藏在代码之中,使你很难发现。应用Refactoring让代码结构结构更清晰的同时,也使得bug自动浮出水面。
Refactoring所追求的目标和过程往往使得代码的Bug无处遁形。Eric E. Allen在IBM中developerWorks的bug pattern中的一个模式就是copy-and-paste方法造成代码重复,从而产生bug的一个例子。这个 Rogue Title Pattern是一个非常常见的bug例子,当你修正了程序中一个bug时,你发现程序运行的结果是你明明已经修正的bug还在作怪。究其原因,就是程序员在实现一个功能的时候把一段代码复制过去,稍加修改,就成为一个新的函数。你修改了一个地方,但却忘了另外一个地方。
Refactoring力求排除重复代码,如果有这样的bug,在refactoring以后,一次修改就能纠正所有的问题。
Code Review
Review可以使得专家知识传播到整个开发队伍。他们能够帮助更多的人理解一个大系统更多方面的问题。它们对于编写清晰的代码也有很好的帮助。俗话说得好,三个臭皮匠顶一个诸葛亮,Review也使得你能够接受更多其他人的好意见和想法,从而使你的设计和代码更加合理。
按照Martin Fowler的建议,我使用refactoring作为我的主要review方法。我有比较多的机会做这样的Code Review,在程序员发现一个难以解决的bug,或者当他们觉得自己的思路无法进行下去的时候,他们通常会要求我的帮助。
我的方法通常是问几个简单的问题,然后直接找到他们的代码,进行refactoring,在这个过程中,我总是一边refactoring,一边和程序员讲解我这样做的意图。令人惊奇的是,一旦我这样去做,程序员往往能够在我refactoring的过程中自己发现问题。由于Refactoring的结果非常具体,我很容易对程序员讲述一些本来就可以直接这样实现的例子,这对提高程序员的编程能力有极大的帮助。
味道
Kent Beck的" Code Smell"或许是OO社团最有人情味的名言了。Martin Fowler和Kent Beck专门合著了《Refactoring》其中的一章就叫《Bad Smells in Code》。
虽然对Refactoring的研究涉及到了形式化地探测代码的结构,譬如专门的重复代码监测工具。但这些研究尚处于幼年期。更何况,很多代码的结构并不是能够用简单的数字统计和抽象语法树所能解决的。
Code Smell是高水平的程序员对代码的一种感觉,当你能够闻到这样的味道时,你就可以在不涉及到程序所要解决的具体问题时,就"闻到"代码结构的好坏。
Kent Beck 和Martin Fowler列出的代码味道有:
Duplicated Code
代码重复几乎是最常见的异味了。他也是Refactoring的主要目标之一。代码重复往往来自于copy-and-paste的编程风格。与他相对应OAOO是一个好系统的重要标志(请参见我的duplicated code一文)。
Long method
它是传统结构化的"遗毒"。一个方法应当具有自我独立的意图,不要把几个意图放在一起,我的《大类和长方法》一文中有详细描述。
Large Class
大类就是你把太多的责任交给了一个类。这里的规则是One Class One Responsibility。参见我的《大类和长方法》。
Divergent Change
一个类里面的内容变化率不同。某些状态一个小时变一次,某些则几个月一年才变一次;某些状态因为这方面的原因发生变化,而另一些则因为其他方面的原因变一次。面向对象的抽象就是把相对不变的和相对变化相隔离。把问题变化的一方面和另一方面相隔离。这使得这些相对不变的可以重用。问题变化的每个方面都可以单独重用。这种相异变化的共存使得重用非常困难。
Shotgun Surgery
这正好和上面相反。对系统一个地方的改变涉及到其他许多地方的相关改变。这些变化率和变化内容相似的状态和行为通常应当放在同一个类中。
Feature Envy
对象的目的就是封装状态以及与这些状态紧密相关的行为。如果一个类的方法频繁用get方法存取其他类的状态进行计算,那么你要考虑把行为移到涉及状态数目最多的那个类。
Data Clumps
某些数据通常像孩子一样成群玩耍:一起出现在很多类的成员变量中,一起出现在许多方法的参数中… ,这些数据或许应该自己独立形成对象。
Primitive Obsession
面向对象的新手通常习惯使用几个原始类型的数据来表示一个概念。譬如对于范围,他们会使用两个数字。对于Money,他们会用一个浮点数来表示。因为你没有使用对象来表达问题中存在的概念,这使得代码变的难以理解,解决问题的难度大大增加。好的习惯是扩充语言所能提供原始类型,用小对象来表示范围、金额、转化率、邮政编码等等。
Switch Statement
基于常量的开关语句是OO的大敌,你应当把他变为子类、state或
trategy。
Parallel Inheritance Hierarchies
并行的继承层次是shotgun surgery的特殊情况。因为当你改变一个层次中的某一个类时,你必须同时改变另外一个层次的并行子类。
Lazy Class
一个干活不多的类。类的维护需要额外的开销,如果一个类承担了太少的责任,应当消除它。
Speculative Generality
一个类实现了从未用到的功能和通用性。通常这样的类或方法唯一的用户是test case。不要犹豫,删除它。
Temporary Field
一个对象的属性可能只在某些情况下才有意义。这样的代码将难以理解。专门建立一个对象来持有这样的孤儿属性,把只和他相关的行为移到该类。最常见的是一个特定的算法需要某些只有该算法才有用的变量。
Message Chain
消息链发生于当一个客户向一个对象要求另一个对象,然后客户又向这另一对象要求另一个对象,再向这另一个对象要求另一个对象,如此如此。这时,你需要隐藏分派。
Middle Man
对象的基本特性之一就是封装,而你经常会通过分派去实现封装。但是这一步不能走得太远,如果你发现一个类接口的一大半方法都在做分派,你可能需要移去这个中间人。
Inappropriate Intimacy
某些类相互之间太亲密,它们花费了太多的时间去砖研别人的私有部分。对人类而言,我们也许不应该太假正经,但我们应当让自己的类严格遵守禁欲主义。
Alternative Classes with Different Interfaces
做相同事情的方法有不同的函数signature,一致把它们往类层次上移,直至协议一致。
Incomplete Library Class
要建立一个好的类库非常困难。我们大量的程序工作都基于类库实现。然而,如此广泛而又相异的目标对库构建者提出了苛刻的要求。库构建者也不是万能的。有时候我们会发现库类无法实现我们需要的功能。而直接对库类的修改有非常困难。这时候就需要用各种手段进行Refactoring。
Data Class
对象包括状态和行为。如果一个类只有状态没有行为,那么肯定有什么地方出问题了。
Refused Bequest
超类传下来很多行为和状态,而子类只是用了其中的很小一部分。这通常意味着你的类层次有问题。
Comments
经常觉得要写很多注释表示你的代码难以理解。如果这种感觉太多,表示你需要Refactoring。
Refactoring和命名
Refactoring使代码更容易理解的最有效和常用的手段之一就是给类、方法和属性一个恰如其分的名字。很多公司和个人都编出了大量的代码命名规范。如果这些规范太繁琐,那就无法执行。有好的命名规则是好事,但并不总是如此。最重要的是,在整个