不改变软件行为只是重构的基本要求。要想真正让重构发挥威力,就必须做到“不需了解软件行为”。那些最需要重构的代码,你只能看到其中的坏味道,接着选择对应的重构手法来消除这些坏味道,然后才有可能理解它的行为。而这整个过程之所以可行,全赖你在脑子里记录着一份坏味道与重构手法的对应表。
记住所有的坏味道,记住它们对应的重构手法,记住常见的重构步骤。
代码被阅读和修改的次数远远多于它被编写的次数。保持代码易读,易修改的关键 ,就是重构。但重构是具有风险的。本书收录的重构手法,保证每次只走一步。需要做好单元测试。
重构是这样一个过程,在不改变代码外在行为的前提下,对代码做出修改,以改进代码的内部结构。重构就是在代码写好之后改进它的设计。
1. 重构,第一个案例
如果发现需要为程序添加一个特性,而代码结构使你无法方便的达到目的,那就先重构那个程序。
重构的第一步为即将修改的代码建立一组可靠的测试环境。
Extract Method、更改变量名称、move method将方法移到适合的类中、replace tmp with query去除临时变量。
根据方法使用的变量,或者系统可能的变化选择方法属于的对象。
State和strategy的区别。
2. 重构原则
重构的目的是使软件更容易被理解和修改。重构不会改变软件可观察的行为。添加新功能和重构应分开进行。
重构改进设计,重构使软件更容易理解,重构帮助找到bug,重构提高变成速度。
重构应该随时随地进行。添加新功能时重构,修补错误时重构,复审代码时重构。
技术复审是减少错误、提高开发速度的一条重要途径。
如果某个函数的所有调用者都在你的控制之下,修改函数名称也不会有任何问题。
除非真得有必要不要发布接口。
重构与设计相互补充。
虽然重构可能使软件运行更慢,但他也使软件的性能优化更容易。
3. 代码的坏味道
参考http://www.cnblogs.com/eyu/archive/2010/05/27/1938334.html
3.1. Duplicated Code重复的代码
如果是两个互为兄弟(sibling)的subclasses内含有相同表达式,应该使用Extract Method将方法提取到父类。
如果是两个不相关的class,可能需要使用Extract Class另外提炼出一个class,或在一个class中调用另一个class。
3.2. Long Method过长的函数
每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立的方法中,并以其用途(而非实现手法)命名。主要使用Extract Method。如果有太多临时变量和参数可以考虑使用Replace Method with Method Object。
条件式和循环常常也是提炼的信号。你可以使用Decompose Conditional处理条件式。至于循环,你应该将循环和其内的代码提炼到一例独立方法中。
3.3. Large Class过大的函数
如果出现太多instance变量,可以运用Extract Class将数个变量一直提炼到新class内。提炼时应该选择class内彼此相关的变量。通常如果class内的数个变量有着相同的前缀或字尾,这就意味有机会把它们提炼到某个组件内。如果这个组件适合作为一个subclass,你会发现Extract Subclass往往比较简单。
如果有太多代码,可以提炼重复的代码。也适合使用Extract Class和Extract Subclass。
3.4. Long parameter List过长参数列
如果[向既有对象发出一条请求]就可以取得原本位于参数列上的一份数据,那么你应该激活重构准则Replace Parameter with Method。还可以运用Preserve Whole Object将来自同一对象的一堆数据收集起来,并以该对象替换它们。如果某些数据缺乏合理的对象归属,可使用Introduce Parameter Object为它们制造出一个[参数对象]。
3.5. Divergent Change发散式的变化
如果某个class经常因为不同的原因在不同的方向上发生变化,Divergent Change就出现了。 此时也许将这个对象分成两个会更好,这么一来每个对象就可以只因一种变化而需要修改。
3.6. Shotgun Sergery散弹式修改
Shotgun Surgery类似Divergent Change,但恰恰相反。如果每遇到某种变化,你都必须在许多不同的class内做出许多小修改以响应之。你应该使用Move Method和Move Field把所有需要修改的代码放进同一个class。如果眼下没有合适的class可以安置这些代码,就创造一个。通常你可以运用Inline Class把一系列相关行为放进同一个class。
Divergent Change是指[一个class受多种变化的影响],Shotgun Surgery则是指[一种变化引发多个classes相应修改]。
3.7. Feature Envy依恋情节
方法对某个class的兴趣高过对自己所处之 host class的兴趣。疗法显而易见:把这个方法移到另一个地点。你应该使用Move Method把它移到它该去的地方。有时候方法中只有一部分受这种依恋之苦,这时候你应该使用Extract Method把这一部分提炼到独立方法中,再使用Move Method带它去它的梦中家园。
一个方法往往会用上数个classes特性,那么它究竟该被置于何处呢?我们的原则是:判断哪个class拥有最多[被此方法使用]的数据,然后就把这个方法和那些数据摆在一起。如果先以Extract Method将这个方法分解为整个较小方法并分别置放于不同地点,上述步骤也就比较容易完成了。
[四巨头]的Streategy和Visitor立刻跳入我的脑海,Kent Beck的Self Delegation破坏了这个原则。最根本的原则是:将总是一起变化的东西放在一块儿。
3.8. Data Clumps数据泥团
在很多地方看到相同的三或四笔数据项:两个classes内的相同字段、许多方法签名式中的相同参数。这些[总是绑在一起出现的数据]真应该放进属于它们自己的对象中。 首先请找出这些数据的字段形式出现点,运用Extract Class将它们提炼到一个独立对象中。然后将注意力转移到方法签名式上头,运用Introduce Parameter Object或Preserve Whole Object为它减肥。这么做的直接好处是可以将很多参数列缩短,简化方法调用动作。不必因为Data Clumps只用上新对象的一部分字段而在意。
一旦拥有新对象就可以着手寻找 Feature Envy。
3.9. Primitive Obsession基本类型偏执
你可以运用Replace Data Value with Object将原本单独存在的数据值替换为对象。如果欲替换之数据值是type code,而它并不影响行为,你可以运用Replace Type Code with Class将它换掉。如果你有相依于此type code的条件式,可运用Replace Type Code with Subclass或Replace Type Code with State/Strategy加以处理。
如果你有一组应该总是被放在一起的字段,可运用Extract Class。如果你在参数列中看到基本型数据,不妨试试Introduce Parameter Object。如果你发现自己正从array中挑选数据,可运用Replace Array with Object。
3.10. Swith Statements
switch语句应该考虑以多态来替换它。你应该使用Extract Method将switch语句提炼到一个独立方法中,再以Move Method将它搬移到需要多态性的那个class里头。此时你必须决定是否使用Replace Type Code with Subclasses或Replace Type Code with State/Strategy。一旦这样完成继承结构之后,你就可以运用Replace Conditional with Polymorphism了。
如果你只是在单一方法中选择事例,而你并不想改动它们,那么[多态]就有点杀鸡用牛刀了。这种情况下Replace Parameter with Explicit Methods是个不错的选择。如果你的选择条件之一是null,可以试试Introduce Null Object。
3.11. Parallel Inheritance Hierarchies平行继承体系
Parallel Inheritance Hierarchies其实是Shotgun Surgery的特殊情况。在这种情况下,每当你为某个class增加一个subclass,必须也为另一个class相应增加一个subclass。
消除这种重复性的一般策略是:让一个继承体系的实体调用另一个继承体系的实体。如果再接再厉运用Move Method和Move Field,就可以将指涉端的继承体系消弭于无形。
3.12. Lazy Class
如果一个class的所得不值其身份,它就应该消失。如果某些subclass没有做满足够工作,试试Collapse Hierarchy[合并继承]。对于几乎没用的组件,你应该以Inline Class对付它们。
3.13. Speculative Generality 夸夸其谈未来
如果你的某个abstract class其实没有太大作用,请运用Collapse Hierarchy。非必要之delegation可运用Inline Class除掉。如果方法的某些参数示被用上,可对它实施Rename Method让它现实一些。
如果方法或class的惟一用户是test cases,这就飘出了坏味道Speculative Generality。如果你发现这样的方法或class,请把它们连同其test cases都删掉。但如果它们的用途是帮助test cases检测正当功能,当然必须刀下留人。
3.14. Temporary Field 令人迷惑的暂时字段
有时你会看到这样的对象:其内某个instance 变量仅为某种特定情势而设。使用Extract Class给这个可怜的孤独创造一个家,然后把所有和这个变量相关的代码都放进这个新家。也许你还可以使用Introduce Null Object在[变量不合法]的情况下创建一个Null对象,从而避免写出[条件式代码]。
如果class中有一个复杂算法,需要好几个变量。由于实现者不希望传递一长串参数,所以他把这些参数都放进字段中。但是这些字段只在使用该算法时才有效,其它情况下只会让人迷惑。这时候你可以利用Extract Class把这些变量和其相关方法提炼到一个独立class中。提炼后的新对象将是一个method object。
3.15. Message Chaind过度耦合的消息链
如果你看到用户向一个对象索求另一个对象,然后再向后者索求另一个对象,然后再索求另一个对象……这就是Message Chain。采取这种方式,意味客户将与查找过程中的航行结构紧密耦合。一旦对象间的关系发生任何变化,客户端就不得不做出相应修改。
这时候你应该使用Hide Delegate。你可以在Message Chain的不同位置进行这种重构手法。理论上你可以重构Message Chain上的任何一个对象,但这么做往往会把所有中介对象都变成Middle Man。通常更好的选择是:先观察Message Chain最终得到的对象是用来干什么的,看看能否以Extract Method把使用该对象的代码提炼到一个独立方法中,再运用Move Method把这个方法推入Message Chain。如果这条链上的某个对象有多位客户打算航行此航线的剩余部分,就加一个方法来做这件事。
3.16. Middle Man中间人
人们可能过度运用delegation。你也许会看到某个class接口有一半的方法都委托给其它class,这样就是过度运用。这里你应该使用 Remove Middle Man,直接和负责对象打交道。
如果这样[不干实事]的方法只有少数几个,可以运用Inline Method把它们”inlining”,放进调用端。如果这些Middle Man还有其它行为可以运用Replace Delegation with Inheritance把它变成负责对象的subclass。
3.17. Inappropriate Intimacy狎昵关系
有时候你会看到两个classes过于亲密,花费太多时间去探究彼此的private成分。你可以采用Move Method和Move Field帮它们划清界线,从而减少狎昵行径。也可以看看是否运用Change Bidirectional Association to Unidirectional[将双向关联改为单向]让其中一个class对另一个斩断情丝。如果两个classes实在情投意合,可以运用Extract Class把两者共同点提炼到一个安全地点,让它们坦荡地使用这个新class。或者也可以尝试运用Hide Delegate让另一个class来为它们传递相思情。
继承往往造成过度亲密,因为subclass对superclass的了解总是超过superclass的主观愿望。如果你觉得该让这个孩子独自生活了,请运用Replace Inheritance with Delegation让它离开继承体系。
3.18. Alternative Classes with Different Interfaces异曲同工的类
如果两个方法做同一件事,却有着不同的签名式,请运用Rename Method根据它们的用途重新命名。但这往往不够,请反复运用Move Method将某些行为移入classes,直到两者的协议一致为止。如果你必须重复而赘余地移入代码才能完成这些,或许可运用Extract Superclass为自己赎点罪。
3.19. Incomplete Library Class不完美的类库
如果你只想修改library classes内的一两个方法,可以运用Introduce Foreign Method;如果想要添加一大堆额外行为,就得运用Introduce Local Extension。
3.20. Data Class纯稚的数据类
Data Class是指:它们拥有一些字段,以及用于访问这些字段的方法。
如果这些classes内含容器类的字段,你应该检查它们是不是得到了恰当的封装;如果没有,就运用Encapsulate Collection把它们封装起来。
对于那些不该被其它classes修改的字段,请运用Remove Setting Method。
然后,找出这些[取值/设值]方法被其它classes运用的地点。尝试以Move Method把那些调用行为搬移到Data Class来。如果无法搬移整个方法,就运用Extract Method产生一个可被搬移的方法。不久之后你就可以运用Hide Method把这些[取值/设值]方法隐藏起来了。
3.21. Refused Bequest被拒绝的馈赠
Subclasses应该继承superclass的方法和数据。但如果它们不想或不需要继承,又该怎么办呢?
按传统说法,这就意味继承体系设计错误。你需要为这个subclass新建一个兄弟,再运用Push Down Method和Push Down Field把所有用不到的方法下推给那兄弟。这样一来superclass就只持有所有subclasses共享的东西。常常你会听到这样的建议:所有 superclasses都应该是抽象的。
但不建议每次都这么做。我们经常利用subclassing手法来复用一些行为,并发现这可以很好地应用于日常工作。这也是一种坏味道。
如果subclass复用了superclass的行为(实现),却又不愿意支持superclass的接口,Refused Bequest的坏味道就会变得浓烈。拒绝继承superclass的实现,这一点我们不介意;但如果拒绝继承superclass的接口,我们不以为然。不过即使你不愿意继承接口,也不要胡乱修改继承系,你应该运用Replace Inheritance with Delegation来达到目的。
3.22. Comments 过多的注释
常常会有这样的情况:你看到一段代码有着长长的注释,然后发现,这些注释之所以存在乃是因为代码很糟糕。如果你需要注释来解释一块代码做了什么,试试Extract Method;如果你需要注释说明某些系统的需求规格,试试Introduce Assertion。