一、为什么重构?
重构技术的给我们代来的第一个好处就是他是我们的代码条理清晰,简明易读。于此同时,重构技术借用OO原则,将强代码的扩展性和灵活性,使得日后添加新功能更加轻松容易。
在软件开发过程中,我们每天应该关注两件事情,“今天应该做什么”和“明天应该做什么”。大多数时候,无论是修改BUG还是添加新功能,我们都是关注今天应该完成的事情,忽略了今天已经做的事情对明天应该做的事情的影响。这样,今天虽然完成任务,但是明天无法完成,同时导致后天也无法完成······就这样,造成了恶性循环,得不偿失。
重构是改变这种束缚之道。今天发现昨天做的事情无法继续,重构之。明天也许发现见天的事情很幼稚,重构之。反正你可以在需要修改的时候进行重构过程。重构虽然在短期内放慢了我们的开发速度,但是重长远角度来看,我们会更容易添加新功能,更容易对现有的代码进行维护。从而弥补先前的损失。正所谓磨刀不误砍柴工吗!
二、如何重构?
所谓重构,就是使用一系列重构手法对软件内部结构进行调整,目的是在不改变程序功能的前提下,提高其可理解性,降低代码的修改和维护成本。重构可以:1改进软件设计,2使代码更容易理解,3提高开发效率。
重构也是设计的一部分,软件设计与重构可以互补。软件设计可以使我们编程时思考更快,但是起中充满了小漏洞,这些漏洞需要重构来弥补。
重构之前,首先检查自己是否有一套可靠的测试机制,这些测试必须有自我检查能力。重构时,每次只修改一个地方的坏味道,然后进行测试,保证功能和重构之前一致,把bug的数量降到最低。
重构一般和添加新功能同时交替进行。在进行这两项活动的时候,就好像戴上两顶帽子,“重构”不会添加删除添加软件功能,对于用户而言,重构是透明的。“添加新功能”就不要过于担心可扩展性和灵活性,首要任务是添加新功能。如果在重构时,发现需要添加的功能,可以记下来,稍后再添加新功能。添加新功能的时候也是一样。
如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性。千万不要说有时间再做重构,有时间再做等于永远不会做。
三、代码的坏味道:
1. Duplicated Code(重复代码):重复代码是万恶之源。重复的代码便显出了设计者的设计不够完善,无法将这些重复的代码放到一个类中安家,而是将他们分散在系统中的许多地方。这样,一旦你需要修改这些地方中的一个,你就需要找到系统中的其他地方,将他们全部修改。这种情况在大型程序中,将会给维护人员带来噩梦。最重要的还不仅仅是代码级的糟糕表现,而是设计级别的不协调。因为没有遵循DRY(Don’t Repeat Yourself,不要重复你自己)。
2. Long Method(过长方法):经验表明,过长的方法体(几百行以上)表明你的代码中存在着严重的OP(Procedure-Oriented,面向过程)编程迹象。所以,你需要重新省视你的代码,抽象出可以重用的代码段,实用Extract Method重构技巧给他们安个家。在过长的方法中,经常出现很大的if—else或switch,这时,你就需要考虑用Replace Nested Conditional with Polymorphism来去除这些巨大的条件选择。总之,作为一个纯粹的OO设计,过长的方法几乎是不会出现的,也不应该出现。
3. Large Class(过大类):你的程序中,出现过大的类,有可能这个类承担了过多的责任,你应该注意它,为他去除些责任。可以用Extract Class,Move Method等手段来帮他瘦身。否则,如果某个责任需要修改,你就要修改这个巨大的耦合的类,带来维护的噩梦。更重要的是,这个坏味道违背了SRP(Single Responsibility Principle,单一责任原则)。
4. Long Parameter List(过长参数类):过长的参数类是客户代码实用该方法时困难重重,因为必须找到每一个适合的参数,才可以调用他,无论这些参数重要与否。这样的函数也不方便阅读,加大了代码的维护程度。于此同时,可能暴露的方法实现的细节,破坏封装性,使得修改该方法时举步维艰。对象技术的最大好处在于将数据与方法融合在一起,那么可以避免许多不必要的参数传递。可以用Introduce Parameter Object,Method Object,Replace Parameter With Method等重构手法,来减短参数列表长度。
5. Divergent Change(发散式变化):来减你的某个类有可能由于需求的改变需要换一种实现方式,但是总体的意图是不变的。此时,如果你的代码过于耦合,以至于十分困难的进行修改,那么,你的代码就发出了发散式变化的凑味道。遇到这中情况,你应该将可能变化的地方提取到一个Class,以应变这种未知的变化。
6. Shotgun Surgery(霰弹式修改):如果某一个小的修改需要在程序的许多其他地方修改,那么你的代码飘着霰弹式修改的臭味。这时,你需要Move Method,或其他重构手段将这些可能修改的地方移动到统一的地方。
7. Feature Envy(依恋情节):如果存在class A和class B,当调用A中的某个方法时却使用了一打class B的数据,那么class A对class B有过大的依恋情节。此时解决方法显而易见,使用Move Method将方法从A移到B,斩断情丝。当然显示的情况往往不是这么简单,有可能class A太花心,当调用同一个方法时,同时对过多的类有依恋情节,那么此时你应该了冷静,将该method移动到依恋程度最深的类中。
8. Data Clumps(数据泥团):数据喜欢成群结对的出现,你有可能常常会看到某三四个类总是一起出现在参数列表,不同class内的值域。那么,你应该感到他们不可分开,否则落单的数据会很孤单。所以,你需要将他们放在一起,所使用Extract Class为他们安个家。由一个比较有效的方法可以证实你的判断,如果去除他们中一个,他们就没有多大意义,那么不要犹豫,他们就应该在一个类中。
9. Primitive Obsession(基本类型偏执):面向对象的语言中,大都存在一些基本类型(比如JAVA中的int, float等等),他们是构成其他对象的基本原属,就好比原子与分子的关系。而其他的OO语言除了上述的基本类型外,将Date,Money这种事物也认为是基本的类型。所以,我们在编程时,往往潜意识中会将一些变量当然地认为是基本类型。但是,有时候,基本类型没有方法,不能完成一些特定的功能,此时就不能仍然将这些数据认为是基本类型,而是应该将他们抽象到类中,即使该类只有一个数据字段。比如,电话号码。可以使用long,string表示,但是如果需要提取电话哦号码的区号,或其他业务相关的信息,我们就需要添加方法。此时,我们只能在宿主类(host class)中添加这些方法。如果我们将其抽象成类,那么就可以很轻松的在这个对象内部安置这些方法。
10. Switch Statements(臭名昭著的switch):switch之所以可恶,是因为他将改变硬编码到代码中,如果日后需要加入新的case,必须到程序中所有的switch语句中添加这个case,这又是一个噩梦的开始。多态性可以所是为此现象量身定做的解决方案,当嗅到此坏味道时,聪明的你应该会想到多态性。
11. Parallel Inheritance Hierachies(平行继承体系):当你太继承体系中添加一个新子类时,发现必须在另一继承体系中需要加入一个对应的继承体系,才能完成特定功能,那么这两个继承体系的耦合程度太大,你需要消除以继承体系。该坏味道其实是shotgun surgery的变种,可以实用Move Filed和委托将多余的继承体系消弭于无形。
12. Lazy Class(累赘类):程序中的每一个类应该担起一步分责任,有自己存在的价值,为这个小小的对象社会贡献一份自己的力量。但是,当你为你的程序做了一些修改,使得一些类的作用微乎其微,或者你发现当初你留下来应对未来变化的类被事实证明并不会发生这样的变化,又或者是你的子类根本就没有做什么。如果向我上述说的那样,就干脆让这些类就义吧,他们的离去是为了小小对象社会的更加繁荣。你可以用Collapse Hierarchy或inline class等重构手法来去除系统中多余的类。
13. Speculative Generality(夸夸其谈的未来性):当有人说:“噢,我想我们总有一天需要这么做”,并因而企图以各式各样的挂钩(hooks)和特殊情况(special cases)来处理一些非必要的事情,这种坏味道就出现了。那么做的结果往往造成系统更难以理解和维护。如果所有的装置都会北用到,那么就值得做,否则就需要去除这些多余的东西。可以用到的手法有:Collapse Hierarchy,Inline Class等等。
14. Temporary Filed(令人迷惑的临时值域):在你的代码中,有可能发现一些临时变量,他们有可能是变量,有可能是常量,但是你无法一眼就识别出他们的意图,此时这些变量就令你迷惑。那么这种坏味道就飘散出来了,你需要明确每一个变量的一同,以易理解的名称,和明显的修饰符来表明这些变量(如常量,就用const声明之)。如果一个详单复杂的算法,有数个临时变量,这些变量只在该算法中有效,那么你可以实用Extract Class为这些无家可归的临时变量安个家,那么他们也会使你的代码更具有可读性。
15. Message Chains(过度耦合的消息链):你有可能遇到这种情况,对象A调用对象B,接着对象C然后·····,这就是过度偶合的消息链,使得客户代码无法面对将来的变化,一旦消息链出现修改,你需要在所有的客户代码中,一个一个的找到并修改。一种好的解决方案是观察这种过长的消息链的意图,看是否可以将消息链的最终结果直接封装到一个方法中,去除这种过长的消息链。
16. Middle Man(过度的中间转手人):对象技术的一大技巧是实用delegation,但是过多的实用delegation也会带来不必要的复杂度。比如一个class中一大半的方法是实用delegation来进行的,他就是成了一个没有必要的中间转手人,在这里挣回扣(消耗不必要的资源)但是用不干实事。这时,你需要实用Remove Middle Man的重构手法来去除这个二道贩子。
17. Inappropriate Intimacy(不适当的亲密关系):如果人与人之间出现了不适当的亲密关系,我们无权干涉,但是在对象世界中出现这种关系,我们必须做卫道人士,斩断这种不适当的亲密行为。可以实用Move Field,Move Method等将这些情投意合的代码段移到一起,亦可以实用Extract Class是两个类合并为一个类,使有情人终成眷属。但是绝对不允许有藕断丝连的想象发生。这种情况最容易发生在继承体系中,因为父类无法预知未来的所有行为,所以他不能总是满足其子类的要求,这时候,父亲应该狠下心,将儿子脱离这个继承体系,实用委托来完成儿子需要的行为以便他能够独立的在对象社会中生存。
18. Alternative Classes with Different Interface(异曲同工的类):如果不同的类以不同的方法名称执行同样的方法,请要留神,此时要实用Rename Method和Move Method将这些意图相同的方法移动到一个地方。你可以实用Extract Super Class重构手法来执行此操作,直到所有的类都达成一致的协议。
19. Incomplete Library Class(不完美的类库):现在的开发,往往都不是重头开始,而是实用别人已经开发出的类库的基础上进行开发。类库的创造者一般多是大牛,但是即使是大牛,也不可能有未卜先知的能力。中有些情况是他们无法预测到的,这时你不要抱怨,可以实用Introduce Local Extension和Introduce Foreign Method来定制你特定的任务。
20. Data Class(存执的数据类):在你的系统中往往会出现一些哑对象,他们只有数据和一些数据访问方法,他们作为数据的内存中的临时容器,除此之外别无它用,特别是在开发一些基于数据库的应用程序时,这种现象更为明显。有了重构芳香剂,我们可以来去除这中坏味道。首先,如果暂时没有好的方法加入这些哑对象中,就那他们去,随着对系统的理解,运用Move Method等重构手法,你会自然而然的为这些哑对象加入方法。如果到最后,这些对像还是没有得到好的去除,你可以Move Field将他们移到自己应该去的地方。当然,世上也不总是完美的,最后如果实在没有安放他们的地方,就让他们呆在那儿吧。哑对象就像小孩子,如果想在对象社会中独立生存,必须承担起责任(拥有自己的方法)。
21. Refused Bequest(被拒绝的遗赠):继承虽然是一种强大的机制,但是最大的缺点是子类必须继承所有的东西,一样不纳。有时候,这时很不必要的。如果出现这种坏味道,你可以考虑实用Push Down/Up Method/Field在继承体系中进行修改。这种修改可能需要经过多次变化,才能达到相对稳定的状态,不要怕,唯一不变的就是变化,只要你没每次向着正确的方向前进,就不会出太大问题。但是如果出现拒绝继承接口,这时最好不要实用继承而改用委托,这样你就可以实用你自己中意的接口。
22. Comments(过多的注释):注释并不是坏东西,但是有时过多的注释反而增加了代码的可读性。有时候,注释可以被易读的方法调用替换,如果发现过多的注释,不妨实用Extract Method,让后将调用之处换用方法调用,同时去掉注释。
表格总结如下:
坏味道 | 特征 | 情况及处理方式 | 目标 | |
重复代码 | 1.重复的表达式 | 同一个类的两个函数有相同表达式 | 重复代码提取为方法 | 相同表达式只在一个类的一个方法出现,供其他方法调用 |
2.不同算法做相同的事 | 兄弟类含有相同表达式 | 重复代码提取为方法 | ||
3.类似代码 | 提升方法到父类 | |||
不相干类含有相同代码 | 提取为独立类供调用 | |||
过长函数 | 1.代码前面有注释 | 提取方法 | 每个方法只做一件事,方法要定义完善、命名准确 | |
2.条件表达式 | ||||
3.循环 | ||||
过大的类 | 1.一个类中有太多实例变量 | 部分字段之间相关性高 | 相关的字段和方法提取为类 | 每个类负责一组具有内在的相互关联的任务 |
2.一个类中有太多代码 | 某些字段和方法只被某些实例用到 | 这些字段和方法移到子类中 | ||
过长参数列 | 1.参数列过长 | 方法可以通过其他方式获取该参数 | 让参数接受者自行获取该参数 | 只需要传给函数足够的、让其可以从中获取自己需要的东西就行了 |
2.参数列变化频繁 | 同一对象的若干属性作为参数 | 在不使依赖恶化的情况下,使用整个对象作为参数 | ||
被调用函数使用了另一个对象的很多属性 | 将方法移动到该对象中 | |||
某些数据缺乏归属对象 | 首先创建对象 | |||
发散式变化 | 一个类受多种变化的影响 | 类经常因为不同的原因在不同的方向上发生变化 | 将特定原因造成的所有变化提取为一个新类 | 针对某一外界变化的所有修改,只应发生在单一类中,而这个类中所有的内容都应反映此变化 |
散弹式修改 | 一种变化引发多个类的修改 | 某种变化需要在许多不同的类中做出小修改 | 把所有需要修改的代码放进同一个类中 | 针对某一外界变化的所有修改,只应发生在单一类中,而这个类中所有的内容都应反映此变化 |
依恋情结 | 一个函数使用其他类属性比使用自身类属性还要多 | 某个函数从另一个对象调用了几乎半打的取值函数 | 将依恋代码提取为单独方法,移动到另一对象 | 将数据和对数据的操作行为包装在一起 |
数据泥团 | 同时使用的相关数据并未以类的方式组织 | 先将字段提取为类,再缩减函数签名中的参数 | 总是绑在一起的数据应该拥有属于它们自己的对象 | |
1.两个类中相同的字段 | ||||
2.许多函数中相同的参数 | ||||
基本类型偏执 | 过多使用基本类型 | 总是被放在一起的基本类型字段 | 提取类 | 将单独存在的数据值转换为对象 |
参数列中有基本类型 | 提取参数对象 | |||
数组中容纳了不同的对象,需要从数组中挑选数据 | 用对象取代数组 | |||
基本数据是类型码 | 使用类替换类型码 | |||
带条件表达式的类型码 | 使用继承类替换类型码 | |||
Switch语句 | 相同的switch、case语句散布于不同地方 | 根据类型码进行选择的switch | 使用多态替代switch | 避免到处做相同的修改 |
单一函数中有switch | 使用显式的方法取代参数 | |||
平行继承体系 | 1.为某个类增加子类时,必须为另一个类增加子类 | 一个继承体系中的实例引用另一个继承体系中的实例,然后迁移成员 | 避免到处做相同的修改 | |
2.某个继承体系类名前缀和另一个继承体系类名前缀相同 | ||||
冗赘类 | 类无所事事 | 父类和子类无太大差别 | 将它们合为一体 | |
某个类没有做太多事情 | 将这个类所有成员移到另一个类中,删除它 | |||
夸夸其谈未来性 | 某个抽象类没有太大作用 | 将父子类合并 | ||
不必要的委托 | 将这个类所有成员移到另一个类中,删除它 | |||
函数的某些参数未用上 | 移除参数 | |||
函数名称带有多余的抽象意味 | 重命名函数名 | |||
函数只被测试方法调用 | 连同测试代码一并删除 | |||
令人迷惑的暂时字段 | 1.某个实例字段仅为某种情况而设 | 提取单独的类,封装相关代码 | ||
2.某些实例字段仅为某个函数的复杂算法少传参数而设 | ||||
过度耦合的消息链 | 一长串的getThis或临时变量 | 客户类通过一个委托类来取得另一个对象 | 隐藏委托 | 消除耦合 |
中间人 | 某个类接口有大量的函数都委托给其他类,过度使用委托 | 有一半的函数 | 移除中间人 | |
少数几个函数 | 直接调用 | |||
中间人还有其他行为 | 让委托类继承受托类 | |||
狎昵关系 | 某个类需要了解另一个类的私有成员 | 子类过分了解超类 | 将继承改为委托,把子类从继承体系移出 | 封装 |
类之间双向关联 | 去掉不必要的关联 | |||
类之间有共同点 | 提取新类 | |||
异曲同工的类 | 两个函数做同一件事,但是签名不同 | 合并 | ||
不完美的类库 | 类库函数构造的不够好,又不能修改它们 | 想修改一两个函数 | 在调用类增加函数 | |
想添加一大堆额外行为 | 使用子类或包装类 | |||
幼稚的数据类 | 某个类除了字段,就是字段访问器、设置器 | 1.用访问器取代public字段 | 封装 | |
2.恰当封装集合 | ||||
3.移除不需要的设置器 | ||||
4.搬移对访问器、设置器调用方法到此类 | ||||
5.隐藏访问器、设置器 | ||||
被拒绝的馈赠 | 派生类仅使用了基类很少一部分成员函数 | 子类拒绝继承超类接口 | 使用委托替代继承 | |
过多的注释 | 一段代码有着长长的注释 | 消除各种坏味道 |