您的位置:首页 > 编程语言

《重构——改善现有代码的设计》 读书笔记

2009-01-02 17:23 369 查看
重构——改善现有代码的设计[/b]
[/b]读书笔记[/b]
[/b]

为什么重构?

相信任何程序员,无论技术拙劣或高超,都可以写出机器理解的代码。但是不是所有的程序员,无论技术拙劣或高超,都可以写出其他人(包括他自己)可以轻易看懂的代码,尤其是离第一次写该代码已经过去一个月甚至一年。重构技术的给我们代来的第一个好处就是他是我们的代码条理清晰,简明易读。于此同时,重构技术借用OO原则,将强代码的扩展性和灵活性,使得日后添加新功能更加轻松容易。
更重要的是,重构加快了我们的开发进度。在软件开发过程中,我们每天应该关注两件事情,“今天应该做什么”和“明天应该做什么”。大多数时候,无论是修改BUG还是添加新功能,我们都是关注今天应该完成的事情,忽略了今天已经做的事情对明天应该做的事情的影响。这样,今天虽然完成任务,但是明天无法完成,同时导致后天也无法完成······就这样,造成了恶性循环,得不偿失。
重构是改变这种束缚之道。今天发现昨天做的事情无法继续,重构之。明天也许发现见天的事情很幼稚,重构之。反正你可以在需要修改的时候进行重构过程。
重构虽然在短期内放慢了我们的开发速度,但是重长远角度来看,我们会更容易添加新功能,更容易对现有的代码进行维护。从而弥补先前的损失。正所谓磨刀不误砍柴工吗!
通过重构,可以去除我们代码中的一些坏味道(bad smell),因此,积极进取的程序员应该对该技术产生兴趣,使用该技术。这就是为什么我们要用重构。

如何重构?

重构一般和添加新功能同时交替进行。在进行这两项活动的时候,就好像戴上两顶帽子,“重构”不会添加删除添加软件功能,对于用户而言,重构是透明的。“添加新功能”就不要过于担心可扩展性和灵活性,首要任务是添加新功能。如果在重构时,发现需要添加的功能,可以记下来,稍后再添加新功能。添加新功能的时候也是一样。
软件设计与重构可以互补。软件设计可以使我们编程时思考更快,但是起中充满了小漏洞,这些漏洞需要重构来弥补。有人把程序员与民工作比喻。程序员只需要照着设计的图纸编码就可以。但我个人并不这样认为,软件的开发与现实世界建筑建设不同,没有精确的数学公式和物理定理可以借鉴,可塑性很大。现实世界的建筑图纸可以精确的设计出所有的管道,走廊布局,门窗结构,民工只需要按照图纸做就可以。但是软件的设计往往不可能一步到位,经常由于需求的变更而改变,所以需要对程序员有较高要求,能够应付这种改变,或者预知这种改变,事先做好准备。重构就可以帮助我们预测这种改变,做到未雨绸缪。
以前的开发,在设计上实用的经历很多。因为总是希望找到一个灵活的设计方案,以应变日后的各种变化。这样往往增加的程序的复杂度和可维护性。但是,最后发现这些灵活性大多没有必要,到头来白忙活。有了重构技术,我们在设计时不需要投入过多的精力,开始只用找到一个恰当的解决方案,如果日后发现该方案不能满足要求,重构之。并且,随着时间的流逝,我们对系统的理解日益加强,我们会更容易的找到灵活的方案。

合适使用重构?

1) 修改Bugs:当你发现代码晦涩难以理解时,你需要重构。
2) 添加新功能:当你发现新功能不容易加入,或是加入新功能后 模块的耦合加大,你需要重构
重构没有固定的时间周期,当你发现需要重构时,就放开手去重构。但是有的时机,重构也不太适宜,如:
1) 项目快到期了,重构的长远效应体现不出来。
2) 软件大部分不能运转。
重构时,不要太注重性能优化,因为此时不是考虑性能优化的时段。你同时应该只把注意力放在一个目标上。等到性能优化时,你在着重考虑该问题。并且,一个项目需要优化的地方只有20%是性能的瓶颈,其余的80%无伤大雅。所以你在总够过程重遇到需要性能优化的地方可能并不会对系统的总体性能造成太大的消耗。

上面讲了这么多重构的好处,但都是站在森林100里外欣赏森林。接下来,我将详细的总结一下重构的细节,仔细的欣赏一下重构森林里的美妙风景。
Martin Fowler大师和Kent Beck大师在谈论如何找到既有代码中需要重构之处时,使用了一个很形象的比喻——坏味道(Bad Smells in Code)。需要重构的地方是散发着浓烈的坏味道的,如果不用重构芳香剂去除之,将会使你的代码慢慢腐败变质,遗臭万年。下面,我就来谈谈这些坏味道。

Bad Smells List:

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,让后将调用之处换用方法调用,同时去掉注释。

测试的重要性

这本书不是一本专门介绍软件测试的书籍,但是他还是花了一定的篇幅谈到了单元测试,说明此行为一定与我们的重构有着密切的关系。没错,频繁测试可以增加实用重构技巧的机会,更重要的是加快开发。这听起来有点违反常识,但是的确是这样。回忆一下,在开发过程中,你用的最多的工作是干什么?没错,不是编码,而是设计,测试调试,代码的编写只是花了一小部分时间。所以,频繁的测试,可以将严重的Bug消灭在摇篮中,而且可发现许多上述提到的坏味道,重而实用你的十八反重构武器进行修改,更重要的是,每次测试时,你可以将bug的范围受到最小——既上次成功之处到你刚刚完成的地方,这样原来可能需要一两个小时的找虫子的工作可以顷刻间解决。

重构技巧

下面就是本书最精彩的部分了——重构技巧名录。在这里,我只想简单的把他们列出来,因为他们有些从名字上就可以直到如何进行,即使不会也可Google一下,找到答案。我只对稍微晦涩的重构技巧展开说明。
l Composing Method
1. Extract Method
2. Inline Method
3. Inline Temp
4. Replace Temp With Query
5. Introducing Explaining Variable
6. Split Temporary Variable
7. Remove Assignments to Parameters
8. Replace Method With Method Object
9. Substitute Algorithm
l Moving Features Betweens Objects
1. Move Method
2. Move Field
3. Extract Class
4. Inline Class
5. Hide Delegate
6. Remove Middle Man
7. Introduce Foreign Method
8. Introduce Local Extension
l Organizing Data
1. Self Encapsulation Field
2. Replace Data Value with Object
3. Change Value to Reference
4. Change Reference to Value
5. Replace Array with Object
6. Duplicate Observed Data
7. Change Unidirectional Association to Bidirectional
8. Change Bidirectional Association to Unidirectional
9. Replace Magic Number with Symbolic Constant
10. Encapsulate Field
11. Encapsulate Collection
12. Replace Record with Data Class
13. Replace Type Code with Class
14. Replace Type Code with Subclass
15. Replace Type Code with State/Strategy
16. Replace Subclass with Fields
l Simplifying Conditional Expressions
1. Decompose Conditional
2. Consolidate Conditional Expression
3. Consolidate Duplicate Conditional Features
4. Remove Control Flag
5. Replace Nested Conditional with Guard Clauses
6. Replace Conditional with Polymorphism
7. Introduce Null Object
8. Introduce Assertion
l Making Method Calls Simpler
1. Rename Method
2. Add Parameter
3. Remove Parameter
4. Separate Query from Modifier
5. Parameterize Method
6. Replace Parameter with Explicit Methods
7. Preserver Whole Object
8. Replace Parameter with Method
9. Introduce Parameter Object
10. Remove Setting Method
11. Hide Method
12. Replace Constructor with Factory Method
13. Encapsulate Downcast
14. Replace Error Code with Exception
15. Replace Exception with Test
l Dealing with Generalization
1. Pull Up Field
2. Pull Up Method
3. Pull Up Constructor Body
4. Push Down Method
5. Push Down Field
6. Extract Subclass
7. Extract Super class
8. Extract Interface
9. Collapse Hierarchy
10. Form Template Method
11. Replace Inheritance with Delegation
12. Replace Delegation with Inheritance
(蓝色为已使用过的手法)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: