![]() |
| 第1页 |
![]() |
| 第10页 |
![]() |
| 第16页 |
![]() |
| 第22页 |
![]() |
| 第41页 |
![]() |
| 第73页 |
参见附件(5690KB,326页)。
重构:改善既有代码的设计,这是一本关于代码重构过程的书籍介绍,全书一共有15个章节,各位程序员们可以通过此书完成对代码的进一步研究。

改善既有代码的设计介绍
本书是经典著作《重构》出版20年后的新版。书中清晰揭示了重构的过程,解释了重构的原理和实践方式,并给出了何时以及何地应该开始挖掘代码以求改善。书中给出了60多个可行的重构,每个重构都介绍了一种经过验证的代码变换手法的动机和技术。本书提出的重构准则将帮助开发人员一次一小步地修改代码,从而减少了开发过程中的风险。本书适合软件开发人员、项目管理人员等阅读,也可作为高等院校计算机及相关专业师生的参考读物。
改善既有代码的设计作者
马丁·福勒(Martin Fowler)
软件开发大师,ThoughtWorks 科学家。他是一位作家、演说者、咨询师。他致力于改善企业级的软件设计,对设计以及支撑设计的工程实践孜孜以求。他在重构、面向对象分析设计、模式、XP 和UML 等领域都有贡献,著有《重构》《分析模式》《领域特定语言》等经典著作。 译者简介 熊节 在IT 行业已经打拼了18年,在金融、零售、政府、电信、制造业等行业的信息化建设方面有着丰富经验,是中国IT业敏捷浪潮的领军人物。熊节拥有利物浦大学MBA学位。
林从羽
ThoughtWorks软件开发工程师,曾服务于 外多家大型企业,致力于帮助团队 快 好地交付可工作的软件。拥抱敏捷精神,TDD爱好者,纯键盘工作者。
改善既有代码的设计亮点
1.软件开发大师的不朽经典
2.生动阐述重构原理和具体做法
3.普通程序员进阶到编程高手必须修炼的秘笈
改善既有代码的设计目录
第1章 重构,第一个案例
第2章 重构原则
第3章 代码的坏味道
第4章 构筑测试体系
第5章 重构列表
第6章 重新组织函数
第7章 在对象之间搬移特性
第8章 重新组织数据
第9章 简化条件表达式
第10章 简化函数调用
第11章 处理概括关系
第12章 大型重构
第13章 重构,复用与现实
第14章 重构工具
第15章 总结
重构:改善既有代码的设计截图

封面
重构列表
扉页
版权
版权声明
重构的重新认识(再版序)
重构的生活方式(译序)
序
前言
第1章 重构,第一个案例
1.1 起点
1.2 重构的第一步
1.3 分解并重组statement
1.4 运用多态取代与价格相关的条件逻辑
1.5 结语
第2章 重构原则
2.1 何谓重构
2.2 为何重构
2.3 何时重构
2.4 怎么对经理说
2.5 重构的难题
2.6 重构与设计
2.7 重构与性能
2.8 重构起源何处
第3章 代码的坏味道
3.1 Duplicated Code(重复代码)
3.2 Long Method(过长函数)
3.3 Large Class(过大的类)
3.4 Long Parameter List(过长参数列)
3.5 Divergent Change(发散式变化)
3.6 Shotgun Surgery(霰弹式修改)
3.7 Feature Envy(依恋情结)
3.8 Data Clumps(数据泥团)
3.9 Primitive Obsession(基本类型偏执)
3.10 Switch Statements(switch惊悚现身)
3.11 Parallel Inheritance Hierarchies(平行继承体系)
3.12 Lazy Class(冗赘类)
3.13 Speculative Generality(夸夸其谈未来性)
3.14 Temporary Field(令人迷惑的暂时字段)
3.15 Message Chains(过度耦合的消息链)
3.16 Middle Man(中间人)
3.17 Inappropriate Intimacy(狎昵关系)
3.18 Alternative Classes with Different Interfaces (异曲同工的类)
3.19 Incomplete Library Class(不完美的库类)
3.20 Data Class(纯稚的数据类)
3.21 Refused Bequest(被拒绝的遗赠)
3.22 Comments(过多的注释)
第4章 构筑测试体系
4.1 自测试代码的价值
4.2 JUnit测试框架
4.3 添加更多测试第5章 重构列表
5.1 重构的记录格式
5.2 寻找引用点
5.3 这些重构手法有多成熟
第6章 重新组织函数
6.1 Extract Method(提炼函数)
6.2 Inline Method(内联函数)
6.3 Inline Temp(内联临时变量)
6.4 Replace Temp with Query(以查询取代临时变量)
6.5 Introduce Explaining Variable(引入解释性变量)
6.6 Split Temporary Variable(分解临时变量)
6.7 Remove Assignments to Parameters(移除对参数的赋值)
6.8 Replace Method with Method Object(以函数对象取代函数)
6.9 Substitute Algorithm(替换算法)
第7章 在对象之间搬移特性
7.1 Move Method(搬移函数)
7.2 Move Field(搬移字段)
7.3 Extract Class(提炼类)
7.4 Inline Class(将类内联化)
7.5 Hide Delegate(隐藏“委托关系” )
7.6 Remove Middle Man(移除中间人)
7.7 Introduce Foreign Method(引入外加函数)
7.8 Introduce Local Extension(引入本地扩展)
第8章 重新组织数据
8.1 Self Encapsulate Field(自封装字段)
8.2 Replace Data Value with Object(以对象取代数据值)
8.3 Change Value to Reference(将值对象改为引用对象)
8.4 Change Reference to Value(将引用对象改为值对象)
8.5 Replace Array with Object(以对象取代数组)
8.6 Duplicate Observed Data(复制“被监视数据”)
8.7 Change Unidirectional Association to Bidirectional (将单向关联改为双向关联)
8.8 Change Bidirectional Association to Unidirectional (将双向关联改为单向关联)
8.9 Replace Magic Number with Symbolic Constant (以字面常量取代魔法数)
8.10 Encapsulate Field(封装字段)
8.11 Encapsulate Collection(封装集合)
8.12 Replace Record with Data Class(以数据类取代记录)
8.13 Replace Type Code with Class(以类取代类型码)
8.14 Replace Type Code with Subclasses(以子类取代类型码)
8.15 Replace Type Code with StateStrategy (以StateStrategy取代类型码)
8.16 Replace Subclass with Fields(以字段取代子类)
第9章 简化条件表达式
9.1 Decompose Conditional(分解条件表达式)
9.2 Consolidate Conditional Expression(合并条件表达式)
9.3 Consolidate Duplicate Conditional Fragments (合并重复的条件片段)
9.4 Remove Control Flag(移除控制标记)
9.5 Replace Nested Conditional with Guard Clauses (以卫语句取代嵌套条件表达式)
9.6 Replace Conditional with Polymorphism (以多态取代条件表达式)
9.7 Introduce Null Object(引入Null对象)
9.8 Introduce Assertion(引入断言)
第10章 简化函数调用
10.1 Rename Method(函数改名)
10.2 Add Parameter(添加参数)
10.3 Remove Parameter(移除参数)
10.4 Separate Query from Modifier (将查询函数和修改函数分离)10.5 Parameterize Method(令函数携带参数)
10.6 Replace Parameter with Explicit Methods (以明确函数取代参数)
10.7 Preserve Whole Object(保持对象完整)
10.8 Replace Parameter with Methods(以函数取代参数)
10.9 Introduce Parameter Object(引入参数对象)
10.10 Remove Setting Method(移除设值函数)
10.11 Hide Method(隐藏函数)
10.12 Replace Constructor with Factory Method (以工厂函数取代构造函数)
10.13 Encapsulate Downcast(封装向下转型)
10.14 Replace Error Code with Exception (以异常取代错误码)
10.15 Replace Exception with Test(以测试取代异常)
第11章 处理概括关系
11.1 Pull Up Field(字段上移)
11.2 Pull Up Method(函数上移)
11.3 Pull Up Constructor Body(构造函数本体上移)
11.4 Push Down Method(函数下移)
11.5 Push Down Field(字段下移)
11.6 Extract Subclass(提炼子类)
11.7 Extract Superclass(提炼超类)
11.8 Extract Interface(提炼接口)
11.9 Collapse Hierarchy(折叠继承体系)
11.10 Form Template Method(塑造模板函数)
11.11 Replace Inheritance with Delegation (以委托取代继承)
11.12 Replace Delegation with Inheritance (以继承取代委托)
第12章 大型重构
12.1 Tease Apart Inheritance(梳理并分解继承体系)
12.2 Convert Procedural Design to Objects (将过程化设计转化为对象设计)
12.3 Separate Domain from Presentation (将领域和表述显示分离)
12.4 Extract Hierarchy(提炼继承体系)
第13章 重构,复用与现实
13.1 现实的检验
13.2 为什么开发者不愿意重构他们的程序
13.3 再论现实的检验
13.4 重构的资源和参考资料
13.5 从重构联想到软件复用和技术传播
13.6 小结
13.7 参考文献
第14章 重构工具
14.1 使用工具进行重构
14.2 重构工具的技术标准
14.3 重构工具的实用标准
14.4 小结
第15章 总结
参考书目
要点列表
索引
代码的坏味道重构列表
Add Parameter(添加参数) 275
Change Bidirectional Association to Unidirectional(将双向关联改为单向关联) 200
Change Reference to Value(将引用对象改为值对象) 183
Change Unidirectional Association to Bidirectional(将单向关联改为双向关联) 197
Change Value to Reference(将值对象改为引用对象) 179
Collapse Hierarchy(折叠继承体系) 344
Consolidate Conditional Expression(合并条件表达式) 240
Consolidate Duplicate Conditional Fragments(合并重复的条件片段) 243
Convert Procedural Design to Objects(将过程化设计转化为对象设计) 368
Decompose Conditional(分解条件表达式) 238
Duplicate Observed Data(复制“被监视数据”) 189
Encapsulate Collection(封装集合) 208
Encapsulate Downcast(封装向下转型) 308
Encapsulate Field(封装字段) 206
Extract Class(提炼类) 149
Extract Hierarchy(提炼继承体系) 375
Extract Interface(提炼接口) 341
Extract Method(提炼函数) 110
Extract Subclass(提炼子类) 330
Extract Superclass(提炼超类) 336
Form Template Method(塑造模板函数) 345
Hide Delegate(隐藏“委托关系”) 157
Hide Method(隐藏函数) 303
Inline Class(将类内联化) 154
Inline Method(内联函数) 117
Inline Temp(内联临时变量) 119
Introduce Assertion(引入断言) 267
Introduce Explaining Variable(引入解释性变量) 124
Introduce Foreign Method(引入外加函数) 162
Introduce Local Extension(引入本地扩展) 164
Introduce Null Object(引入Null对象) 260
Introduce Parameter Object(引入参数对象) 295
Move Field(搬移字段) 146
Move Method(搬移函数) 142
Parameterize Method(令函数携带参数) 283
Preserve Whole Object(保持对象完整) 288Pull Up Constructor Body(构造函数本体上移) 325
Pull Up Field(字段上移) 320
Pull Up Method(函数上移) 322
Push Down Field(字段下移) 329
Push Down Method(函数下移) 328
Remove Assignments to Parameters(移除对参数的赋值) 131
Remove Control Flag(移除控制标记) 245
Remove Middle Man(移除中间人) 160
Remove Parameter(移除参数) 277
Remove Setting Method(移除设值函数) 300
Rename Method(函数改名) 273
Replace Array with Object(以对象取代数组) 186
Replace Conditional with Polymorphism(以多态取代条件表达式) 255
Replace Constructor with Factory Method(以工厂函数取代构造函数) 304
Replace Data Value with Object(以对象取代数据值) 175
Replace Delegation with Inheritance(以继承取代委托) 355
Replace Error Code with Exception(以异常取代错误码) 310
Replace Exception with Test(以测试取代异常) 315
Replace Inheritance with Delegation(以委托取代继承) 352
Replace Magic Number with Symbolic Constant(以字面常量取代魔法数) 204
Replace Method with Method Object(以函数对象取代函数) 135
Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件表达式) 250
Replace Parameter with Explicit Methods(以明确函数取代参数) 285
Replace Parameter with Methods(以函数取代参数) 292
Replace Record with Data Class(以数据类取代记录) 217
Replace Subclass with Fields(以字段取代子类) 232
Replace Temp with Query(以查询取代临时变量) 120
Replace Type Code with Class(以类取代类型码) 218
Replace Type Code with StateStrategy(以StateStrategy取代类型码) 227
Replace Type Code with Subclasses(以子类取代类型码) 223
Self Encapsulate Field(自封装字段) 171
Separate Domain from Presentation(将领域和表述显示分离) 370
Separate Query from Modifier(将查询函数和修改函数分离) 279
Split Temporary Variable(分解临时变量) 128
Substitute Algorithm(替换算法) 139
Tease Apart Inheritance(梳理并分解继承体系) 362PEARSON
Refactoring Improving the Design of Existing Code
重构改善既有代码的设计
[美]Martin Fowler 著
熊节 译
人民邮电出版社
北京
◆著 [美]Martin Fowler
译 熊节
责任编辑 杨海玲
责任印制 张佳莹 焦志炜
◆人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http:www.ptpress.com.cn
三河市中晟雅豪印务有限公司印刷
◆开本:800×1000 116
印张:28.25
字数:490千字 2015年8月第2版
印数:1-6000册 2015年8月河北第1次印刷
著作权合同登记号 图字:01-2009-5707号
定价:69.00元
读者服务热线:(010)81055410 印装质量热线:(010)81055316
反盗版热线:(010)81055315
广告经营许可证:京崇工商广字第0021号
物。
本书适合软件开发人员、项目管理人员等阅读,也可作为高等院校计算机及相关专业师生的参考读
和技术。本书提出的重构准则将帮助你一次一小步地修改你的代码,从而减少了开发过程中的风险。
掘代码以求改善。书中给出了70多个可行的重构,每个重构都介绍了一种经过验证的代码变换手法的动机
本书清晰揭示了重构的过程,解释了重构的原理和最佳实践方式,并给出了何时以及何地应该开始挖
内容提要
中国版本图书馆CIP数据核字(2015)第137778号
Ⅰ.①重… Ⅱ.①福…②熊… Ⅲ.①机器代码程序—程序设计 Ⅳ.①TP311.11
ISBN 978-7-115-36909-3
书名原文:Refactoring:Improving the Design of Existing Code
2015.8
重构:改善既有代码的设计(美)福勒(Fowler,M.)著;熊节译.--2版.--北京:人民邮电出版社,加工、传播自负法律后果。
本书仅供个人学习之用,请勿用于商业用途。如对本书有兴趣,请购买正版书籍。任何对本书籍的修改、重构的重新认识(再版序)
光阴荏苒,从当年译完这本《重构》,到如今重新整理译稿,不知不觉已经过去6年了。6年来,在各
种大型系统中进行重构和指导别人重构,一直是我的一项工作。对于这本早已烂熟于心的书,也有了一些
新的认识。
不得不遗憾地说,尽管“重构”已经成了常用词汇,但重构技术并没有像我当初乐观认为的那样“变得像
空气与水一样普通”。一方面,一种甚嚣尘上的观点认为只要掌握重构的思想就足够了,没必要记住那些详
细琐碎的重构手法;另一方面,倒是有很多人高擎“重构”大旗,刀劈斧砍进行着令人触目惊心的大胆修改
——有些干脆就是在重做整个系统。
这些人常常忘了一个最基本的定义:重构是在不改变软件可观察行为的前提下改善其内部结构。当你
面对一个最需要重构的遗留系统时,其规模之大、历史之久、代码质量之差,常会使得添加单元测试或者
理解其逻辑都成为不可能的任务。此时你唯一能依靠的就是那些已经被证明是行为保持的重构手法:用绝
对安全的手法从“焦油坑”中整理出可测试的接口,给它添加测试,以此作为继续重构的立足点。
六年来,在各种语言、各种行业、各种软件形态,包括规模达到上百万行代码的项目中进行重构的经
验让我明白,“不改变软件行为”只是重构的最基本要求。要想真正让重构技术发挥威力,就必须做到“不需
了解软件行为”——听起来很荒谬,但事实如此。如果一段代码能让你容易了解其行为,说明它还不是那么
迫切需要被重构。那些最需要重构的代码,你只能看到其中的“坏味道”,接着选择对应的重构手法来消除
这些“坏味道”,然后才有可能理解它的行为。而这整个过程之所以可行,全赖你在脑子里记录着一份“坏味
道”与重构手法的对应表。
而且,尽管Java和.NET的自动化重构工具已经相当成熟,但另一些重要的面向对象语言(C++、Ruby、Python……)还远未享受到这样的便利。在重构这些语言编写的程序时,我们仍然必须遵循这些看
似琐碎的做法指导(加上语言特有的细节调整),按部就班地进行——如果你还想以安全的方式重构的
话。
所以,仅仅掌握思想是没用的。如果把重构比作一门功夫的话,它的威力全都来自日积月累的勤学苦
练。记住所有的“坏味道”,记住它们对应的重构手法,记住常见的重构步骤,然后你才可能有信心面对各
种复杂情况——学会所有的招式,才可能“无招胜有招”。我知道这听起来很难,但我也知道这并不像你想
象的那么难。你所需要的只是耐心、毅力和不断重读这本书。
熊节
2009年10月21日重构的生活方式(译序)
第一次听到“重构”这个词,是在2001年10月。在当时,它的思想足以令我感到震撼。软件自有其美感
所在。软件工程希望建立完美的需求与设计,按照既有的规范编写标准划一的代码,这是结构的美;快速
迭代和RAD颠覆“全知全能”的神话,用近乎刀劈斧砍(crack)的方式解决问题,在混沌的循环往复中实现
需求,这是解构的美;而Kent Beck与Martin Fowler两人站在一起,以XP那敏捷而又严谨的方法论演绎了重
构的美——我不知道是谁最初把refactoring一词翻译为“重构”,或许无心插柳,却成了点睛之笔。
我一直是设计模式的爱好者。曾经在我的思想中,软件开发应该有一个“理想国”——当然,在这个理
想国维持着完美秩序的,不是哲学家,而是模式。设计模式给我们的,不仅仅是一些具体问题的解决方
案,更有追求完美“理型”的渴望。但是,Joshua Kerievsky在那篇著名的《模式与XP》(收录于《极限编程
研究》一书)中明白地指出:在设计前期使用模式常常导致过度工程(over-engineering)。这是一个残酷
的现实,单凭对完美的追求无法写出实用的代码,而“实用”是软件压倒一切的要素。从一篇《停止过度工
程》开始,Kerievsky撰写了“Refactoring to Patterns”系列文章。这位犹太人用他民族性的睿智头脑,敏锐地
发现了软件的后结构主义道路。而让设计模式在飞速变化的网络时代重新闪现光辉的,又是重构的力量。
在一篇流传甚广的帖子里,有人把《重构》与《设计模式》并列为“Java行业的圣经”。在我看来这种并
列其实并不准确。实际上,尽管我如此喜爱这本《重构》,但自从完成翻译之后,就再也没有读过它。
不,不是因为我已经对它烂熟于心,而是因为重构已经变成了我的另一种生活方式,变成了我每天的“面包
与黄油”,变成了我们整个团队的空气与水,以至于无需再到书中寻找任何“神谕”。而《设计模式》,我倒
是放在手边时常翻阅,因为总是记得不那么真切。
所以,在你开始阅读本书之前,我要给你两个建议:首先,把你的敬畏扔到太平洋里去,对于即将变
得像空气与水一样普通的技术,你无需对它敬畏;其次,找到合适的开发工具(如果你和我一样是Java人,那么这个“合适的工具”就是Eclipse),学会使用其中的自动测试和重构功能,然后再尝试使用本书介绍的
任何技术。懒惰是程序员的美德之一,绝不要因为这本书让你变得勤快。
最后,即使你完全掌握了这本书中的所有东西,也千万不要跟别人吹嘘。在我们的团队里,程序员常
常会说:“如果没有单元测试和重构,我没办法写代码。”
好了,感谢你耗费一点点的时间来倾听我现在对重构、对《重构》这本书的想法。Martin Fowler经常
说,花一点时间来重构是值得的,希望你会觉得花一点时间看我的文字也是值得的。
熊节
2003年6月11日于杭州序
“重构”这个概念来自Smalltalk圈子,没多久就进入了其他语言阵营之中。由于重构是框架开发中不可缺
少的一部分,所以当框架开发人员讨论自己的工作时,这个术语就诞生了。当他们精炼自己的类继承体系
时,当他们叫喊自己可以拿掉多少多少行代码时,重构的概念慢慢浮出水面。框架设计者知道,这东西不
可能一开始就完全正确,它将随着设计者的经验成长而进化;他们也知道,代码被阅读和被修改的次数远
远多于它被编写的次数。保持代码易读、易修改的关键,就是重构——对框架而言如此,对一般软件也如
此。
好极了,还有什么问题吗?问题很显然:重构具有风险。它必须修改运作中的程序,这可能引入一些
不易察觉的错误。如果重构方式不恰当,可能毁掉你数天甚至数星期的成果。如果重构时不做好准备,不
遵守规则,风险就更大。你挖掘自己的代码,很快发现了一些值得修改的地方,于是你挖得更深。挖得越
深,找到的重构机会就越多,于是你的修改也越多……最后你给自己挖了个大坑,却爬不出去了。为了避
免自掘坟墓,重构必须系统化进行。我在《设计模式》书中和另外三位作者曾经提过:设计模式为重构提
供了目标。然而“确定目标”只是问题的一部分而已,改造程序以达到目标,是另一个难题。
Martin Fowler和本书另几位作者清楚揭示了重构过程,他们为面向对象软件开发所做的贡献难以衡
量。本书解释了重构的原理和最佳实践,并指出何时何地你应该开始挖掘你的代码以求改善。本书的核心
是一系列完整的重构方法,其中每一项都介绍一种经过实践检验的代码变换手法的动机和技术。某些项目
如Extract Method和Move Field看起来可能很浅显,但不要掉以轻心,因为理解这类技术正是有条不紊地进
行重构的关键。本书所提的这些重构手法将帮助你一次一小步地修改你的代码,这就减少了过程中的风
险。很快你就会把这些重构手法和其名称加入自己的开发词典中,并且朗朗上口。
我第一次体验有讲究的、一次一小步的重构,是某次与Kent Beck在30 000英尺高空的飞行旅途中结对
编程。我们运用本书收录的重构手法,保证每次只走一步。最后,我对这种实践方式的效果感到十分惊
讶。我不但对最后结果更有信心,而且开发压力也小了很多。所以,我极力推荐你试试这些重构手法,你
和你的程序都将因此更美好。
Erich Gamma
《设计模式》第一作者,Eclipse平台主架构师前言
从前,有位咨询顾问造访客户调研其开发项目。系统核心是个类继承体系,顾问看了开发人员所写的
一些代码。他发现整个体系相当凌乱,上层超类对于系统的运作做了一些假设,下层子类实现这些假设。
但是这些假设并不适合所有子类,导致覆写(override)工作非常繁重。只要在超类做点修改,就可以减少
许多覆写工作。在另一些地方,超类的某些意图并未被良好理解,因此其中某些行为在子类内重复出现。
还有一些地方,好几个子类做相同的事情,其实可以把它们搬到继承体系的上层去做。
这位顾问于是建议项目经理看看这些代码,把它们整理一下,但是经理并不热衷于此,毕竟程序看上
去还可以运行,而且项目面临很大的进度压力。于是经理说,晚些时候再抽时间做这些整理工作。
顾问也把他的想法告诉了在这个继承体系上工作的程序员,告诉他们可能发生的事情。程序员都很敏
锐,马上就看出问题的严重性。他们知道这并不全是他们的错,有时候的确需要借助外力才能发现问题。
程序员立刻用了一两天的时间整理好这个继承体系,并删掉了其中一半代码,功能毫发无损。他们对此十
分满意,而且发现在继承体系中加入新的类或使用系统中的其他类都更快、更容易了。
项目经理并不高兴。进度排得很紧,有许多工作要做。系统必须在几个月之后发布,而这些程序员却
白白耗费了两天时间,干的工作与要交付的多数功能毫无关系。原先的代码运行起来还算正常,他们的新
设计看来有点过于追求完美。项目要交付给客户的,是可以有效运行的代码,不是用以取悦学究的完美东
西。顾问接下来又建议应该在系统的其他核心部分进行这样的整理工作,这会使整个项目停顿一至二个星
期。所有这些工作只是为了让代码看起来更漂亮,并不能给系统添加任何新功能。
你对这个故事有什么感想?你认为这个顾问的建议(更进一步整理程序)是对的吗?你会遵循那句古
老的工程谚语吗:“如果它还可以运行,就不要动它。”
我必须承认自己有某些偏见,因为我就是那个顾问。六个月之后这个项目宣告失败,很大的原因是代
码太复杂,无法调试,也无法获得可被接受的性能。
后来,项目重新启动,几乎从头开始编写整个系统,Kent Beck受邀做了顾问。他做了几件迥异以往的
事,其中最重要的一件就是坚持以持续不断的重构行为来整理代码。这个项目的成功,以及重构在这个成
功项目中扮演的角色,启发了我写这本书,如此一来我就能够把Kent和其他一些人已经学会的“以重构方式
改进软件质量”的知识,传播给所有读者。
什么是重构
所谓重构(refactoring)是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进
程序的内部结构。重构是一种经千锤百炼形成的有条不紊的程序整理方法,可以最大限度地减少整理过程
中引入错误的几率。本质上说,重构就是在代码写好之后改进它的设计。
“在代码写好之后改进它的设计”?这种说法有点奇怪。按照目前对软件开发的理解,我们相信应该先设
计而后编码:首先得有一个良好的设计,然后才能开始编码。但是,随着时间流逝,人们不断修改代码,于是根据原先设计所得的系统,整体结构逐渐衰弱。代码质量慢慢沉沦,编码工作从严谨的工程堕落为胡
砍乱劈的随性行为。
“重构”正好与此相反。哪怕你手上有一个糟糕的设计,甚至是一堆混乱的代码,你也可以借由重构将
它加工成设计良好的代码。重构的每个步骤都很简单,甚至显得有些过于简单:你只需要把某个字段从一
个类移到另一个类,把某些代码从一个函数拉出来构成另一个函数,或是在继承体系中把某些代码推上推
下就行了。但是,聚沙成塔,这些小小的修改累积起来就可以根本改善设计质量。这和一般常见的“软件会慢慢腐烂”的观点恰恰相反。
通过重构,你可以找出改变的平衡点。你会发现所谓设计不再是一切动作的前提,而是在整个开发过
程中逐渐浮现出来。在系统构筑过程中,你可以学习如何强化设计,其间带来的互动可以让一个程序在开
发过程中持续保有良好的设计。
本书有什么
本书是一本为专业程序员而写的重构指南。我的目的是告诉你如何以一种可控制且高效率的方式进行
重构。你将学会如何有条不紊地改进程序结构,而且不会引入错误,这就是正确的重构方式。
按照传统,图书应该以引言开头。尽管我也同意这个原则,但是我发现以概括性的讨论或定义来介绍
重构,实在不是件容易的事。所以我决定用一个实例做为开路先锋。第1章展示了一个小程序,其中有些常
见的设计缺陷,我把它重构为更合格的面向对象程序。其间我们可以看到重构的过程,以及几个很有用的
重构手法。如果你想知道重构到底是怎么回事,这一章不可不读。
第2章讨论重构的一般性原则、定义,以及进行重构的原因,我也大致介绍了重构所存在的一些问题。
第3章由Kent Beck介绍如何嗅出代码中的“坏味道”,以及如何运用重构清除这些坏味道。测试在重构中扮演
着非常重要的角色,第4章介绍如何运用一个简单而且开源的Java测试框架,在代码中构筑测试环境。
本书的核心部分——重构列表——从第5章延伸至第12章。它不能说是一份全面的列表,只是一个起
步,其中包括迄今为止我在工作中整理下来的所有重构手法。每当我想做点什么(例如Replace Conditional
with Polymorphism (255))的时候,这份列表就会提醒我如何一步一步安全前进。我希望这是值得你日后一
再回顾的部分。
本书介绍了其他人的许多研究成果,最后几章就是由他们之中的几位所客串写就的。Bill Opdyke在第
13章记述他将重构技术应用于商业开发过程中遇到的一些问题。Don Roberts和John Brant在第14章展望重构
技术的未来——自动化工具。我把最后一章(第15章)留给重构技术的顶尖大师Kent Beck来压轴。
在Java中运用重构
本书范例全部使用Java撰写。重构当然也可以在其他语言中实现,而且我也希望这本书能够给其他语言
使用者带来帮助。但我觉得我最好在本书中只使用Java,因为那是我最熟悉的语言。我会不时写下一些提
示,告诉读者如何在其他语言中进行重构,不过我真心希望看到其他人在本书基础上针对其他语言写出更
多重构方面的书籍。
为了很好地与读者交流我的想法,我没有使用Java语言中特别复杂的部分。所以我避免使用内嵌类、反
射机制、线程以及很多强大的Java特性。这是因为我希望尽可能清楚地展现重构的核心。
我应该提醒你,这些重构手法并不针对并发或分布式编程。那些主题会引出更多的考虑,本书并未涉
及。
谁该阅读本书
本书的目标读者是专业程序员,也就是那些以编写软件为生的人。书中的示例和讨论,涉及大量需要
详细阅读和理解的代码。这些例子都以Java写成。之所以选择Java,因为它是一种应用范围越来越广的语
言,而且任何具备C语言背景的人都可以轻易理解它。Java是一种面向对象语言,而面向对象机制对于重构
有很大帮助。
尽管关注对象是代码,但重构对于系统设计也有巨大影响。资深设计师和架构师也很有必要了解重构
原理,并在自己的项目中运用重构技术。最好是由老资格、经验丰富的开发人员来引入重构技术,因为这
样的人最能够透彻理解重构背后的原理,并根据情况加以调整,使之适用于特定工作领域。如果你使用的
不是Java,这一点尤其重要,因为你必须把我给出的范例以其他语言改写。
下面我要告诉你,如何能够在不通读全书的情况下充分用好它。如果你想知道重构是什么,请阅读第1章,其中示例会让你清楚重构的过程。
如果你想知道为什么应该重构,请阅读前两章。它们告诉你重构是什么以及为什么应该重构。
如果你想知道该在什么地方重构,请阅读第3章。它会告诉你一些代码特征,这些特征指出“这里需要
重构”。
如果你想着手进行重构,请完整阅读前四章,然后选择性地阅读重构列表。一开始只需概略浏览列
表,看看其中有些什么,不必理解所有细节。一旦真正需要实施某个准则,再详细阅读它,从中获取帮
助。列表部分是供查阅的参考性内容,你不必一次就把它全部读完。此外你还应该读一读列表之后其他作
者的“客串章节”,特别是第15章。
站在前人的肩膀上
就在本书一开始的此时此刻,我必须说:这本书让我欠了一大笔人情债,欠那些在过去十年中做了大
量研究工作并开创重构领域的人一大笔债。这本书原本应该由他们之中的某个人来写,但最后却是由我这
个有时间有精力的人捡了便宜。
重构技术的两位最早倡导者是Ward Cunningham和Kent Beck。他们很早就把重构作为开发过程的一个
核心成分,并且在自己的开发过程中运用它。尤其需要说明的是,正因为和Kent的合作,才让我真正看到
了重构的重要性,并直接激励了我写出这本书。
Ralph Johnson在UIUC(伊利诺伊大学厄巴纳—尚佩恩分校)领导了一个小组,这个小组因其在对象技
术方面的实际贡献而声名远扬。Ralph很早就是重构技术的拥护者,他的一些学生也一直在研究这个课题。
Bill Opdyke的博士论文是重构研究的第一份详细的书面成果。John Brant和Don Roberts则早已不满足于写文
章了,他们写了一个工具叫Refactoring Browser(重构浏览器),对Smalltalk程序实施重构工程。
致谢
尽管有这些研究成果可以借鉴,我还是需要很多协助才能写出这本书。首先,并且也是最重要的,Kent Beck给了我巨大的帮助。Kent在底特律的某个酒吧和我谈起他正在为Smalltalk Report撰写一篇论文
[Beck,hanoi],从此播下本书的第一颗种子。那次谈话不但让我开始注意到重构技术,而且我还从
中“偷”了许多想法放到本书第1章。Kent也在其他很多方面帮助我,想出“代码味道”这个概念的是他,当我
遇到各种困难时,鼓励我的人也是他,常常和我一起工作助我完成这本书的,还是他。我常常忍不住这么
想:他完全可以自己把这本书写得更好。可惜有时间写书的人是我,所以我也只能希望自己不要做得太
差。
写这本书的时候,我希望能把一些专家经验直接与你分享,所以我非常感激那些花时间为本书添砖加
瓦的人。Kent Beck、John Brant、William Opdyke和Don Roberts编撰或合写了本书部分章节。此外Rich
Garzaniti和Ron Jeffries帮我添加了一些有用的文中注解。
在任何一本此类技术书里,作者都会告诉你,技术审阅者提供了巨大的帮助。一如既往,Addison-
Wesley出版社的Carter Shanklin和他的团队组织了强大的审稿人阵容,他们是:
Ken Auer,Rolemodel软件公司
Joshua Bloch,Sun公司Java软件部
John Brant,UIUC
Scott Corley,High Voltage软件公司
Ward Cunningham,Cunningham&Cunningham公司
Stéphane Ducasse
Erich Gamma,对象技术国际公司
Ron JeffriesRalph Johnson,伊利诺伊大学
Joshua Kerievsky,Industrial Logic公司
Doug Lea,纽约州立大学Oswego分校
Sander Tichelaar
他们大大提高了本书的可读性和准确性,并且至少去掉了一些任何手稿都可能会藏有的错误。在此我
要特别感谢两个效果显著的建议,它们让我的书看上去耳目一新:Ward和Ron建议我以重构前后效果并列
对照的方式写第1章,Joshua Kerievsky建议我在重构列表中画出代码草图。
除了正式审阅小组,还有很多非正式的审阅者。这些人或看过我的手稿,或关注我的网页并留下对我
很有帮助的意见。他们是Leif Bennett、Michael Feathers、Michael Finney、Neil Galarneau、Hisham
Ghazouli、Tony Gould、John Isner、Brian Marick、Ralf Reissing、John Salt、Mark Swanson、Dave Thomas和
Don Wells。我相信肯定还有一些被我遗忘的人,请容我在此向你们道歉,并致上我的谢意。
有一个特别有趣的审阅小组,就是“恶名昭彰”的UIUC读书小组。本书反映出他们的众多研究成果,我
要特别感谢他们用录音记录的意见。这个小组成员包括Fredrico“Fred”Balaguer、John Brant、Ian Chai、Brian
Foote、Alejandra Garrido、Zhijiang“John”Han、Peter Hatch、Ralph Johnson、Songyu“Raymond”Lu、Dragos-
Anton Manolescu、Hiroaki Nakamura、James Overturf、Don Roberts、Chieko Shirai、Les Tyrell和Joe Yoder。
任何好想法都需要在严酷的生产环境中接受检验。我看到重构对于克莱斯勒综合薪资系统(Chrysler
Comprehensive Compensation,C3)发挥了巨大的作用。我要感谢那个团队的所有成员:Ann Anderson、Ed
Anderi、Ralph Beattie、Kent Beck、David Bryant、Bob Coe、Marie DeArment、Margaret Fronczak、Rich
Garzaniti、Dennis Gore、Brian Hacker、Chet Hendrickson、Ron Jeffries、Doug Joppie、David Kim、Paul
Kowalsky、Debbie Mueller、Tom Murasky、Richard Nutter、Adrian Pantea、Matt Saigeon、Don Thomas和
Don Wells。和他们一起工作所获得的第一手数据,巩固了我对重构原理和作用的认识。他们使用重构技术
所取得的进步极大程度地帮助我看到:重构技术应用于历时多年的大型项目中,可以起到何等的作用!
再提一句,我得到了Addison-Wesley出版社的J.Carter Shanklin及其团队的帮助,包括Krysia Bebick、Susan Cestone、Chuck Dutton、Kristin Erickson、John Fuller、Christopher Guzikowski、Simone Payment和
Genevieve Rajewski。与优秀出版商合作是一个令人愉快的经历,他们为我提供了大量的支持和帮助。
谈到支持,为一本书付出最多的,总是距离作者最近的人。那就是现在已成为我妻子的Cindy。感谢
她,当我埋首工作的时候,还是一样爱我。即使在我投入写书时,也总会不断想起她。
Martin Fowler
于马萨诸塞州Melrose市
fowler@acm.org
http:www.martinfowler.com
http:www.refactoring.com第1章 重构,第一个案例
我该从何说起呢?按照传统做法,一开始介绍某个东西时,首先应该大致讲讲它的历史、主要原理等
等。可是每当有人在会场上介绍这些东西,总是诱发我的瞌睡虫。我的思绪开始游荡,我的眼神开始迷
离,直到主讲人秀出实例,我才能够提起精神。实例之所以可以拯救我于太虚之中,因为它让我看见事情
在真正进行。谈原理,很容易流于泛泛,又很难说明如何实际应用。给出一个实例,就可以帮助我把事情
认识清楚。
所以我决定从一个实例说起。在此过程中我将告诉你很多重构的道理,并且让你对重构过程有一点感
觉。然后我才能向你展开通常的原理介绍。
但是,面对这个介绍性实例,我遇到了一个大问题。如果我选择一个大型程序,那么对程序自身的描
述和对整个重构过程的描述就太复杂了,任何读者都不忍卒读(我试了一下,哪怕稍微复杂一点的例子都
会超过100页)。如果我选择一个容易理解的小程序,又恐怕看不出重构的价值。
和任何立志要介绍“应用于真实世界中的有用技术”的人一样,我陷入了一个十分典型的两难困境。我
只能带引你看看如何在一个我所选择的小程序中进行重构,然而坦白说,那个程序的规模根本不值得我们
那么做。但是如果我给你看的代码是大系统的一部分,重构技术很快就变得重要起来。所以请你一边观赏
这个小例子,一边想象它身处于一个大得多的系统。
1.1 起点
实例非常简单。这是一个影片出租店用的程序,计算每一位顾客的消费金额并打印详单。操作者告诉
程序:顾客租了哪些影片、租期多长,程序便根据租赁时间和影片类型算出费用。影片分为三类:普通
片、儿童片和新片。除了计算费用,还要为常客计算积分,积分会根据租片种类是否为新片而有不同。
我用了几个类来表现这个例子中的元素。图1-1是一张UML类图,用以显示这些类。
我会逐一列出这些类的代码。
图1-1 本例一开始的各个类。此图只显示最重要的特性。图中所用符号是UML([Fowler,UML])
Movie(影片)
Movie只是一个简单的纯数据类。
public class Movie {
public static final int CHILDRENS = 2;
public static final int REGULAR = 0;
public static final int NEW_RELEASE = 1;private String _title;
private int _priceCode;
public Movie(String title,int priceCode){
_title = title;
_priceCode = priceCode;
}
public int getPriceCode{
return _priceCode;
}
public void setPriceCode(int arg){
_priceCode = arg;
}
public String getTitle {
return _title;
};
}
Rental(租赁)
Rental表示某个顾客租了一部影片。
class Rental {
private Movie _movie;
private int _daysRented;
public Rental(Movie movie,int daysRented){
_movie = movie;
_daysRented = daysRented;
}
public int getDaysRented{
return _daysRented;
}
public Movie getMovie{
return _movie;
}
}
Customer(顾客)
Customer类用来表示顾客。就像其他类一样,它也拥有数据和相应的访问函数:
class Customer {
private String _name;
private Vector _rentals = new Vector;
public Customer (String name){
_name = name;
};public void addRental(Rental arg){
_rentals.addElement(arg);
}
public String getName {
return _name;
};
Customer还提供了一个用于生成详单的函数,图1-2显示这个函数带来的交互过程。完整代码显示于下
一页。
图1-2 statement的交互过程
public String statement{
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements;
String result = Rental Record for + getName+ \n;
while (rentals.hasMoreElements){
double thisAmount = 0;
Rental each = (Rental)rentals.nextElement;
determine amounts for each line
switch (each.getMovie.getPriceCode){
case Movie.REGULAR:
thisAmount += 2;
if (each.getDaysRented> 2)
thisAmount += (each.getDaysRented- 2) 1.5;
break;case Movie.NEW_RELEASE:
thisAmount += each.getDaysRented 3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if (each.getDaysRented> 3)
thisAmount += (each.getDaysRented- 3) 1.5;
break;
}
add frequent renter points
frequentRenterPoints ++;
add bonus for a two day new release rental
if ((each.getMovie.getPriceCode== Movie.NEW_RELEASE)
each.getDaysRented> 1)frequentRenterPoints ++;
show figures for this rental
result += \t + each.getMovie.getTitle+ \t +
String.valueOf(thisAmount)+ \n;
totalAmount += thisAmount;
}
add footer lines
result += Amount owed is + String.valueOf(totalAmount)+ \n;
result += You earned + String.valueOf(frequentRenterPoints)+
frequent renter points;
return result;
}
对此起始程序的评价
这个起始程序给你留下什么印象?我会说它设计得不好,而且很明显不符合面向对象精神。对于这样
一个小程序,这些缺点其实没有什么大不了的。快速而随性地设计一个简单的程序并没有错。但如果这是
复杂系统中具有代表性的一段,那么我就真的要对这个程序信心动摇了。Customer里头那个长长的
statement做的事情实在太多了,它做了很多原本应该由其他类完成的事情。
即便如此,这个程序还是能正常工作。所以这只是美学意义上的判断,只是对丑陋代码的厌恶,是
吗?如果不去修改这个系统,那么的确如此,编译器才不会在乎代码好不好看呢。但是当我们打算修改系
统的时候,就涉及了人,而人在乎这些。差劲的系统是很难修改的,因为很难找到修改点。如果很难找到
修改点,程序员就很有可能犯错,从而引入bug。
在这个例子里,我们的用户希望对系统做一点修改。首先他们希望以HTML格式输出详单,这样就可
以直接在网页上显示,这非常符合时下的潮流。现在请你想一想,这个变化会带来什么影响。看看代码你
就会发现,根本不可能在打印HTML报表的函数中复用目前statement的任何行为。你唯一可以做的就是编
写一个全新的htmlStatement,大量重复statement的行为。当然,现在做这个还不太费力,你可以把
statement复制一份然后按需要修改就是了。
但如果计费标准发生变化,又会如何?你必须同时修改statement和htmlStatement,并确保两处修改的一致性。当你后续还要再修改时,复制粘贴带来的问题就浮现出来了。如果你编写的是一个永不需要修
改的程序,那么剪剪贴贴就还好,但如果程序要保存很长时间,而且可能需要修改,复制粘贴行为就会造
成潜在的威胁。
现在,第二个变化来了:用户希望改变影片分类规则,但是还没有决定怎么改。他们设想了几种方
案,这些方案都会影响顾客消费和常客积分点的计算方式。作为一个经验丰富的开发者,你可以肯定:不
论用户提出什么方案,你唯一能够获得的保证就是他们一定会在六个月之内再次修改它。
为了应付分类规则和计费规则的变化,程序必须对statement做出修改。但如果我们把statement内的
代码复制到用以打印HTML详单的函数中,就必须确保将来的任何修改在两个地方保持一致。随着各种规
则变得越来越复杂,适当的修改点越来越难找,不犯错的机会也越来越少。
你的态度也许倾向于尽量少修改程序:不管怎么说,它还运行得很好。你心里牢牢记着那句古老的工
程谚语:“如果它没坏,就不要动它。”这个程序也许还没坏掉,但它造成了伤害。它让你的生活比较难
过,因为你发现很难完成客户所需的修改。这时候,重构技术就该粉墨登场了。
如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便地达成目的,那就先重构那个
程序,使特性的添加比较容易进行,然后再添加特性。
1.2 重构的第一步
每当我要进行重构的时候,第一个步骤永远相同:我得为即将修改的代码建立一组可靠的测试环境。
这些测试是必要的,因为尽管遵循重构手法可以使我避免绝大多数引入bug的情形,但我毕竟是人,毕竟有
可能犯错。所以我需要可靠的测试。
由于statement的运作结果是个字符串,所以我首先假设一些顾客,让他们每个人各租几部不同的影
片,然后产生报表字符串。然后我就可以拿新字符串和手上已经检查过的参考字符串做比较。我把所有测
试都设置好,只要在命令行输入一条Java命令就把它们统统运行起来。运行这些测试只需几秒钟,所以你会
看到我经常运行它们。
测试过程中很重要的一部分,就是测试程序对于结果的报告方式。它们要么说“OK”,表示所有新字符
串都和参考字符串一样,要么就列出失败清单,显示问题字符串的出现行号。这些测试都能够自我检验。
是的,你必须让测试有能力自我检验,否则就得耗费大把时间来回比对,这会降低你的开发速度。
进行重构的时候,我们需要依赖测试,让它告诉我们是否引入了bug。好的测试是重构的根本。花时间
建立一个优良的测试机制是完全值得的,因为当你修改程序时,好测试会给你必要的安全保障。测试机制
在重构领域的地位实在太重要了,我将在第4章详细讨论它。
重构前,先检查自己是否有一套可靠的测试机制。这些测试必须有自我检验能力。
1.3 分解并重组statement
第一个明显引起我注意的就是长得离谱的statement。每当看到这样长长的函数,我就想把它大卸八
块。要知道,代码块越小,代码的功能就愈容易管理,代码的处理和移动也就越轻松。
本章重构过程的第一阶段中,我将说明如何把长长的函数切开,并把较小块的代码移至更合适的类。
我希望降低代码重复量,从而使新的(打印HTML详单用的)函数更容易撰写。第一个步骤是找出代码的逻辑泥团并运用Extract Method (110)。本例一个明显的逻辑泥团就是switch语
句,把它提炼到独立函数中似乎比较好。
和任何重构手法一样,当我提炼一个函数时,我必须知道可能出什么错。如果提炼得不好,就可能给
程序引入bug。所以重构之前我需要先想出安全做法。由于先前我已经进行过数次这类重构,所以我已经把
安全步骤记录于后面的重构列表中了。
首先我得在这段代码里找出函数内的局部变量和参数。我找到了两个,each和thisAmount,前者并未被
修改,后者会被修改。任何不会被修改的变量都可以被我当成参数传入新的函数,至于会被修改的变量就
需格外小心。如果只有一个变量会被修改,我可以把它当作返回值。thisAmount是个临时变量,其值在每次
循环起始处被设为0,并且在switch语句之前不会改变,所以我可以直接把新函数的返回值赋给它。
下面两页展示了重构前后的代码。重构前的代码在左页,重构后的代码在右页。凡是从函数提炼出来
的代码,以及新代码所做的任何修改,只要我觉得不是明显到可以一眼看出,就以粗体字标示出来特别提
醒你。本章剩余部分将延续这种左右比对形式。
public String statement{
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements;
String result = Rental Record for + getName+ \n;
while (rentals.hasMoreElements){
double thisAmount = 0;
Rental each = (Rental)rentals.nextElement;
determine amounts for each line
switch (each.getMovie.getPriceCode){
case Movie.REGULAR:
thisAmount += 2;
if (each.getDaysRented> 2)
thisAmount += (each.getDaysRented- 2) 1.5;
break;
case Movie.NEW_RELEASE:
thisAmount += each.getDaysRented 3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if (each.getDaysRented> 3)
thisAmount += (each.getDaysRented- 3) 1.5;
break;
}
add frequent renter points
frequentRenterPoints ++;
add bonus for a two day new release rental
if ((each.getMovie.getPriceCode== Movie.NEW_RELEASE) each.getDaysRented> 1)frequentRenterPoints ++;
show figures for this rental
result += \t + each.getMovie.getTitle+ \t +
String.valueOf(thisAmount)+ \n;
totalAmount += thisAmount;
}
add footer lines
result += Amount owed is + String.valueOf(totalAmount)+ \n;
result += You earned + String.valueOf(frequentRenterPoints)
+ frequent renter points;
return result;
}
public String statement{
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements;
String result = Rental Record for + getName+ \n;
while (rentals.hasMoreElements){
double thisAmount = 0;
Rental each = (Rental)rentals.nextElement;
thisAmount = amountFor(each);
add frequent renter points
frequentRenterPoints ++;
add bonus for a two day new release rental
if ((each.getMovie.getPriceCode== Movie.NEW_RELEASE)
each.getDaysRented> 1)frequentRenterPoints ++;
show figures for this rental
result += \t + each.getMovie.getTitle+ \t +
String.valueOf(thisAmount)+ \n;
totalAmount += thisAmount;
}
add footer lines
result += Amount owed is + String.valueOf(totalAmount)+ \n;
result += You earned + String.valueOf(frequentRenterPoints)+
frequent renter points;
return result;
}
}
private int amountFor(Rental each){
int thisAmount = 0;switch (each.getMovie.getPriceCode){
case Movie.REGULAR:
thisAmount += 2;
if (each.getDaysRented> 2)
thisAmount += (each.getDaysRented- 2) 1.5;
break;
case Movie.NEW_RELEASE:
thisAmount += each.getDaysRented 3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if (each.getDaysRented> 3)
thisAmount += (each.getDaysRented- 3) 1.5;
break;
}
return thisAmount;
}
每次做完这样的修改,我都要编译并测试。这一次起头不算太好——测试失败了,有两条测试数据告
诉我发生了错误。一阵迷惑之后,我明白了自己犯的错误。我愚蠢地将amountFor的返回值类型声明为
int,而不是double。
private double amountFor(Rental each){
double thisAmount = 0;
switch (each.getMovie.getPriceCode){
case Movie.REGULAR:
thisAmount += 2;
if (each.getDaysRented> 2)
thisAmount += (each.getDaysRented- 2) 1.5;
break;
case Movie.NEW_RELEASE:
thisAmount += each.getDaysRented 3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if (each.getDaysRented> 3)
thisAmount += (each.getDaysRented- 3) 1.5;
break;
}
return thisAmount;
}
我经常犯这种愚蠢可笑的错误,而这种错误往往很难发现。在这里,Java无怨无尤地把double类型转换为int类型,而且还愉快地做了取整动作[Java Spec]。还好此处这个问题很容易发现,因为我做的修改很小,而且我有很好的测试。借着这个意外疏忽,我要阐述重构步骤的本质:由于每次修改的幅度都很小,所以
任何错误都很容易发现。你不必耗费大把时间调试,哪怕你和我一样粗心。
重构技术就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。
由于我用的是Java,所以我需要对代码做一些分析,决定如何处理局部变量。如果拥有相应的工具,这
个工作就超级简单了。Smalltalk的确拥有这样的工具:Refactoring Browser。运用这个工具,重构过程非常
轻松,我只需标示出需要重构的代码,在菜单中选择Extract Method,输入新的函数名称,一切就自动搞
定。而且工具决不会像我那样犯下愚蠢可笑的错误。我非常盼望早日出现Java版本的重构工具![1]
现在,我已经把原来的函数分为两块,可以分别处理它们。我不喜欢amountFor内的某些变量名称,现在正是修改它们的时候。
下面是原本的代码:
private double amountFor(Rental each){
double thisAmount = 0;
switch (each.getMovie.getPriceCode){
case Movie.REGULAR:
thisAmount += 2;
if (each.getDaysRented> 2)
thisAmount += (each.getDaysRented- 2) 1.5;
break;
case Movie.NEW_RELEASE:
thisAmount += each.getDaysRented 3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if (each.getDaysRented> 3)
thisAmount += (each.getDaysRented- 3) 1.5;
break;
}
return thisAmount;
}
下面是改名后的代码:
private double amountFor(Rental aRental){
double result = 0;
switch (aRental.getMovie.getPriceCode){
case Movie.REGULAR:
result += 2;
if (aRental.getDaysRented> 2)
result += (aRental.getDaysRented- 2) 1.5;
break;
case Movie.NEW_RELEASE:result += aRental.getDaysRented 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (aRental.getDaysRented> 3)
result += (aRental.getDaysRented- 3) 1.5;
break;
}
return result;
}
改名之后,我需要重新编译并测试,确保没有破坏任何东西。
更改变量名称是值得的行为吗?绝对值得。好的代码应该清楚表达出自己的功能,变量名称是代码清
晰的关键。如果为了提高代码的清晰度,需要修改某些东西的名字,那么就大胆去做吧。只要有良好的查
找替换工具,更改名称并不困难。语言所提供的强类型检查以及你自己的测试机制会指出任何你遗漏的东
西。记住:
任何一个傻瓜都能写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员。
代码应该表现自己的目的,这一点非常重要。阅读代码的时候,我经常进行重构。这样,随着对程序
的理解逐渐加深,我也就不断地把这些理解嵌入代码中,这么一来才不会遗忘我曾经理解的东西。
搬移“金额计算”代码
观察amountFor时,我发现这个函数使用了来自Rental类的信息,却没有使用来自Customer类的信息。
class Customer...
private double amountFor(Rental aRental){
double result = 0;
switch (aRental.getMovie.getPriceCode){
case Movie.REGULAR:
result += 2;
if (aRental.getDaysRented> 2)
result += (aRental.getDaysRented- 2) 1.5;
break;
case Movie.NEW_RELEASE:
result += aRental.getDaysRented 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (aRental.getDaysRented> 3)
result += (aRental.getDaysRented- 3) 1.5;
break;
}
return result;
}这立刻使我怀疑它是否被放错了位置。绝大多数情况下,函数应该放在它所使用的数据的所属对象
内,所以amountFor应该移到Rental类去。为了这么做,我要运用Move Method (142)。首先把代码复制到
Rental类,调整代码使之适应新家,然后重新编译。像下面这样:
class Rental...
double getCharge{
double result = 0;
switch (getMovie.getPriceCode){
case Movie.REGULAR:
result += 2;
if (getDaysRented> 2)
result += (getDaysRented- 2) 1.5;
break;
case Movie.NEW_RELEASE:
result += getDaysRented 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (getDaysRented> 3)
result += (getDaysRented- 3) 1.5;
break;
}
return result;
}
在这个例子里,“适应新家”意味着要去掉参数。此外,我还要在搬移的同时变更函数名称。
现在我可以测试新函数是否正常工作。只要改变Customer.amountFor函数内容,使它委托调用新函数
即可:
class Customer...
private double amountFor(Rental aRental){
return aRental.getCharge;
}
现在我可以编译并测试,看看有没有破坏什么东西。
下一个步骤是找出程序中对于旧函数的所有引用点,并修改它们,让它们改用新函数。下面是原本的
程序:
class Customer...
public String statement{
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements;
String result = Rental Record for + getName+ \n;
while (rentals.hasMoreElements){double thisAmount = 0;
Rental each = (Rental)rentals.nextElement;
thisAmount = amountFor(each);
add frequent renter points
frequentRenterPoints++;
add bonus for a two day new release rental
if ((each.getMovie.getPriceCode== Movie.NEW_RELEASE)
each.getDaysRented> 1)frequentRenterPoints++;
show figures for this rental
result += \t + each.getMovie.getTitle+ \t +
String.valueOf(thisAmount)+ \n;
totalAmount += thisAmount;
}
add footer lines
result += Amount owed is + String.valueOf(totalAmount)+ \n;
result += You earned + String.valueOf(frequentRenterPoints)+
frequent renter points;
return result;
}
本例之中,这个步骤很简单,因为我才刚刚产生新函数,只有一个地方使用了它。一般情况下,你得
在可能运用该函数的所有类中查找一遍。
class Customer
public String statement{
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements;
String result = Rental Record for + getName+ \n;
while (rentals.hasMoreElements){
double thisAmount = 0;
Rental each = (Rental)rentals.nextElement;
thisAmount = each.getCharge;
add frequent renter points
frequentRenterPoints++;
add bonus for a two day new release rental
if ((each.getMovie.getPriceCode== Movie.NEW_RELEASE)
each.getDaysRented> 1)frequentRenterPoints++;
show figures for this rental
result += \t + each.getMovie.getTitle+ \t +
String.valueOf(thisAmount)+ \n;
totalAmount += thisAmount;}
add footer lines
result += Amount owed is + String.valueOf(totalAmount)+ \n;
result += You earned + String.valueOf(frequentRenterPoints)+
frequent renter points;
return result;
}
图1-3 搬移“金额计算”函数后,所有类的状态
做完这些修改之后(图1-3),下一件事就是去掉旧函数。编译器会告诉我是否我漏掉了什么。然后我
进行测试,看看有没有破坏什么东西。
有时候我会保留旧函数,让它调用新函数。如果旧函数是一个public函数,而我又不想修改其他类的接
口,这便是一种有用的手法。
当然我还想对Rental.getCharge做些修改,不过暂时到此为止,让我们回到Customer.statement函数。
public String statement{
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements;
String result = Rental Record for + getName+ \n;
while (rentals.hasMoreElements){
double thisAmount = 0;
Rental each = (Rental)rentals.nextElement;
thisAmount = each.getCharge;
add frequent renter points
frequentRenterPoints++;
add bonus for a two day new release rental
if ((each.getMovie.getPriceCode== Movie.NEW_RELEASE)
each.getDaysRented> 1)frequentRenterPoints++;
show figures for this rental
result += \t + each.getMovie.getTitle+ \t +
String.valueOf(thisAmount)+ \n;
totalAmount += thisAmount;
}
add footer lines
result += Amount owed is + String.valueOf(totalAmount)+ \n;result += You earned + String.valueOf(frequentRenterPoints)+ frequent renter points;
return result;
}
下一件引我注意的事是:thisAmount如今变得多余了。它接受each.get-Charge的执行结果,然后就不
再有任何改变。所以我可以运用Replace Temp with Query (120)把thisAmount除去:
public String statement{
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements;
String result = Rental Record for + getName+ \n;
while (rentals.hasMoreElements){
Rental each = (Rental)rentals.nextElement;
add frequent renter points
frequentRenterPoints++;
add bonus for a two day new release rental
if ((each.getMovie.getPriceCode== Movie.NEW_RELEASE)
each.getDaysRented> 1)frequentRenterPoints++;
show figures for this rental
result += \t + each.getMovie.getTitle+ \t + String.valueOf
(each.getCharge)+ \n;
totalAmount += each.getCharge;
}
add footer lines
result += Amount owed is + String.valueOf(totalAmount)+ \n;
result += You earned + String.valueOf(frequentRenterPoints)+ frequent renter points;
return result;
}
}
做完这份修改,我立刻编译并测试,保证自己没有破坏任何东西。
我喜欢尽量除去这一类临时变量。临时变量往往引发问题,它们会导致大量参数被传来传去,而其实
完全没有这种必要。你很容易跟丢它们,尤其在长长的函数之中更是如此。当然我这么做也需付出性能上
的代价,例如本例的费用就被计算了两次。但是这很容易在Rental类中被优化。而且如果代码有合理的组织
和管理,优化就会有很好的效果。我将在第69页的“重构与性能”一节详谈这个问题。
提炼“常客积分计算”代码
下一步要对“常客积分计算”做类似处理。积分的计算视影片种类而有不同,不过不像收费规则有那么
多变化。看来似乎有理由把积分计算责任放在Rental类身上。首先需要针对“常客积分计算”这部分代码(粗
体部分)运用Extract Method(110)重构手法:
public String statement{
double totalAmount = 0;
int frequentRenterPoints = 0;Enumeration rentals = _rentals.elements;
String result = Rental Record for + getName+ \n;
while (rentals.hasMoreElements){
Rental each = (Rental)rentals.nextElement;
add frequent renter points
frequentRenterPoints++;
add bonus for a two day new release rental
if ((each.getMovie.getPriceCode== Movie.NEW_RELEASE)
each.getDaysRented> 1)frequentRenterPoints++;
show figures for this rental
result += \t + each.getMovie.getTitle+ \t
+ String.valueOf(each.getCharge)+ \n;
totalAmount += each.getCharge;
}
add footer lines
result += Amount owed is + String.valueOf(totalAmount)+ \n;
result += You earned + String.valueOf(frequentRenterPoints)
+ frequent renter points;
return result;
}
}
我们再来看局部变量。这里再一次用到了each,而它可以被当作参数传入新函数中。另一个临时变量
是frequentRenterPoints。本例中,它在被使用之前已经先有初值,但提炼出来的函数并没有读取该值,所以
我们不需要将它当作参数传进去,只需把新函数的返回值累加上去就行了。
我完成了函数的提炼,重新编译并测试,然后做一次搬移,再编译、再测试。重构时最好小步前进,如此一来犯错的几率最小。
class Customer...
public String statement{
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements;
String result = Rental Record for + getName+ \n;
while (rentals.hasMoreElements){
Rental each = (Rental)rentals.nextElement;
frequentRenterPoints += each.getFrequentRenterPoints;
show figures for this rental
result += \t + each.getMovie.getTitle+ \t +
String.valueOf(each.getCharge)+ \n;
totalAmount += each.getCharge;
} add footer lines
result += Amount owed is + String.valueOf(totalAmount)+ \n;
result += You earned + String.valueOf(frequentRenterPoints)+
frequent renter points;
return result;
}
class Rental...
int getFrequentRenterPoints{
if ((getMovie.getPriceCode== Movie.NEW_RELEASE)
getDaysRented> 1)return 2;
else
return 1;
}
我利用重构前后的UML图(图1-4~图1-7)来总结刚才所做的修改。和先前一样,左页是修改前的
图,右页是修改后的图。
图1-4 “常客积分计算”函数被提炼及搬移之前的类图
图1-5 “常客积分计算”函数被提炼及搬移之前的序列图图1-6 “常客积分计算”函数被提炼及搬移之后的类图
图1-7 “常客积分计算”函数被提炼及搬移之后的序列图
去除临时变量
正如我在前面提过的,临时变量可能是个问题。它们只在自己所属的函数中有效,所以它们会助长冗
长而复杂的函数。这里有两个临时变量,两者都是用来从Customer对象相关的Rental对象中获得某个总量。
不论ASCII版或HTML版都需要这些总量。我打算运用Replace Temp with Query (120),并利用查询函数
(query method)来取代totalAmount和frequentRentalPoints这两个临时变量。由于类中的任何函数都可以调
用上述查询函数,所以它能够促成较干净的设计,而减少冗长复杂的函数:
class Customer...
public String statement{
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements;
String result = Rental Record for + getName+ \n;
while (rentals.hasMoreElements){
Rental each = (Rental)rentals.nextElement;
frequentRenterPoints += each.getFrequentRenterPoints; show figures for this rental
result += \t + each.getMovie.getTitle+ \t +
String.valueOf(each.getCharge)+ \n;
totalAmount += each.getCharge;
}
add footer lines
result += Amount owed is + String.valueOf(totalAmount)+ \n;
result += You earned + String.valueOf(frequentRenterPoints)+
frequent renter points;
return result;
}
首先我用Customer 类的getTotalCharge取代totalAmount:
class Customer...
public String statement{
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements;
String result = Rental Record for + getName+ \n;
while (rentals.hasMoreElements){
Rental each = (Rental)rentals.nextElement;
frequentRenterPoints += each.getFrequentRenterPoints;
show figures for this rental
result += \t + each.getMovie.getTitle+ \t +
String.valueOf(each.getCharge)+ \n;
}
add footer lines
result += Amount owed is + String.valueOf(getTotalCharge)+ \n;
result += You earned + String.valueOf(frequentRenterPoints)
+ frequent renter points;
return result;
}
private double getTotalCharge{
double result = 0;
Enumeration rentals = _rentals.elements;
while (rentals.hasMoreElements){
Rental each = (Rental)rentals.nextElement;
result += each.getCharge;
}
return result;
}
这并不是Replace Temp with Query (120)的最简单情况。由于totalAmount在循环内部被赋值,我不得不把循环复制到查询函数中。
重构之后,重新编译并测试,然后以同样手法处理frequentRenterPoints:
class Customer...
public String statement{
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements;
String result = Rental Record for + getName+ \n',while (rentals.hasMoreElements){
Rental each = (Rental)rentals.nextElement;
frequentRenterPoints += each.getFrequentRenterPoints;
show figures for this rental
result += \t + each.getMovie.getTitle+ \t +
String.valueOf(each.getCharge)+ \n;
}
add footer lines
result += Amount owed is + String.valueOf(getTotalCharge)+ \n;
result += You earned + String.valueOf(frequentRenterPoints)+
frequent renter points;
return result;
}
public String statement{
Enumeration rentals = _rentals.elements;
String result = Rental Record for + getName+ \n;
while (rentals.hasMoreElements){
Rental each = (Rental)rentals.nextElement;
show figures for this rental
result += \t + each.getMovie.getTitle+ \t +
String.valueOf(each.getCharge)+ \n;
}
add footer lines
result += Amount owed is + String.valueOf(getTotalCharge)+ \n;
result += You earned + String.valueOf(getTotaIFrequentRenterPoints)+
frequent renter points;
return result;
}
private int getTotaIFrequentRenterPoints{
int result = 0;
Enumeration rentals = _rentals.elements;
while (rentals.hasMoreElements){
Rental each = (Rental)rentals.nextElement;result += each.getFrequentRenterPoints;
}
return result;
}
图1-8~图1-11分别以UML 类图和交互图展示statement重构前后的变化。
图1-8 “总量计算”函数被提炼前的类图
图1-9 “总量计算”函数被提炼前的序列图
图1-10 “总量计算”函数被提炼后的类图图1-11 “总量计算”函数被提炼后的序列图
做完这次重构,有必要停下来思考一下。大多数重构都会减少代码总量,但这次却增加了代码总量,那是因为Java l.1需要大量语句来设置一个累加循环。哪怕只是一个简单的累加循环,每个元素只需一行代
码,外围的支持代码也需要六行之多。这其实是任何程序员都熟悉的习惯写法,但代码数量还是太多
了。[2]
这次重构存在另一个问题,那就是性能。原本代码只执行while循环一次,新版本要执行三次。如果
while循环耗时很多,就可能大大降低程序的性能。单单为了这个原因,许多程序员就不愿进行这个重构动
作。但是请注意我的用词:“如果”和“可能”。除非我进行评测,否则我无法确定循环的执行时间,也无法知
道这个循环是否被经常使用以至于影响系统的整体性能。重构时你不必担心这些,优化时你才需要担心它
们,但那时候你已处于一个比较有利的位置,有更多选择可以完成有效优化(见第69页的讨论)。
现在,Customer类内的任何代码都可以调用这些查询函数了。如果系统其他部分需要这些信息,也可
以轻松地将查询函数加入Customer类接口。如果没有这些查询函数,其他函数就必须了解Rental类,并自行
建立循环。在一个复杂系统中,这将使程序的编写难度和维护难度大大增加。
你可以很明显看出来,htmlStatement和statement是不同的。现在,我应该脱下“重构”的帽子,戴
上“添加功能”的帽子。我可以像下面这样编写html-Statement,并添加相应测试:
public String htmlStatement{
Enumeration rentals = _rentals.elements;
String result =
Rentals for + getName+ \n;
while (rentals.hasMoreElements){
Rental each = (Rental)rentals.nextElement;
show figures for each rentalresult += each.getMovie.getTitle+ : +
String.valueOf(each.getCharge)+
\n;
}
add footer lines
result +=
You owe + String.valueOf(getTotalCharge)+
\n;
result += On this rental you earned +
String.valueOf(getTotalFrequentRenterPoints)
+ frequent renter points;
return result;
}
通过计算逻辑的提炼,我可以完成一个htmlStatement,并复用原本state-ment内的所有计算。我不必
剪剪贴贴,所以如果计算规则发生改变,我只需在程序中做一处修改。完成其他任何类型的详单也都很快
而且很容易。这次重构并没有花很多时间,其中大半时间我用来弄清楚代码所做的事,而这是我无论如何
都得做的。
前面有些代码是从ASCII版本中复制过来的——主要是循环设置部分。更深入的重构动作可以清除这些
重复代码。我可以把处理表头(header)、表尾(footer)和详单细目的代码都分别提炼出来。在Form
Template Method (345)实例中,你可以看到如何做这些动作。但是,现在用户又开始嘀咕了,他们准备修改
影片分类规则。我们尚未清楚他们想怎么做,但似乎新分类法很快就要引入,现有的分类法马上就要变
更。与之相应的费用计算方式和常客积分计算方式都还有待决定,现在就对程序做修改,肯定是愚蠢的。
我必须进入费用计算和常客积分计算中,把因条件而异的代码[3]替换掉,这样才能为将来的改变镀上一层
保护膜。现在,请重新戴回“重构”这顶帽子。
1.4 运用多态取代与价格相关的条件逻辑
这个问题的第一部分是switch语句。最好不要在另一个对象的属性基础上运用switch语句。如果不得不
使用,也应该在对象自己的数据上使用,而不是在别人的数据上使用。
class Rental...
double getCharge{
double result = 0;
switch (getMovie.getPriceCode){
case Movie.REGULAR:
result += 2;
if (getDaysRented> 2)
result += (getDaysRented- 2) 1.5;
break;
case Movie.NEW_RELEASE:
result += getDaysRented 3;
break;case Movie.CHILDRENS:
result += 1.5;
if (getDaysRented> 3)
result += (getDaysRented- 3) 1.5;
break;
}
return result;
}
这暗示getCharge应该移到Movie类里去:
class Movie...
double getCharge(int daysRented){
double result = 0;
switch (getPriceCode){
case Movie.REGULAR:
result += 2;
if (daysRented > 2)
result += (daysRented - 2) 1.5;
break;
case Movie.NEW_RELEASE:
result += daysRented 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (daysRented > 3)
result += (daysRented - 3) 1.5;
break;
}
return result;
}
为了让它得以运作,我必须把租期长度作为参数传递进去。当然,租期长度来自Rental对象。计算费用
时需要两项数据:租期长度和影片类型。为什么我选择将租期长度传给Movie对象,而不是将影片类型传给
Rental对象呢?因为本系统可能发生的变化是加入新影片类型,这种变化带有不稳定倾向。如果影片类型有
所变化,我希望尽量控制它造成的影响,所以选择在Movie对象内计算费用。
我把上述计费方法放进Movie类,然后修改Rental的getCharge,让它使用这个新函数(图1-12和图1-
13):
class Rental...
double getCharge{
return _movie.getCharge(_daysRented);
}
搬移getCharge之后,我以相同手法处理常客积分计算。这样我就把根据影片类型而变化的所有东西,都放到了影片类型所属的类中。以下是重构前的代码:
class Rental...
int getFrequentRenterPoints{
if ((getMovie.getPriceCode== Movie.NEW_RELEASE) getDaysRented> 1)
return 2;
else
return 1;
}
图1-12 本节所讨论的两个函数被移到Movie类内之前系统的类图
重构后的代码如下:
class Rental...
int getFrequentRenterPoints{
return _movie.getFrequentRenterPoints(_daysRented);
}
class Movie...
int getFrequentRenterPoints(int daysRented){
if ((getPriceCode== Movie.NEW_RELEASE) daysRented > 1)
return 2;
else
return 1;
}图1-13 本节所讨论的两个函数被移到Movie类内之后系统的类图
终于……我们来到继承
我们有数种影片类型,它们以不同的方式回答相同的问题。这听起来很像子类的工作。我们可以建立
Movie的三个子类,每个都有自己的计费法(图1-14)。
图1-14 以继承机制表现不同的影片类型
这么一来,我就可以用多态来取代switch语句了。很遗憾的是这里有个小问题,不能这么干。一部影片
可以在生命周期内修改自己的分类,一个对象却不能在生命周期内修改自己所属的类。不过还是有一个解
决方法:State模式 [Gang of Four]。运用它之后,我们的类看起来像图1-15。图1-15 运用State模式表现不同的影片
加入这一层间接性,我们就可以在Price对象内进行子类化动作[4],于是便可在任何必要时刻修改价
格。
如果你很熟悉GoF(Gang of Four,四巨头)[5]所列的各种模式,可能会问:“这是一个State,还是一个
Strategy?”答案取决于Price类究竟代表计费方式(此时我喜欢把它叫做Pricer还PricingStrategy),还是代表
影片的某个状态(例如“Star Trek X是一部新片”)。在这个阶段,对于模式(和其名称)的选择反映出你对
结构的想法。此刻我把它视为影片的某种状态。如果未来我觉得Strategy能更好地说明我的意图,我会再重
构它,修改名字,以形成Strategy。
为了引入State模式,我使用三个重构手法。首先运用Replace Type Code with StateStrategy (227),将与
类型相关的行为搬移至State模式内。然后运用Move Method (142)将switch语句移到Price类。最后运用
Replace Conditional with Polymorphism (255)去掉switch语句。
首先我要使用Replace Type Code with StateStrategy (227)。第一步骤是针对类型代码使用Self
Encapsulate Field (171),确保任何时候都通过取值函数和设值函数来访问类型代码。多数访问操作来自其他
类,它们已经在使用取值函数。但构造函数仍然直接访问价格代码[6]:
class Movie...
public Movie(String title,int priceCode){
_title= title;
_priceCode = priceCode;
}
我可以用一个设值函数来代替:
class Movie
public Movie(String title,int priceCode){
_title = title;
setPriceCode(priceCode);
}
然后编译并测试,确保没有破坏任何东西。现在我新建一个Price类,并在其中提供类型相关的行为。
为了实现这一点,我在Price类内加入一个抽象函数,并在所有子类中加上对应的具体函数:abstract class Price {
abstract int getPriceCode;
}
class ChildrensPrice extends Price {
int getPriceCode{
return Movie.CHILDRENS;
}
}
class NewReleasePrice extends Price {
int getPriceCode{
return Movie.NEW_RELEASE;
}
}
class RegularPrice extends Price {
int getPriceCode{
return Movie.REGULAR;
}
}
然后就可以编译这些新建的类了。
现在,我需要修改Movie类内的“价格代号”访问函数(取值函数设值函数,如下),让它们使用新类。
下面是重构前的样子:
public int getPriceCode{
return _priceCode;
}
public setPriceCode(int arg){
_priceCode = arg;
}
private int _priceCode;
这意味着我必须在Movie类内保存一个Price对象,而不再是保存一个_price-Code变量。此外我还需要修
改访问函数:
class Movie...
public int getPriceCode{
return _price.getPriceCode;
}
public void setPriceCode(int arg){
switch (arg){
case REGULAR:
_price = new RegularPrice;
break;
case CHILDRENS:_price = new ChildrensPrice;
break;
case NEW_RELEASE:
_price = new NewReleasePrice;
break;
default:
throw new IllegalArgumentException(Incorrect Price Code);
}
}
private Price _price;
现在我可以重新编译并测试,那些比较复杂的函数根本不知道世界已经变了个样儿。
现在我要对getCharge实施Move Method (142)。下面是重构前的代码:
class Movie...
double getCharge(int daysRented){
double result = 0;
switch (getPriceCode){
case Movie.REGULAR:
result += 2;
if (daysRented > 2)
result += (daysRented - 2) 1.5;
break;
case Movie.NEW_RELEASE:
result += daysRented 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (daysRented > 3)
result += (daysRented - 3) 1.5;
break;
}
return result;
}
搬移动作很简单。下面是重构后的代码:
class Movie...
double getCharge(int daysRented){
return _price.getCharge(daysRented);
}
class Price...
double getCharge(int daysRented){
double result = 0;switch (getPriceCode){
case Movie.REGULAR:
result += 2;
if (daysRented > 2)
result += (daysRented - 2) 1.5;
break;
case Movie.NEW_RELEASE:
result += daysRented 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (daysRented > 3)
result += (daysRented - 3) 1.5;
break;
}
return result;
}
搬移之后,我就可以开始运用Replace Conditional with Polymorphism (255)了。
下面是重构前的代码:
class Price...
double getCharge(int daysRented){
double result = 0;
switch (getPriceCode){
case Movie.REGULAR:
result += 2;
if (daysRented > 2)
result += (daysRented - 2) 1.5;
break;
case Movie.NEW_RELEASE:
result += daysRented 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (daysRented > 3)
result += (daysRented - 3) 1.5;
break;
}
return result;
}
我的做法是一次取出一个case分支,在相应的类建立一个覆盖函数。先从RegularPrice开始:class RegularPrice...
double getCharge(int daysRented){
double result = 2;
if (daysRented > 2)
result += (daysRented - 2) 1.5;
return result;
}
这个函数覆盖了父类中的case语句,而我暂时还把后者留在原处不动。现在编译并测试,然后取出下一
个case分支,再编译并测试。(为了保证被执行的确实是子类中的代码,我喜欢故意丢一个错误进去,然后
让它运行,让测试失败。噢,我是不是有点太偏执了?)
class ChildrensPrice
double getCharge(int daysRented){
double result = 1.5;
if (daysRented > 3)
result += (daysRented - 3) 1.5;
return result;
}
class NewReleasePrice...
double getCharge(int daysRented){
return daysRented 3;
}
处理完所有case分支之后,我就把Price.getCharge声明为abstract:
class Price...
abstract double getCharge(int daysRented);
现在我可以运用同样手法处理getFrequentRenterPoints。重构前的样子如下[7]:
class Movie...
int getFrequentRenterPoints(int daysRented){
if ((getPriceCode== Movie.NEW_RELEASE) daysRented > 1)
return 2;
else
return 1;
}
首先我把这个函数移到Price类:
class Movie...
int getFrequentRenterPoints(int daysRented){
return _price.getFrequentRenterPoints(daysRented);
}
class Price...
int getFrequentRenterPoints(int daysRented){
if ((getPriceCode== Movie.NEW_RELEASE) daysRented > 1)return 2;
else
return 1;
}
但是这一次我不把超类函数声明为abstract。我只是为新片类型增加一个覆写函数,并在超类内留下一
个已定义的函数,使它成为一种默认行为。
class NewReleasePrice
int getFrequentRenterPoints(int daysRented){
return (daysRented > 1)? 2 : 1;
}
class Price...
int getFrequentRenterPoints(int daysRented){
return 1;
}
引入State模式花了我不少力气,值得吗?这么做的收获是:如果我要修改任何与价格有关的行为,或
是添加新的定价标准,或是加入其他取决于价格的行为,程序的修改会容易得多。这个程序的其余部分并
不知道我运用了State模式。对于我目前拥有的这么几个小量行为来说,任何功能或特性上的修改也许都不
合算,但如果在一个更复杂的系统中,有十多个与价格相关的函数,程序的修改难易度就会有很大的区
别。以上所有修改都是小步骤进行,进度似乎太过缓慢,但是我一次都没有打开过调试器,所以整个过程
实际上很快就过去了。我写本章文字所用的时间,远比修改那些代码的时间多得多。
现在我已经完成了第二个重要的重构行为。从此,修改影片分类结构,或是改变费用计算规则、改变
常客积分计算规则,都容易多了。图1-16和图1-17描述State模式对于价格信息所起的作用。
图1-16 加入State模式后的交互图图1-17 加入State模式后的类图
1.5 结语
这是一个简单的例子,但我希望它能让你对于“重构怎么做”有一点感觉。例中我已经示范了数个重构
手法,包括Extract Method (110)、Move Method (142)、Replace Conditional with Polymorphism (255)、Self
Encapsulate Field (171)、Replace Type Code with StateStrategy (227)。所有这些重构行为都使责任的分配更合
理,代码的维护更轻松。重构后的程序风格,将迥异于过程化风格——后者也许是某些人习惯的风格。不
过一旦你习惯了这种重构后的风格,就很难再满足于结构化风格了。
这个例子给我们最大的启发是重构的节奏:测试、小修改、测试、小修改、测试、小修改……正是这
种节奏让重构得以快速而安全地前进。
如果你看懂了前面的例子,就应该已经理解重构是怎么回事了。现在,让我们了解一些背景、原理和
理论(好在不太多)。
[1].本书写作于1999年。十年之后,各种主要的Java IDE都已经提供了良好的重构支持。——译者注[2].十年之后的今天,Java在这方面已经有所改进。——译者注
[3].指的是switch语句内的case子句。——译者注
[4].如图1-15。——译者注
[5].Ralph Johnson和另外三位先生Erich Gamma、Richard Helm、John Vlissides合写了软件开发界驰名的《设
计模式》,人称四巨头(Gang of Four)。——译者注
[6].程序中的_priceCode。——译者注
[7].其中有类型相关的行为,也就是“判断是否为新片”那个动作。——译者注第2章 重构原则
前面所举的例子应该已经让你对重构有了一个良好的感受。现在,我们应该回头看看重构的关键原
则,以及重构时需要考虑的某些问题。
2.1 何谓重构
我总是不太喜欢下定义,因为每个人对每样东西都有自己的定义。但是既然在写书,总得选择自己满
意的定义。在重构这个概念上,我的定义以Ralph Johnson团队和其他相关研究成果为基础。
首先要说明的是:视上下文不同,“重构”这个词有两种不同的定义。你可能会觉得这挺烦人的(我就
是这么想的),不过处理自然语言本来就是件烦人的事,这只不过是又一个实例而已。
第一个定义是名词形式。重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行
为的前提下,提高其可理解性,降低其修改成本。
你可以在后续章节中找到许多重构范例,诸如Extract Method (110)和Pull Up Field (320),等等。一般而
言,重构都是对软件的小改动,但重构之中还可以包含另一个重构。例如Extract Class (149)通常包含Move
Method (142)和Move Field (146)。
“重构”的另一个用法是动词形式。
重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
所以,在软件开发过程中,你可能会花上数小时进行重构,其间可能用上数十种重构手法。
曾经有人这样问我:“重构就只是整理代码吗?”从某种角度来说,是的。但我认为重构不止于此,因为
它提供了一种更高效且受控的代码整理技术。自从运用重构技术后,我发现自己对代码的整理比以前更有
效率。这是因为我知道该使用哪些重构手法,也知道以怎样的方式使用它们才能够将错误减到最少,而且
在每一个可能出错的地方我都加以测试。
我的定义还需要往两方面扩展。首先,重构的目的是使软件更容易被理解和修改。你可以在软件内部
做很多修改,但必须对软件可观察的外部行为只造成很小变化,或甚至不造成变化。与之形成对比的是性
能优化。和重构一样,性能优化通常不会改变组件的行为(除了执行速度),只会改变其内部结构。但是
两者出发点不同:性能优化往往使代码较难理解,但为了得到所需的性能你不得不那么做。
我要强调的第二点是:重构不会改变软件可观察的行为——重构之后软件功能一如以往。任何用户,不论最终用户或其他程序员,都不知道已经有东西发生了变化。
两顶帽子
上述第二点引出了Kent Beck的“两顶帽子”比喻。使用重构技术开发软件时,你把自己的时间分配给两
种截然不同的行为:添加新功能,以及重构。添加新功能时,你不应该修改既有代码,只管添加新功能。
通过测试(并让测试正常运行),你可以衡量自己的工作进度。重构时你就不能再添加功能,只管改进程
序结构。此时你不应该添加任何测试(除非发现有先前遗漏的东西),只在绝对必要(用以处理接口变
化)时才修改测试。
软件开发过程中,你可能会发现自己经常变换帽子。首先你会尝试添加新功能,然后会意识到:如果
把程序结构改一下,功能的添加会容易得多。于是你换一顶帽子,做一会儿重构工作。程序结构调整好后,你又换上原先的帽子,继续添加新功能。新功能正常工作后,你又发现自己的编码造成程序难以理
解,于是又换上重构帽子……整个过程或许只花十分钟,但无论何时你都应该清楚自己戴的是哪一顶帽
子。
2.2 为何重构
我不想把重构说成是包治百病的万灵丹,它绝对不是所谓的“银弹”。不过它的确很有价值,虽不是一
颗银子弹却是一把“银钳子”,可以帮助你始终良好地控制自己的代码。重构是个工具,它可以(并且应
该)用于以下几个目的。
重构改进软件设计
如果没有重构,程序的设计会逐渐腐败变质。当人们只为短期目的,或是在完全理解整体设计之前,就贸然修改代码,程序将逐渐失去自己的结构,程序员越来越难通过阅读源码而理解原来的设计。重构很
像是在整理代码,你所做的就是让所有东西回到应处的位置上。代码结构的流失是累积性的。越难看出代
码所代表的设计意图,就越难保护其中设计,于是该设计就腐败得越快。经常性的重构可以帮助代码维持
自己该有的形态。
完成同样一件事,设计不良的程序往往需要更多代码,这常常是因为代码在不同的地方使用完全相同
的语句做同样的事。因此改进设计的一个重要方向就是消除重复代码。这个动作的重要性在于方便未来的
修改。代码量减少并不会使系统运行更快,因为这对程序的运行轨迹几乎没有任何明显影响。然而代码量
减少将使未来可能的程序修改动作容易得多。代码越多,正确的修改就越困难,因为有更多代码需要理
解。你在这儿做了点修改,系统却不如预期那样工作,是因为你没有修改另一处——那儿的代码做着几乎
完全一样的事情,只是所处环境略有不同。如果消除重复代码,你就可以确定所有事物和行为在代码中只
表述一次,这正是优秀设计的根本。
重构使软件更容易理解
所谓程序设计,很大程度上就是与计算机交谈:你编写代码告诉计算机做什么事,它的响应则是精确
按照你的指示行动。你得及时填补“想要它做什么”和“告诉它做什么”之间的缝隙。这种编程模式的核心就
是“准确说出我所要的”。除了计算机外,你的源码还有其他读者:几个月之后可能会有另一位程序员尝试
读懂你的代码并做一些修改。我们很容易忘记这第二位读者,但他才是最重要的。计算机是否多花了几个
小时来编译,又有什么关系呢?如果一个程序员花费一周时间来修改某段代码,那才要命呢——如果他理解
了你的代码,这个修改原本只需一小时。
问题在于,当你努力让程序运转的时候,不会想到未来出现的那个开发者。是的,我们应该改变一下
开发节奏,对代码做适当修改,让代码变得更易理解。重构可以帮助我们让代码更易读。一开始进行重构
时,你的代码可以正常运行,但结构不够理想。在重构上花一点点时间,就可以让代码更好地表达自己的
用途。这种编程模式的核心就是“准确说出我所要的”。
关于这一点,我没必要表现得如此无私。很多时候那个未来的开发者就是我自己。此时重构就显得尤
其重要了。我是个很懒惰的程序员,我的懒惰表现形式之一就是:总是记不住自己写过的代码。事实上,对于任何能够立刻查阅的东西,我都故意不去记它,因为我怕把自己的脑袋塞爆。我总是尽量把该记住的
东西写进程序里,这样我就不必记住它了。这么一来我就不必太担心Old Peculier[1][Jackson]杀光我的脑
细胞。
这种可理解性还有另一方面的作用。我利用重构来协助我理解不熟悉的代码。每当看到不熟悉的代码,我必须试着理解其用途。我先看两行代码,然后对自己说:“噢,是的,它做了这些那些……”有了重
构这个强大武器在手,我不会满足于这么一点体会。我会真正动手修改代码,让它更好地反映出我的理
解,然后重新执行,看它是否仍然正常运作,以此检验我的理解是否正确。
一开始我所做的重构都像这样停留在细枝末节上。随着代码渐趋简洁,我发现自己可以看到一些以前
看不到的设计层面的东西。如果不对代码做这些修改,也许我永远看不见它们,因为我的聪明才智不足以
在脑子里把这一切都想象出来。Ralph Johnson把这种“早期重构”描述为“擦掉窗户上的污垢,使你看得更
远”。研究代码时我发现,重构把我带到更高的理解层次上。如果没有重构,我达不到这种层次。
重构帮助找到bug
对代码的理解,可以帮助我找到bug。我承认我不太擅长调试。有些人只要盯着一大段代码就可以找出
里面的bug,我可不行。但我发现,如果对代码进行重构,我就可以深入理解代码的作为,并恰到好处地把
新的理解反馈回去。搞清楚程序结构的同时,我也清楚了自己所做的一些假设,于是想不把bug揪出来都
难。
这让我想起了Kent Beck经常形容自己的一句话:“我不是个伟大的程序员,我只是个有着一些优秀习惯
的好程序员。”重构能够帮助我更有效地写出强健的代码。
重构提高编程速度
终于,前面的一切都归结到了这最后一点:重构帮助你更快速地开发程序。
听起来有点违反直觉。当我谈到重构,人们很容易看出它能够提高质量。改善设计、提升可读性、减
少错误,这些都是提高质量。但这难道不会降低开发速度吗?
我绝对相信:良好的设计是快速开发的根本——事实上,拥有良好设计才可能做到快速开发。如果没
有良好设计,或许某一段时间内你的进展迅速,但恶劣的设计很快就让你的速度慢下来。你会把时间花在
调试上面,无法添加新功能。修改时间越来越长,因为你必须花越来越多的时间去理解系统、寻找重复代
码。随着你给最初程序打上一个又一个的补丁,新特性需要更多代码才能实现。真是个恶性循环。
良好设计是维持软件开发速度的根本。重构可以帮助你更快速地开发软件,因为它阻止系统腐败变
质,它甚至还可以提高设计质量。
2.3 何时重构
当我谈论重构,常常有人问我应该怎样安排重构时间表。我们是不是应该每两个月就专门安排两个星
期来进行重构呢?
几乎任何情况下我都反对专门拨出时间进行重构。在我看来,重构本来就不是一件应该特别拨出时间
做的事情,重构应该随时随地进行。你不应该为重构而重构,你之所以重构,是因为你想做别的什么事,而重构可以帮助你把那些事做好。
三次法则
Don Roberts给了我一条准则:第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如
何还是可以去做;第三次再做类似的事,你就应该重构。
事不过三,三则重构。
添加功能时重构
最常见的重构时机就是我想给软件添加新特性的时候。此时,重构的直接原因往往是为了帮助我理解
需要修改的代码——这些代码可能是别人写的,也可能是我自己写的。无论何时,只要我想理解代码所做的事,我就会问自己:是否能对这段代码进行重构,使我能更快地理解它。然后我就会重构。之所以这么
做,部分原因是为了让我下次再看这段代码时容易理解,但最主要的原因是:如果在前进过程中把代码结
构理清,我就可以从中理解更多东西。
在这里,重构的另一个原动力是:代码的设计无法帮助我轻松添加我所需要的特性。我看着设计,然
后对自己说:“如果用某种方式来设计,添加特性会简单得多。”这种情况下我不会因为自己过去的错误而
懊恼——我用重构来弥补它。之所以这么做,部分原因是为了让未来增加新特性时能够更轻松一些,但最
主要的原因还是:我发现这是最快捷的途径。重构是一个快速流畅的过程,一旦完成重构,新特性的添加
就会更快速、更流畅。
修补错误时重构
调试过程中运用重构,多半是为了让代码更具可读性。当我看着代码并努力理解它的时候,我用重构
帮助加深自己的理解。我发现以这种程序来处理代码,常常能够帮助我找出bug。你可以这么想:如果收到
一份错误报告,这就是需要重构的信号,因为显然代码还不够清晰——没有清晰到让你能一眼看出bug。
复审代码时重构
很多公司都会做常规的代码复审,因为这种活动可以改善开发状况。这种活动有助于在开发团队中传
播知识,也有助于让较有经验的开发者把知识传递给比较欠缺经验的人,并帮助更多人理解大型软件系统
中的更多部分。代码复审对于编写清晰代码也很重要。我的代码也许对我自己来说很清晰,对他人则不
然。这是无法避免的,因为要让开发者设身处地为那些不熟悉自己所做所为的人着想,实在太困难了。代
码复审也让更多人有机会提出有用的建议,毕竟我在一个星期之内能够想出的好点子很有限。如果能得到
别人的帮助,我的生活会滋润得多,所以我总是期待更多复审。
我发现,重构可以帮助我复审别人的代码。开始重构前我可以先阅读代码,得到一定程度的理解,并
提出一些建议。一旦想到一些点子,我就会考虑是否可以通过重构立即轻松地实现它们。如果可以,我就
会动手。这样做了几次以后,我可以把代码看得更清楚,提出更多恰当的建议。我不必想象代码应该是什
么样,我可以“看见”它是什么样。于是我可以获得更高层次的认识。如果不进行重构,我永远无法得到这
样的认识。
重构还可以帮助代码复审工作得到更具体的结果。不仅获得建议,而且其中许多建议能够立刻实现。
最终你将从实践中得到比以往多得多的成就感。
为了让过程正常运转,你的复审团队必须保持精练。就我的经验,最好是一个复审者搭配一个原作
者,共同处理这些代码。复审者提出修改建议,然后两人共同判断这些修改是否能够通过重构轻松实现。
果真能够如此,就一起着手修改。
如果是比较大的设计复审工作,那么在一个较大团队内保留多种观点通常会更好一些。此时直接展示
代码往往不是最佳办法。我喜欢运用UML示意图展现设计,并以CRC卡展示软件情节。换句话说,我会和
某个团队进行设计复审,而和单个复审者进行代码复审。
极限编程[Beck,XP]中的“结对编程”形式,把代码复审的积极性发挥到了极致。一旦采用这种形
式,所有正式开发任务都由两名开发者在同一台机器上进行。这样便在开发过程中形成随时进行的代码复
审工作,而重构也就被包含在开发过程内了。
为什么重构有用
——Kent Beck
程序有两面价值:“今天可以为你做什么”和“明天可以为你做什么”。大多数时候,我们都只关注自己今
天想要程序做什么。不论是修复错误或是添加特性,我们都是为了让程序能力更强,让它在今天更有价
值。但是系统当下的行为,只是整个故事的一部分,如果没有认清这一点,你无法长期从事编程工作。如
果你为求完成今天的任务而不择手段,导致不可能在明天完成明天的任务,那么最终还是会失败。但是,你知道自己今天需要什么,却不一定知道自己明天需要什么。也许你可以猜到明天的需求,也许吧,但肯
定还有些事情出乎你的意料。
对于今天的工作,我了解得很充分;对于明天的工作,我了解得不够充分。但如果我纯粹只是为今天
工作,明天我将完全无法工作。
重构是一条摆脱困境的道路。如果你发现昨天的决定已经不适合今天的情况,放心改变这个决定就
是,然后你就可以完成今天的工作了。明天,喔,明天回头看今天的理解也许觉得很幼稚,那时你还可以
改变你的理解。
是什么让程序如此难以相与? 眼下我能想起下述四个原因,它们是:
难以阅读的程序,难以修改;
逻辑重复的程序,难以修改;
添加新行为时需要修改已有代码的程序,难以修改;
带复杂条件逻辑的程序,难以修改。
因此,我们希望程序:(1)容易阅读;(2)所有逻辑都只在唯一地点指定;(3)新的改动不会危及现有行
为;(4)尽可能简单表达条件逻辑。
重构是这样一个过程:它在一个目前可运行的程序上进行,在不改变程序行为的前提下使其具备上述
美好性质,使我们能够继续保持高速开发,从而增加程序的价值。
2.4 怎么对经理说
“该怎么跟经理说重构的事?”这是我最常被问到的一个问题。如果这位经理懂技术,那么向他介绍重构
应该不会很困难。如果这位经理只对质量感兴趣,那么问题就集中到了“质量”上面。此时,在复审过程中
使用重构就是一个不错的办法。大量研究结果显示,技术复审是减少错误、提高开发速度的一条重要途
径。随便找一本关于复审、审查或软件开发程序的书看看,从中找些最新引证,应该可以让大多数经理认
识复审的价值。然后你就可以把重构当作“将复审意见引入代码内”的方法来使用,这很容易。
当然,很多经理嘴巴上说自己“质量驱动”,其实更多是“进度驱动”。这种情况下我会给他们一个较有争
议的建议:不要告诉经理!
这是在搞破坏吗?我不这样想。软件开发者都是专业人士。我们的工作就是尽可能快速创造出高效软
件。我的经验告诉我,对于快速创造软件,重构可带来巨大帮助。如果需要添加新功能,而原本设计却又
使我无法方便地修改,我发现先重构再添加新功能会更快些。如果要修补错误,就得先理解软件的工作方
式,而我发现重构是理解软件的最快方式。受进度驱动的经理要我尽可能快速完事,至于怎么完成,那就
是我的事了。我认为最快的方式就是重构,所以我就重构。
间接层和重构
——Kent Beck
“计算机科学是这样一门科学:它相信所有问题都可以通过增加一个间接层来解决。”
——Dennis DeBruler
由于软件工程师对间接层如此醉心,你应该不会惊讶大多数重构都为程序引入了更多间接层。重构往
往把大型对象拆成多个小型对象,把大型函数拆成多个小型函数。但是,间接层是一柄双刃剑。每次把一个东西分成两份,你就需要多管理一个东西。如果某个对象委
托另一对象,后者又委托另一对象,程序会愈加难以阅读。
基于这个观点,你会希望尽量减少间接层。
别急,伙计!间接层有它的价值。下面就是间接层的某些价值。
允许逻辑共享。比如说一个子函数在两个不同的地点被调用,或超类中的某个函数被所有子类共享。
分开解释意图和实现。你可以选择每个类和函数的名字,这给了你一个解释自己意图的机会。类或函
数内部则解释实现这个意图的做法。如果类和函数内部又以更小单元的意图来编写,你所写的代码就可以
描述其结构中的大部分重要信息。
隔离变化。很可能我在两个不同地点使用同一对象,其中一个地点我想改变对象行为,但如果修改了
它,我就要冒同时影响两处的风险。为此我做出一个子类,并在需要修改处引用这个子类。现在,我可以
修改这个子类而不必承担无意中影响另一处的风险。
封装条件逻辑。对象有一种奇妙的机制:多态消息,可以灵活而清晰地表达条件逻辑。将条件逻辑转
化为消息形式,往往能降低代码的重复、增加清晰度并提高弹性。
这就是重构游戏:在保持系统现有行为的前提下,如何才能提高系统的质量或降低其成本,从而使它
更有价值?
这个游戏中最常见的变量就是:你如何看待你自己的程序。找出一个缺乏“间接层利益”之处,在不修
改现有行为的前提下,为它加入一个间接层。现在你获得了一个更有价值的程序,因为它有较高的质量,让我们在明天(未来)受益。
请将这种方法与“小心翼翼的事前设计”做个比较。推测性设计总是试图在任何一行代码诞生之前就先
让系统拥有所有优秀质量,然后程序员将代码塞进这个强健的骨架中就行了。这个过程的问题在于:太容
易猜错。如果运用重构,你就永远不会面临全盘错误的危险。程序自始至终都能保持一致的行为,而你又
有机会为程序添加更多价值不菲的质量。
还有一种比较少见的重构游戏:找出不值得的间接层,并将它拿掉。这种间接层常以中介函数形式出
现,它也许曾经有过贡献,但芳华已逝。它也可能是个组件,你本来期望在不同地点共享它,或让它表现
出多态性,最终却只在一处用到。如果你找到这种“寄生式间接层”,请把它扔掉。如此一来你会获得一个
更有价值的程序,不是因为它取得了更多的优秀质量,而 ......
while (rentals.hasMoreElements){
Rental each = (Rental)rentals.nextElement;
show figures for each rentalresult += each.getMovie.getTitle+ : +
String.valueOf(each.getCharge)+
\n;
}
add footer lines
result +=
\n; ;
result += On this rental you earned +
String.valueOf(getTotalFrequentRenterPoints)
+ frequent renter points
return result;
}
通过计算逻辑的提炼,我可以完成一个htmlStatement,并复用原本state-ment内的所有计算。我不必
剪剪贴贴,所以如果计算规则发生改变,我只需在程序中做一处修改。完成其他任何类型的详单也都很快
而且很容易。这次重构并没有花很多时间,其中大半时间我用来弄清楚代码所做的事,而这是我无论如何
都得做的。
前面有些代码是从ASCII版本中复制过来的——主要是循环设置部分。更深入的重构动作可以清除这些
重复代码。我可以把处理表头(header)、表尾(footer)和详单细目的代码都分别提炼出来。在Form
Template Method (345)实例中,你可以看到如何做这些动作。但是,现在用户又开始嘀咕了,他们准备修改
影片分类规则。我们尚未清楚他们想怎么做,但似乎新分类法很快就要引入,现有的分类法马上就要变
更。与之相应的费用计算方式和常客积分计算方式都还有待决定,现在就对程序做修改,肯定是愚蠢的。
我必须进入费用计算和常客积分计算中,把因条件而异的代码[3]替换掉,这样才能为将来的改变镀上一层
保护膜。现在,请重新戴回“重构”这顶帽子。
1.4 运用多态取代与价格相关的条件逻辑
这个问题的第一部分是switch语句。最好不要在另一个对象的属性基础上运用switch语句。如果不得不
使用,也应该在对象自己的数据上使用,而不是在别人的数据上使用。
class Rental...
double getCharge{
double result = 0;
switch (getMovie.getPriceCode){
case Movie.REGULAR:
result += 2;
if (getDaysRented> 2)
result += (getDaysRented- 2) 1.5;
break;
case Movie.NEW_RELEASE:
result += getDaysRented 3;
break;case Movie.CHILDRENS:
result += 1.5;
if (getDaysRented> 3)
result += (getDaysRented- 3) 1.5;
break;
}
return result;
}
这暗示getCharge应该移到Movie类里去:
class Movie...
double getCharge(int daysRented){
double result = 0;
switch (getPriceCode){
case Movie.REGULAR:
result += 2;
if (daysRented > 2)
result += (daysRented - 2) 1.5;
break;
case Movie.NEW_RELEASE:
result += daysRented 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (daysRented > 3)
result += (daysRented - 3) 1.5;
break;
}
return result;
}
为了让它得以运作,我必须把租期长度作为参数传递进去。当然,租期长度来自Rental对象。计算费用
时需要两项数据:租期长度和影片类型。为什么我选择将租期长度传给Movie对象,而不是将影片类型传给
Rental对象呢?因为本系统可能发生的变化是加入新影片类型,这种变化带有不稳定倾向。如果影片类型有
所变化,我希望尽量控制它造成的影响,所以选择在Movie对象内计算费用。
我把上述计费方法放进Movie类,然后修改Rental的getCharge,让它使用这个新函数(图1-12和图1-
13):
class Rental...
double getCharge{
return _movie.getCharge(_daysRented);
}
搬移getCharge之后,我以相同手法处理常客积分计算。这样我就把根据影片类型而变化的所有东西,都放到了影片类型所属的类中。以下是重构前的代码:
class Rental...
int getFrequentRenterPoints{
if ((getMovie.getPriceCode== Movie.NEW_RELEASE) getDaysRented> 1)
return 2;
else
return 1;
}
图1-12 本节所讨论的两个函数被移到Movie类内之前系统的类图
重构后的代码如下:
class Rental...
int getFrequentRenterPoints{
return _movie.getFrequentRenterPoints(_daysRented);
}
class Movie...
int getFrequentRenterPoints(int daysRented){
if ((getPriceCode== Movie.NEW_RELEASE) daysRented > 1)
return 2;
else
return 1;
}图1-13 本节所讨论的两个函数被移到Movie类内之后系统的类图
终于……我们来到继承
我们有数种影片类型,它们以不同的方式回答相同的问题。这听起来很像子类的工作。我们可以建立
Movie的三个子类,每个都有自己的计费法(图1-14)。
图1-14 以继承机制表现不同的影片类型
这么一来,我就可以用多态来取代switch语句了。很遗憾的是这里有个小问题,不能这么干。一部影片
可以在生命周期内修改自己的分类,一个对象却不能在生命周期内修改自己所属的类。不过还是有一个解
决方法:State模式 [Gang of Four]。运用它之后,我们的类看起来像图1-15。图1-15 运用State模式表现不同的影片
加入这一层间接性,我们就可以在Price对象内进行子类化动作[4],于是便可在任何必要时刻修改价
格。
如果你很熟悉GoF(Gang of Four,四巨头)[5]所列的各种模式,可能会问:“这是一个State,还是一个
Strategy?”答案取决于Price类究竟代表计费方式(此时我喜欢把它叫做Pricer还PricingStrategy),还是代表
影片的某个状态(例如“Star Trek X是一部新片”)。在这个阶段,对于模式(和其名称)的选择反映出你对
结构的想法。此刻我把它视为影片的某种状态。如果未来我觉得Strategy能更好地说明我的意图,我会再重
构它,修改名字,以形成Strategy。
为了引入State模式,我使用三个重构手法。首先运用Replace Type Code with StateStrategy (227),将与
类型相关的行为搬移至State模式内。然后运用Move Method (142)将switch语句移到Price类。最后运用
Replace Conditional with Polymorphism (255)去掉switch语句。
首先我要使用Replace Type Code with StateStrategy (227)。第一步骤是针对类型代码使用Self
Encapsulate Field (171),确保任何时候都通过取值函数和设值函数来访问类型代码。多数访问操作来自其他
类,它们已经在使用取值函数。但构造函数仍然直接访问价格代码[6]:
class Movie...
public Movie(String title,int priceCode){
_title= title;
_priceCode = priceCode;
}
我可以用一个设值函数来代替:
class Movie
public Movie(String title,int priceCode){
_title = title;
setPriceCode(priceCode);
}
然后编译并测试,确保没有破坏任何东西。现在我新建一个Price类,并在其中提供类型相关的行为。
为了实现这一点,我在Price类内加入一个抽象函数,并在所有子类中加上对应的具体函数:abstract class Price {
abstract int getPriceCode;
}
class ChildrensPrice extends Price {
int getPriceCode{
return Movie.CHILDRENS;
}
}
class NewReleasePrice extends Price {
int getPriceCode{
return Movie.NEW_RELEASE;
}
}
class RegularPrice extends Price {
int getPriceCode{
return Movie.REGULAR;
}
}
然后就可以编译这些新建的类了。
现在,我需要修改Movie类内的“价格代号”访问函数(取值函数设值函数,如下),让它们使用新类。
下面是重构前的样子:
public int getPriceCode{
return _priceCode;
}
public setPriceCode(int arg){
_priceCode = arg;
}
private int _priceCode;
这意味着我必须在Movie类内保存一个Price对象,而不再是保存一个_price-Code变量。此外我还需要修
改访问函数:
class Movie...
public int getPriceCode{
return _price.getPriceCode;
}
public void setPriceCode(int arg){
switch (arg){
case REGULAR:
_price = new RegularPrice;
break;
case CHILDRENS:_price = new ChildrensPrice;
break;
case NEW_RELEASE:
_price = new NewReleasePrice;
break;
default:
throw new IllegalArgumentException(Incorrect Price Code);
}
}
private Price _price;
现在我可以重新编译并测试,那些比较复杂的函数根本不知道世界已经变了个样儿。
现在我要对getCharge实施Move Method (142)。下面是重构前的代码:
class Movie...
double getCharge(int daysRented){
double result = 0;
switch (getPriceCode){
case Movie.REGULAR:
result += 2;
if (daysRented > 2)
result += (daysRented - 2) 1.5;
break;
case Movie.NEW_RELEASE:
result += daysRented 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (daysRented > 3)
result += (daysRented - 3) 1.5;
break;
}
return result;
}
搬移动作很简单。下面是重构后的代码:
class Movie...
double getCharge(int daysRented){
return _price.getCharge(daysRented);
}
class Price...
double getCharge(int daysRented){
double result = 0;switch (getPriceCode){
case Movie.REGULAR:
result += 2;
if (daysRented > 2)
result += (daysRented - 2) 1.5;
break;
case Movie.NEW_RELEASE:
result += daysRented 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (daysRented > 3)
result += (daysRented - 3) 1.5;
break;
}
return result;
}
搬移之后,我就可以开始运用Replace Conditional with Polymorphism (255)了。
下面是重构前的代码:
class Price...
double getCharge(int daysRented){
double result = 0;
switch (getPriceCode){
case Movie.REGULAR:
result += 2;
if (daysRented > 2)
result += (daysRented - 2) 1.5;
break;
case Movie.NEW_RELEASE:
result += daysRented 3;
break;
case Movie.CHILDRENS:
result += 1.5;
if (daysRented > 3)
result += (daysRented - 3) 1.5;
break;
}
return result;
}
我的做法是一次取出一个case分支,在相应的类建立一个覆盖函数。先从RegularPrice开始:class RegularPrice...
double getCharge(int daysRented){
double result = 2;
if (daysRented > 2)
result += (daysRented - 2) 1.5;
return result;
}
这个函数覆盖了父类中的case语句,而我暂时还把后者留在原处不动。现在编译并测试,然后取出下一
个case分支,再编译并测试。(为了保证被执行的确实是子类中的代码,我喜欢故意丢一个错误进去,然后
让它运行,让测试失败。噢,我是不是有点太偏执了?)
class ChildrensPrice
double getCharge(int daysRented){
double result = 1.5;
if (daysRented > 3)
result += (daysRented - 3) 1.5;
return result;
}
class NewReleasePrice...
double getCharge(int daysRented){
return daysRented 3;
}
处理完所有case分支之后,我就把Price.getCharge声明为abstract:
class Price...
abstract double getCharge(int daysRented);
现在我可以运用同样手法处理getFrequentRenterPoints。重构前的样子如下[7]:
class Movie...
int getFrequentRenterPoints(int daysRented){
if ((getPriceCode== Movie.NEW_RELEASE) daysRented > 1)
return 2;
else
return 1;
}
首先我把这个函数移到Price类:
class Movie...
int getFrequentRenterPoints(int daysRented){
return _price.getFrequentRenterPoints(daysRented);
}
class Price...
int getFrequentRenterPoints(int daysRented){
if ((getPriceCode== Movie.NEW_RELEASE) daysRented > 1)return 2;
else
return 1;
}
但是这一次我不把超类函数声明为abstract。我只是为新片类型增加一个覆写函数,并在超类内留下一
个已定义的函数,使它成为一种默认行为。
class NewReleasePrice
int getFrequentRenterPoints(int daysRented){
return (daysRented > 1)? 2 : 1;
}
class Price...
int getFrequentRenterPoints(int daysRented){
return 1;
}
引入State模式花了我不少力气,值得吗?这么做的收获是:如果我要修改任何与价格有关的行为,或
是添加新的定价标准,或是加入其他取决于价格的行为,程序的修改会容易得多。这个程序的其余部分并
不知道我运用了State模式。对于我目前拥有的这么几个小量行为来说,任何功能或特性上的修改也许都不
合算,但如果在一个更复杂的系统中,有十多个与价格相关的函数,程序的修改难易度就会有很大的区
别。以上所有修改都是小步骤进行,进度似乎太过缓慢,但是我一次都没有打开过调试器,所以整个过程
实际上很快就过去了。我写本章文字所用的时间,远比修改那些代码的时间多得多。
现在我已经完成了第二个重要的重构行为。从此,修改影片分类结构,或是改变费用计算规则、改变
常客积分计算规则,都容易多了。图1-16和图1-17描述State模式对于价格信息所起的作用。
图1-16 加入State模式后的交互图图1-17 加入State模式后的类图
1.5 结语
这是一个简单的例子,但我希望它能让你对于“重构怎么做”有一点感觉。例中我已经示范了数个重构
手法,包括Extract Method (110)、Move Method (142)、Replace Conditional with Polymorphism (255)、Self
Encapsulate Field (171)、Replace Type Code with StateStrategy (227)。所有这些重构行为都使责任的分配更合
理,代码的维护更轻松。重构后的程序风格,将迥异于过程化风格——后者也许是某些人习惯的风格。不
过一旦你习惯了这种重构后的风格,就很难再满足于结构化风格了。
这个例子给我们最大的启发是重构的节奏:测试、小修改、测试、小修改、测试、小修改……正是这
种节奏让重构得以快速而安全地前进。
如果你看懂了前面的例子,就应该已经理解重构是怎么回事了。现在,让我们了解一些背景、原理和
理论(好在不太多)。
[1].本书写作于1999年。十年之后,各种主要的Java IDE都已经提供了良好的重构支持。——译者注[2].十年之后的今天,Java在这方面已经有所改进。——译者注
[3].指的是switch语句内的case子句。——译者注
[4].如图1-15。——译者注
[5].Ralph Johnson和另外三位先生Erich Gamma、Richard Helm、John Vlissides合写了软件开发界驰名的《设
计模式》,人称四巨头(Gang of Four)。——译者注
[6].程序中的_priceCode。——译者注
[7].其中有类型相关的行为,也就是“判断是否为新片”那个动作。——译者注第2章 重构原则
前面所举的例子应该已经让你对重构有了一个良好的感受。现在,我们应该回头看看重构的关键原
则,以及重构时需要考虑的某些问题。
2.1 何谓重构
我总是不太喜欢下定义,因为每个人对每样东西都有自己的定义。但是既然在写书,总得选择自己满
意的定义。在重构这个概念上,我的定义以Ralph Johnson团队和其他相关研究成果为基础。
首先要说明的是:视上下文不同,“重构”这个词有两种不同的定义。你可能会觉得这挺烦人的(我就
是这么想的),不过处理自然语言本来就是件烦人的事,这只不过是又一个实例而已。
第一个定义是名词形式。重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行
为的前提下,提高其可理解性,降低其修改成本。
你可以在后续章节中找到许多重构范例,诸如Extract Method (110)和Pull Up Field (320),等等。一般而
言,重构都是对软件的小改动,但重构之中还可以包含另一个重构。例如Extract Class (149)通常包含Move
Method (142)和Move Field (146)。
“重构”的另一个用法是动词形式。
重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
所以,在软件开发过程中,你可能会花上数小时进行重构,其间可能用上数十种重构手法。
曾经有人这样问我:“重构就只是整理代码吗?”从某种角度来说,是的。但我认为重构不止于此,因为
它提供了一种更高效且受控的代码整理技术。自从运用重构技术后,我发现自己对代码的整理比以前更有
效率。这是因为我知道该使用哪些重构手法,也知道以怎样的方式使用它们才能够将错误减到最少,而且
在每一个可能出错的地方我都加以测试。
我的定义还需要往两方面扩展。首先,重构的目的是使软件更容易被理解和修改。你可以在软件内部
做很多修改,但必须对软件可观察的外部行为只造成很小变化,或甚至不造成变化。与之形成对比的是性
能优化。和重构一样,性能优化通常不会改变组件的行为(除了执行速度),只会改变其内部结构。但是
两者出发点不同:性能优化往往使代码较难理解,但为了得到所需的性能你不得不那么做。
我要强调的第二点是:重构不会改变软件可观察的行为——重构之后软件功能一如以往。任何用户,不论最终用户或其他程序员,都不知道已经有东西发生了变化。
两顶帽子
上述第二点引出了Kent Beck的“两顶帽子”比喻。使用重构技术开发软件时,你把自己的时间分配给两
种截然不同的行为:添加新功能,以及重构。添加新功能时,你不应该修改既有代码,只管添加新功能。
通过测试(并让测试正常运行),你可以衡量自己的工作进度。重构时你就不能再添加功能,只管改进程
序结构。此时你不应该添加任何测试(除非发现有先前遗漏的东西),只在绝对必要(用以处理接口变
化)时才修改测试。
软件开发过程中,你可能会发现自己经常变换帽子。首先你会尝试添加新功能,然后会意识到:如果
把程序结构改一下,功能的添加会容易得多。于是你换一顶帽子,做一会儿重构工作。程序结构调整好后,你又换上原先的帽子,继续添加新功能。新功能正常工作后,你又发现自己的编码造成程序难以理
解,于是又换上重构帽子……整个过程或许只花十分钟,但无论何时你都应该清楚自己戴的是哪一顶帽
子。
2.2 为何重构
我不想把重构说成是包治百病的万灵丹,它绝对不是所谓的“银弹”。不过它的确很有价值,虽不是一
颗银子弹却是一把“银钳子”,可以帮助你始终良好地控制自己的代码。重构是个工具,它可以(并且应
该)用于以下几个目的。
重构改进软件设计
如果没有重构,程序的设计会逐渐腐败变质。当人们只为短期目的,或是在完全理解整体设计之前,就贸然修改代码,程序将逐渐失去自己的结构,程序员越来越难通过阅读源码而理解原来的设计。重构很
像是在整理代码,你所做的就是让所有东西回到应处的位置上。代码结构的流失是累积性的。越难看出代
码所代表的设计意图,就越难保护其中设计,于是该设计就腐败得越快。经常性的重构可以帮助代码维持
自己该有的形态。
完成同样一件事,设计不良的程序往往需要更多代码,这常常是因为代码在不同的地方使用完全相同
的语句做同样的事。因此改进设计的一个重要方向就是消除重复代码。这个动作的重要性在于方便未来的
修改。代码量减少并不会使系统运行更快,因为这对程序的运行轨迹几乎没有任何明显影响。然而代码量
减少将使未来可能的程序修改动作容易得多。代码越多,正确的修改就越困难,因为有更多代码需要理
解。你在这儿做了点修改,系统却不如预期那样工作,是因为你没有修改另一处——那儿的代码做着几乎
完全一样的事情,只是所处环境略有不同。如果消除重复代码,你就可以确定所有事物和行为在代码中只
表述一次,这正是优秀设计的根本。
重构使软件更容易理解
所谓程序设计,很大程度上就是与计算机交谈:你编写代码告诉计算机做什么事,它的响应则是精确
按照你的指示行动。你得及时填补“想要它做什么”和“告诉它做什么”之间的缝隙。这种编程模式的核心就
是“准确说出我所要的”。除了计算机外,你的源码还有其他读者:几个月之后可能会有另一位程序员尝试
读懂你的代码并做一些修改。我们很容易忘记这第二位读者,但他才是最重要的。计算机是否多花了几个
小时来编译,又有什么关系呢?如果一个程序员花费一周时间来修改某段代码,那才要命呢——如果他理解
了你的代码,这个修改原本只需一小时。
问题在于,当你努力让程序运转的时候,不会想到未来出现的那个开发者。是的,我们应该改变一下
开发节奏,对代码做适当修改,让代码变得更易理解。重构可以帮助我们让代码更易读。一开始进行重构
时,你的代码可以正常运行,但结构不够理想。在重构上花一点点时间,就可以让代码更好地表达自己的
用途。这种编程模式的核心就是“准确说出我所要的”。
关于这一点,我没必要表现得如此无私。很多时候那个未来的开发者就是我自己。此时重构就显得尤
其重要了。我是个很懒惰的程序员,我的懒惰表现形式之一就是:总是记不住自己写过的代码。事实上,对于任何能够立刻查阅的东西,我都故意不去记它,因为我怕把自己的脑袋塞爆。我总是尽量把该记住的
东西写进程序里,这样我就不必记住它了。这么一来我就不必太担心Old Peculier[1][Jackson]杀光我的脑
细胞。
这种可理解性还有另一方面的作用。我利用重构来协助我理解不熟悉的代码。每当看到不熟悉的代码,我必须试着理解其用途。我先看两行代码,然后对自己说:“噢,是的,它做了这些那些……”有了重
构这个强大武器在手,我不会满足于这么一点体会。我会真正动手修改代码,让它更好地反映出我的理
解,然后重新执行,看它是否仍然正常运作,以此检验我的理解是否正确。
一开始我所做的重构都像这样停留在细枝末节上。随着代码渐趋简洁,我发现自己可以看到一些以前
看不到的设计层面的东西。如果不对代码做这些修改,也许我永远看不见它们,因为我的聪明才智不足以
在脑子里把这一切都想象出来。Ralph Johnson把这种“早期重构”描述为“擦掉窗户上的污垢,使你看得更
远”。研究代码时我发现,重构把我带到更高的理解层次上。如果没有重构,我达不到这种层次。
重构帮助找到bug
对代码的理解,可以帮助我找到bug。我承认我不太擅长调试。有些人只要盯着一大段代码就可以找出
里面的bug,我可不行。但我发现,如果对代码进行重构,我就可以深入理解代码的作为,并恰到好处地把
新的理解反馈回去。搞清楚程序结构的同时,我也清楚了自己所做的一些假设,于是想不把bug揪出来都
难。
这让我想起了Kent Beck经常形容自己的一句话:“我不是个伟大的程序员,我只是个有着一些优秀习惯
的好程序员。”重构能够帮助我更有效地写出强健的代码。
重构提高编程速度
终于,前面的一切都归结到了这最后一点:重构帮助你更快速地开发程序。
听起来有点违反直觉。当我谈到重构,人们很容易看出它能够提高质量。改善设计、提升可读性、减
少错误,这些都是提高质量。但这难道不会降低开发速度吗?
我绝对相信:良好的设计是快速开发的根本——事实上,拥有良好设计才可能做到快速开发。如果没
有良好设计,或许某一段时间内你的进展迅速,但恶劣的设计很快就让你的速度慢下来。你会把时间花在
调试上面,无法添加新功能。修改时间越来越长,因为你必须花越来越多的时间去理解系统、寻找重复代
码。随着你给最初程序打上一个又一个的补丁,新特性需要更多代码才能实现。真是个恶性循环。
良好设计是维持软件开发速度的根本。重构可以帮助你更快速地开发软件,因为它阻止系统腐败变
质,它甚至还可以提高设计质量。
2.3 何时重构
当我谈论重构,常常有人问我应该怎样安排重构时间表。我们是不是应该每两个月就专门安排两个星
期来进行重构呢?
几乎任何情况下我都反对专门拨出时间进行重构。在我看来,重构本来就不是一件应该特别拨出时间
做的事情,重构应该随时随地进行。你不应该为重构而重构,你之所以重构,是因为你想做别的什么事,而重构可以帮助你把那些事做好。
三次法则
Don Roberts给了我一条准则:第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如
何还是可以去做;第三次再做类似的事,你就应该重构。
事不过三,三则重构。
添加功能时重构
最常见的重构时机就是我想给软件添加新特性的时候。此时,重构的直接原因往往是为了帮助我理解
需要修改的代码——这些代码可能是别人写的,也可能是我自己写的。无论何时,只要我想理解代码所做的事,我就会问自己:是否能对这段代码进行重构,使我能更快地理解它。然后我就会重构。之所以这么
做,部分原因是为了让我下次再看这段代码时容易理解,但最主要的原因是:如果在前进过程中把代码结
构理清,我就可以从中理解更多东西。
在这里,重构的另一个原动力是:代码的设计无法帮助我轻松添加我所需要的特性。我看着设计,然
后对自己说:“如果用某种方式来设计,添加特性会简单得多。”这种情况下我不会因为自己过去的错误而
懊恼——我用重构来弥补它。之所以这么做,部分原因是为了让未来增加新特性时能够更轻松一些,但最
主要的原因还是:我发现这是最快捷的途径。重构是一个快速流畅的过程,一旦完成重构,新特性的添加
就会更快速、更流畅。
修补错误时重构
调试过程中运用重构,多半是为了让代码更具可读性。当我看着代码并努力理解它的时候,我用重构
帮助加深自己的理解。我发现以这种程序来处理代码,常常能够帮助我找出bug。你可以这么想:如果收到
一份错误报告,这就是需要重构的信号,因为显然代码还不够清晰——没有清晰到让你能一眼看出bug。
复审代码时重构
很多公司都会做常规的代码复审,因为这种活动可以改善开发状况。这种活动有助于在开发团队中传
播知识,也有助于让较有经验的开发者把知识传递给比较欠缺经验的人,并帮助更多人理解大型软件系统
中的更多部分。代码复审对于编写清晰代码也很重要。我的代码也许对我自己来说很清晰,对他人则不
然。这是无法避免的,因为要让开发者设身处地为那些不熟悉自己所做所为的人着想,实在太困难了。代
码复审也让更多人有机会提出有用的建议,毕竟我在一个星期之内能够想出的好点子很有限。如果能得到
别人的帮助,我的生活会滋润得多,所以我总是期待更多复审。
我发现,重构可以帮助我复审别人的代码。开始重构前我可以先阅读代码,得到一定程度的理解,并
提出一些建议。一旦想到一些点子,我就会考虑是否可以通过重构立即轻松地实现它们。如果可以,我就
会动手。这样做了几次以后,我可以把代码看得更清楚,提出更多恰当的建议。我不必想象代码应该是什
么样,我可以“看见”它是什么样。于是我可以获得更高层次的认识。如果不进行重构,我永远无法得到这
样的认识。
重构还可以帮助代码复审工作得到更具体的结果。不仅获得建议,而且其中许多建议能够立刻实现。
最终你将从实践中得到比以往多得多的成就感。
为了让过程正常运转,你的复审团队必须保持精练。就我的经验,最好是一个复审者搭配一个原作
者,共同处理这些代码。复审者提出修改建议,然后两人共同判断这些修改是否能够通过重构轻松实现。
果真能够如此,就一起着手修改。
如果是比较大的设计复审工作,那么在一个较大团队内保留多种观点通常会更好一些。此时直接展示
代码往往不是最佳办法。我喜欢运用UML示意图展现设计,并以CRC卡展示软件情节。换句话说,我会和
某个团队进行设计复审,而和单个复审者进行代码复审。
极限编程[Beck,XP]中的“结对编程”形式,把代码复审的积极性发挥到了极致。一旦采用这种形
式,所有正式开发任务都由两名开发者在同一台机器上进行。这样便在开发过程中形成随时进行的代码复
审工作,而重构也就被包含在开发过程内了。
为什么重构有用
——Kent Beck
程序有两面价值:“今天可以为你做什么”和“明天可以为你做什么”。大多数时候,我们都只关注自己今
天想要程序做什么。不论是修复错误或是添加特性,我们都是为了让程序能力更强,让它在今天更有价
值。但是系统当下的行为,只是整个故事的一部分,如果没有认清这一点,你无法长期从事编程工作。如
果你为求完成今天的任务而不择手段,导致不可能在明天完成明天的任务,那么最终还是会失败。但是,你知道自己今天需要什么,却不一定知道自己明天需要什么。也许你可以猜到明天的需求,也许吧,但肯
定还有些事情出乎你的意料。
对于今天的工作,我了解得很充分;对于明天的工作,我了解得不够充分。但如果我纯粹只是为今天
工作,明天我将完全无法工作。
重构是一条摆脱困境的道路。如果你发现昨天的决定已经不适合今天的情况,放心改变这个决定就
是,然后你就可以完成今天的工作了。明天,喔,明天回头看今天的理解也许觉得很幼稚,那时你还可以
改变你的理解。
是什么让程序如此难以相与? 眼下我能想起下述四个原因,它们是:
难以阅读的程序,难以修改;
逻辑重复的程序,难以修改;
添加新行为时需要修改已有代码的程序,难以修改;
带复杂条件逻辑的程序,难以修改。
因此,我们希望程序:(1)容易阅读;(2)所有逻辑都只在唯一地点指定;(3)新的改动不会危及现有行
为;(4)尽可能简单表达条件逻辑。
重构是这样一个过程:它在一个目前可运行的程序上进行,在不改变程序行为的前提下使其具备上述
美好性质,使我们能够继续保持高速开发,从而增加程序的价值。
2.4 怎么对经理说
“该怎么跟经理说重构的事?”这是我最常被问到的一个问题。如果这位经理懂技术,那么向他介绍重构
应该不会很困难。如果这位经理只对质量感兴趣,那么问题就集中到了“质量”上面。此时,在复审过程中
使用重构就是一个不错的办法。大量研究结果显示,技术复审是减少错误、提高开发速度的一条重要途
径。随便找一本关于复审、审查或软件开发程序的书看看,从中找些最新引证,应该可以让大多数经理认
识复审的价值。然后你就可以把重构当作“将复审意见引入代码内”的方法来使用,这很容易。
当然,很多经理嘴巴上说自己“质量驱动”,其实更多是“进度驱动”。这种情况下我会给他们一个较有争
议的建议:不要告诉经理!
这是在搞破坏吗?我不这样想。软件开发者都是专业人士。我们的工作就是尽可能快速创造出高效软
件。我的经验告诉我,对于快速创造软件,重构可带来巨大帮助。如果需要添加新功能,而原本设计却又
使我无法方便地修改,我发现先重构再添加新功能会更快些。如果要修补错误,就得先理解软件的工作方
式,而我发现重构是理解软件的最快方式。受进度驱动的经理要我尽可能快速完事,至于怎么完成,那就
是我的事了。我认为最快的方式就是重构,所以我就重构。
间接层和重构
——Kent Beck
“计算机科学是这样一门科学:它相信所有问题都可以通过增加一个间接层来解决。”
——Dennis DeBruler
由于软件工程师对间接层如此醉心,你应该不会惊讶大多数重构都为程序引入了更多间接层。重构往
往把大型对象拆成多个小型对象,把大型函数拆成多个小型函数。但是,间接层是一柄双刃剑。每次把一个东西分成两份,你就需要多管理一个东西。如果某个对象委
托另一对象,后者又委托另一对象,程序会愈加难以阅读。
基于这个观点,你会希望尽量减少间接层。
别急,伙计!间接层有它的价值。下面就是间接层的某些价值。
允许逻辑共享。比如说一个子函数在两个不同的地点被调用,或超类中的某个函数被所有子类共享。
分开解释意图和实现。你可以选择每个类和函数的名字,这给了你一个解释自己意图的机会。类或函
数内部则解释实现这个意图的做法。如果类和函数内部又以更小单元的意图来编写,你所写的代码就可以
描述其结构中的大部分重要信息。
隔离变化。很可能我在两个不同地点使用同一对象,其中一个地点我想改变对象行为,但如果修改了
它,我就要冒同时影响两处的风险。为此我做出一个子类,并在需要修改处引用这个子类。现在,我可以
修改这个子类而不必承担无意中影响另一处的风险。
封装条件逻辑。对象有一种奇妙的机制:多态消息,可以灵活而清晰地表达条件逻辑。将条件逻辑转
化为消息形式,往往能降低代码的重复、增加清晰度并提高弹性。
这就是重构游戏:在保持系统现有行为的前提下,如何才能提高系统的质量或降低其成本,从而使它
更有价值?
这个游戏中最常见的变量就是:你如何看待你自己的程序。找出一个缺乏“间接层利益”之处,在不修
改现有行为的前提下,为它加入一个间接层。现在你获得了一个更有价值的程序,因为它有较高的质量,让我们在明天(未来)受益。
请将这种方法与“小心翼翼的事前设计”做个比较。推测性设计总是试图在任何一行代码诞生之前就先
让系统拥有所有优秀质量,然后程序员将代码塞进这个强健的骨架中就行了。这个过程的问题在于:太容
易猜错。如果运用重构,你就永远不会面临全盘错误的危险。程序自始至终都能保持一致的行为,而你又
有机会为程序添加更多价值不菲的质量。
还有一种比较少见的重构游戏:找出不值得的间接层,并将它拿掉。这种间接层常以中介函数形式出
现,它也许曾经有过贡献,但芳华已逝。它也可能是个组件,你本来期望在不同地点共享它,或让它表现
出多态性,最终却只在一处用到。如果你找到这种“寄生式间接层”,请把它扔掉。如此一来你会获得一个
更有价值的程序,不是因为它取得了更多的优秀质量,而 ......
您现在查看是摘要介绍页, 详见PDF附件(5690KB,326页)。





