目录
第一章 重构,第一个案例
第二章 重构原则
第三章 代码中的坏味道
第四章 构筑测试体系
第五章 重构列表
第六章 重新组织函数
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.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.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.12Replace Record with Data Class (以数据类取代记录)
8.13 Replace Type Code with Class ( 以类取代类型码)
8.14 Replace Type Code with Subclasses (以子类取代类型码)
8.15 Replace Type Code with State/Strategy(以State/Strategy取代类型码)
8.16 Replace Subclass with Fields ( 以字段取代子类)
第九章 简化条件表达式
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.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.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.1 Tease Apart Inheritance (梳理并分解继承体系)
12.2 Convert Procedural Design to Objects(将过程化设计转化为对象设计)
12.3 Separate Domain from Presentation(将领域和表述/显示分离)
12.4 Extract Hierarchy (提炼继承体系)
第十三章 重构,复用与实现
第十四章 重构工具
第十五章 总结
最近一段时间,我们不门针对Java开发人员,组织学习了一下《重构:改善既有代码设计》书籍,在学习的过程中,我也简略的记了一些自己认为的要点,给自己读书留下点印记。
重构(名词):
对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
重构(动词):
使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
重构的目的:
1.重构改进软件设计
2.重构是软件更容易理解
3.重构帮助找打bug
4.重构提高编程速度
重构时机:
1.三次原则
2.添加功能是重构
3.修补错误时重构
4.复审代码时重构
重构过程:
它在一个目前可运行的程序上进行,在不改变程序行为的前提习使其具备上述美好性质,使我们能够继续保持高速开发,从而增加程序的价值。
间接层价值:
1.允许逻辑共享
2.分开解释意图和实现。
3.隔离变化
4.封装条件逻辑
1.重复代码
2.过长函数
3.过大的类
4.过长的参数列表
5.发散式变化
6.霰弹式修改
7.依恋情结
8.数据泥团
9.基本类型偏执
10.switch惊悚现身
11.平行继承体系
12.冗赘类
13.夸夸其谈未来性
14.令人迷惑的暂时字段
15.过度耦合的信息链
16.中间人
17.狎昵关系
18.异曲同工的类
19.不完美的类库
20.纯稚的数据类
21.被拒绝的遗赠
22.过多注释
撰写测试代码的最有用时机是在开始编程之前。频繁地运行测试。每次编译请把测试也考虑进去一每天 至少执行每个测试一次。
编写这些测试的目的是为了提高程序员的生产率。
功能测试就完全不同。它们用来保证软件能够正常运作。
每当你收到bug报告,请先写一个单元测试来暴露bug.
哪怕只做一点点测试, 你也能从中受益。测试的要诀是:测试你最担心出错的部分,这样你就能从测试工作中得到最大利益。
编写未臻完善的测试并实际运行,好过对完美测试的无尽等待。
考虑可能出错的边界条件,把测试火力集中在那儿。
当事情被认为应该会出错时,别忘了检查是否抛出了预期的异常。
任何测试都不能证明一个程序没有bug。
测试可以提高编程速度。
不要因为测试无法捕捉所有bug就不写测试,因为测试的确可以捕捉到大多数bug。
每个重构手法都有如下五个部分:
首先是名称(name)。 建造一个重构词汇表,名称是很重要的。这个名称也就是我将在本书其他地方使用的名称。
名称之后是一个简短概要(summary)。 简单介绍此重构手法的适用情景,以及它所做的事情。这部分可以帮助你更快找到你所需要的重构手法。
动机(motivation) 为你介绍“为什么需要这个重构”和“什么情况下不该使用这个重构”。
做法(mechanics) 简明扼要地一步一步介绍如何进行此一重构。
范例(examples) 以一个十分简单的例子说明此重构手法如何运作。
重构的基本技巧一小步前进、频繁测试。
当看见一个过长的函数或者一段需要注释才能让人理解用途的代码,就会将这段代码放进一个独立函数中。
做法
创造一个新函数,根据这个函数的意图来对它命名(以它“做什么”来命名,而不是以它“怎样做”命名)。
即使你想要提炼的代码非常简单,例如只是一条消息或一个函数调用,只要新函数的名称能够以更好方式昭示代码意图,你也应该提炼它。但如果 你想不出一个更有意义的名称,就别动。
将提炼出的代码从源函数复制到新建的目标函数中。
仔细检查提炼出的代码,看看其中是否引用了“作用域限于源函数”的变量(包括局部变量和源函数参数)。
检查是否有“仅用于被提炼代码段"的临时变量。如果有,在目标函数中将它们声明为临时变量。
检查被提炼代码段,看看是否有任何局部变量的值被它改变。如果-一个临时 变量值被修改了,看看是否可以将被提炼代码段处理为-一个查询,并将结果 赋值给相关变量。如果很难这样做,或如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动地提炼出来。你可能需要先使用Split Temporary Variable (128),然后再尝试提炼。也可以使用Replace Temp with Query (120) 将临时变量消灭掉(请看“范例”中的讨论)。
将被提炼代码段中需要读取的局部变量,当作参数传给目标函数。
处理完所有局部变量之后,进行编译。
在源函数中,将被提炼代码段替换为对目标函数的调用。
如果你将任何临时变量移到目标函数中,请检查它们原本的声明式是否在被提炼代码段的外围。如果是,现在你可以删除这些声明式了.
编译,测试。
一个函数的本体与名称同样清楚易懂。
如果别人使用了太多间接层,使得系统中的所有函数都似乎只是对另一个函数 的简单委托,造成我在这些委托动作之间晕头转向,那么我通常都会使用Inline Method(117)。
做法
检查函数,确定它不具多态性。
如果子类继承了这个函数,就不要将此函数内联,因为子类无法覆写一个根本不存在的函数。
找出这个函数的所有被调用点。
将这个函数的所有被调用点都替换为函数本体.
编译,测试。
删除该函数的定义。
你有一个临时变量,只被一个简单表达式赋值一次, 而它妨碍了其他重构手法。
做法
检查给临时变量赋值的语句,确保等号右边的表达式没有副作用。
如果这个临时变量并未被声明为final,那就将它声明为final,然后编译。
这可以检查该临时变量是否真的只被赋值一 次。
找到该临时变量的所有引用点,将它们替换为“为临时变量赋值"的表达式。
每次修改后,编译并测试。
修改完所有引用点之后,删除该临时变量的声明和赋值语句。
编译,测试。
你的程序以一个临时变量保存某一表达式的运算结果。
做法
首先是简单情况:
找出只被赋值一-次的临时变量。
切如果某个临时变量被赋值超过一次, 考虑使用Split Temporary Variable (128)将它分割成多个变量。
将该临时变量声明为final.
编译。
功这可确保该临时变量的确只被赋值一次。
将“对该临时变量赋值”之语句的等号右侧部分提炼到一个独立函数中。
首先将函数声明为private。日后你可能会发现有更多类需要使用它,那时放松对它的保护也很容易。
确保提炼出来的函数无任何副作用,也就是说该函数并不修改任何对象内容。如果它有副作用,就对它进行Separate Query from Modifler (279)。
编译,测试。
在该临时变量身上实施Inline Temp (119)。
动机
表达式有可能非常复杂而难以阅读。这种情况下,临时变量可以帮助你将表达式分解为比较容易管理的形式。
做法
声明一个final临时变量,将待分解之复杂表达式中的一部分动作的运算结果赋值给它。
将表达式中的“运算结果”这一部分,替换为上述临时变量。
如果被替换的这一部分在代码中重复出现,你可以每次一个,逐一替换。
编译,测试。
重复上述过程,处理表达式的其他部分。
程序有某个临时变量被赋值超过一次,它既不是循环变量,也不被用于收集计算结果。
针对每次赋值,创造一个独立、对应的临时变量。
做法
在待分解临时变量的声明及其第一次被赋值处,修改其名称。
如果稍后之赋值语句是[i=i+某表达式]形式,就意味这是个结果收集变量,那么就不要分解它。结果收集变量的作用通常是累加、字符串接合、写入流或者向集合添加元素。
将新的临时变量声明为final.
以该临时变量的第二次赋值动作为界,修改此前对该临时变量的所有引用点,让它们引用新的临时变量。
在第二次赋值处,重新声明原先那个临时变量。
编译,测试。
逐次重复上述过程。每次都在声明处对临时变量改名,并修改下次赋值之前的引用点。
代码对一个参数进行赋值。 以一个临时变量取代该参数的位置。
做法
建立一个临时变量,把待处理的参数值赋予它。
以“对参数的赋值"为界,将其后所有对此参数的引用点,全部替换为“对此临时变量的引用”。
修改赋值语句,使其改为对新建之临时变量赋值。
编译,测试。
如果代码的语义是按引用传递的,请在调用端检查调用后是否还使用了这参数。也要检查有多少个按引用传递的参数被赋值后又被使用。请尽量只以return方式返回一个值。如果需要返回的值不止一个,看看可否把需返回的大堆数据变成单一对象,或干脆为每个返回值设计对应的一个独立函数
你有一个大型函数,其中对局部变量的使用使你无法采用Extract Method (110)。
将这个函数放进一个单独对象中,如此- -来局部变量就成了对象内的字段。然后你
可以在同一个对象中将这个大型函数分解为多个小型函数。
做法
我厚着脸皮从Kent Beck [Beck]那里偷来了下列做法。
建立一个新类,根据待处理函数的用途,为这个类命名。
在新类中建立一个final字段,用以保存原先大型函数所在的对象。我们将这个字段称为“源对象”。同时,针对原函数的每个临时变量和每个参数, 在新类中建立一个对应的字段保存之。
在新类中建立一个构造函数,接收源对象及原函数的所有参数作为参数。
在新类中建立一个compute()函数。
将原函数的代码复制到compute()函数中。如果需要调用源对象的任何函数,请通过源对象字段调用。
编译。
将旧函数的函数本体替换为这样一条语句:“创建上述新类的一个新对象,而后调用其中的compute()函数”。
你想要把某个算法替换为另一个更清晰的算法。
将函数本体替换为另一个算法。
做法
准备好另一个(替换用)算法,让它通过编译。
针对现有测试,执行上述的新算法。如果结果与原本结果相同,重构结束。
如果测试结果不同于原先,在测试和调试过程中,以旧算法为比较参照标准。
对于每个测试用例,分别以新旧两种算法执行,并观察两者结果是否相同.这可以帮助你看到哪一个测试用例出现麻烦,以及出现了怎样的麻烦.
你的程序中,有个函数与其所驻类之外的另一个类进行更多交流:调用后者,或被后者调用。
在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是将旧函数完全移除。
做法
检查源类中被源函数所使用的一切特性(包括字段和函数) ,考虑它们是否 也该被搬移。
如果某个特性只被你打算搬移的那个函数用到,就应该将它一并搬移。如果另有其他函数使用了这个特性,你可以考虑将使用该特性的所有函数全都一并搬移。有时候,搬移一组函数比逐一搬移简单些。
检查源类的子类和超类,看看是否有该函数的其他声明。
如果出现其他声明,你或许无法进行搬移,除非目标类也同样表现出多态性。
在目标类中声明这个函数。
由你可以为此函数选择一个新名称——对目标类更有意义的名称。
将源函数的代码复制到目标函数中。调整后者,使其能在新家中正常运行。
如果目标函数使用了源类中的特性,你得决定如何从目标函數引用源对象。如果目标类中没有相应的引用机制,就把源对象的引用当作参数,传给新建立的日标函数。
如果源函数包含异常处理,你得判断逻辑上应诮由哪个类来处理这一异 常。如果应该由源类来负责,就把异常处理留在原地。
编译目标类。
决定如何从源函数正确引用口标对象。.
可能会有一个现成的字段或函数帮助你取得目标对象。如果没有,就看能否轻松建立一个这样的函数。如果还是不行,就得在源类中新建一个字段来保存目标对象。这可能是一个永久性修改,但你也可以让它是暂时的,因为后继的其他重构项目可能会把这个新建字段去掉.
修改源函数,使之成为一个纯委托函数。
编译,测试。
决定是否删除源函数,或将它当作一个委托函数保留下来。
如果你经常要在源对象中引用目标函数,那么将源函数作为委托函数保留下来会比较简单。.
如果要移除源函数,请将源类中对源函数的所有调用,替换为对目标函数的调用。
你可以每修改一个引用点就编译并测试一次。也可以通过一次“查找/替换”改掉所有引用点,这通常简单一些。
编译,测试
你的程序中,某个字段被其所驻类之外的另一个类更多地用到。 在目标类新建一个字段,修改源字段的所有用户,令它们改用新字段。
做法
如果字段的访问级是public使用Encapsulate Field (206)将它封装起来。
如果你有可能移动那些频繁访问该字段的函数,或如果有许多函数访问某个字段,先使用Self Encapsulate Field(171)也许会有帮助。
编译,测试。
在目标类中建立与源字段相同的字段,并同时建立相应的设值/取值函数。
编译目标类。
决定如何在源对象中引用目标对象。
首先看是否有一个现成的字段成函数可以助你得到目标对象,如果没有,就看能否轻易建立这样一个函数。如果还不行,就得在源类中新建一个字段来存放目标对象。这可能是个永久性修改,但你也可以让它是暂时的,因为后续重枸可能会把这个新建字段除掉.
删除源字段。
将所有对源字段的引用替换为对某个目标函数的调用。
如果需要读取该变量,就把对源字段的引用替换为对目标取值函数的调用;如果要对该变量赋值,就把对源字段的引用替换成对设值函數的调用。如果源字段不是private的,就必须在源类的所有子类中查找源字段的引用点,并进行相应替换。
口编译,测试。
某个类做了应该由两个类做的事。
建立一个新类,将相关的字段和函数从旧类搬移到新类。
做法
决定如何分解类所负的责任。
建立一个新类,用以表现从旧类中分离出来的责任。
如果旧类剩下的责任与旧类名称不符,为旧类更名。
建立“从旧类访问新类"的连接关系。
有可能需要一个双向连接。但是在真正需要它之前,不要建立“从新类通 往旧类"的连接。
对于你想搬移的每-一个字段,运用Move Field (146)搬移之。
每次搬移后,编译、测试。
使用Move Method(142)将必要函数搬移到新类。先搬移较低层函数(也就是“被其他函数调用”多于“调用其他函敷”者),再搬移较高层函数。
每次搬移之后,编译、测试。
检查,精简每个类的接口。
如果你建立起双向连接, 检查是否可以将它改为单向连接。
决定是否公开新类。如果你的确需要公开它,就要决定让它成为引用对象还是不可变的值对象。
某个类没有做太多事情。
将这个类的所有特性搬移到另一个类中,然后移除原类。
做法
在目标类身上声明源类的public协议,并将其中所有函数委托至源类。
如果“以一个独立接口表示源类函数”更合适的话,就应该在内联之前先使用Extract Interface (341).
修改所有源类引用点,改而引用目标类。
将源类声明为private,以斩断包之外的所有引用可能。同时修改源类的名称,这便可使编译器帮助你捕捉到所有对于源类的隐藏引用点。
编译,测试。
运用Move Method (142)和Move Field (146),将源类的特性全部搬移到目标类。
为源类举行- 一个简单的“丧礼”。
客户通过一个委托类来调用另一个对象。
在服务类上建立客户所需的所有函数,用以隐藏委托关系。
做法
对于每一个委托关系中的函数,在服务对象端建立一个简单的委托函数。
调整客户,令它只调用服务对象提供的函数。
如果使用者和服务提供者不在同一个包,考虑修改委托函数的访问权限,让客户得以在包之外调用它。
每次调整后,编译并测试。
如果将来不再有任何客户需要取用图7-1的Delegate (受托类),便可移除服务对象中的相关访问函数。
编译,测试。
某个类做了过多的简单委托动作。
让客户直接调用受托类。
做法
建立一个函数,用以获得受托对象。
对于每个委托函数,在服务类中删除该函数,并让需要调用该函数的客户转调用受托对象。
处理每个委托函数后,编译、测试。
你需要为提供服务的类增加一个函数,但你无法修改这个类。
在客户类中建立一个函数,并以第一参数形式传入一个服务类实例。
做法
在客户类中建立一个函数,用来提供你需要的功能。
这个函数不应该调用客户类的任何特性。如果它需-一个值,把该值当作参数传给它。
以服务类实例作为该函数的第一个参数。
将该函数注释为:“ 外加函数(foreign method),应在服务类实现。”
这么一来,如果将来有机会将外加函数搬移到服务类中时,你便可以轻松找出这些外加函数。
你需要为服务类提供一些额外函数, 但你无法修改这个类。
建立一个新类,使它包含这些额外函数。让这个扩展品成为源类的子类或包装类。
做法
建立一个扩展类,将它作为原始类的子类或包装类。
在扩展类中加入转型构造函数。
所谓“转型构造函数”是指“接受原对象作为参数”的构造函数。如果采用子类化方案,那么转型构造函数应该调用适当的超类构造函数;如果采用包装类方案,那么转型构造函数应该将它得到的传入参数以实例变量的形式保存起来,用作接受委托的原对象。
在扩展类中加入新特性。
根据需要,将原对象替换为扩展对象。
将针对原始类定义的所有外加函数搬移到扩展类中。
你直接访问一个字段,但与字段之间的耦合关系逐渐变得笨拙。
为这个字段建立取值/设值函数,并且只以这些函数来访问字段。
做法
为待封装字段建立取值/设值函数。
找出该字段的所有引用点,将它们全部改为调用取值/设值函数。
如果引用点要读取字段值,就将它替换为调用取值函数;如果引用点要给字段赋值,就将它替换为调用设值函数。
你可以暂时将该字段改名,让编译器帮助你查找引用点。
将该字段声明为private。
复查,确保找出所有引用点。
编译,测试。
你有一个数据项,需要与其他数据和行为一起使用才有意义。
将数据项变成对象。
做法
为待替换数值新建一个类,在其中声明一个final字段,其类型和源类中的待替换数值类型一样。然后在新类中加入这个字段的取值函数,再加上一个接 受此字段为参数的构造函数。
编译。
将源类中的待替换数值字段的类型改为前面新建的类。
修改源类中该字段的取值函数,令它调用新类的取值函数。
如果源类构造函数中用到这个待替换字段(多半是赋值动作),我们就修改构造函数,令它改用新类的构造函数来对字段进行赋值动作。
修改源类中待替换字段的设值函数,令它为新类创建一个实例。
编译,测试。
现在,你有可能需要对新类使用Change Value to Reference (179)。
你从一个类衍生出许多彼此相等的实例,希望将它们替换为同一个对象。
将这个值对象变成引用对象。
做法
使用Replace Constructor with Factory Method (304)。
编译,测试。
决定由什么对象负责提供访问新对象的途径。
可能是一个静态字典或一个注册表对象.
你也可以使用多个对象作为新对象的访问点。
决定这些引用对象应该预先创建好,或是应该动态创建。
如果这些引用对象是预先创建好的,而你必须从内存中将它们读取出来,那么就得确保它们在被需要的时候能够被及时加载。
修改工厂函数,令它返回引用对象。
如果对象是预先创建好的,你就需要考虑:万一有人索求一个其实并不存在的对象,要如何处理错误?
你可能希望对工厂函数使用Rename Method (273),使其传达这样的信息:它返回的是一个既存对象。
编译,测试。
你有一个引用对象,很小且不可变,而且不易管理。
将它变成一个值对象。
做法
检查重构目标是否为不可变对象,或是否可修改为不可变对象。
如果该对象目前还不是不可变的,就使用Remove Setting Method (300),直到它成为不可变的为止。
如果无法将该对象修改为不可变的,就放弃使用本项重构。
建立equals ()和hashCode()。
编译,测试。
考虑是否可以删除工厂函数,并将构造函数声明为public。
你有一个数组,其中的元素各自代表不同的东西。
以对象替换数组。对于数组中的每个元素,以一个字段来表示。
做法
新建一个类表示数组所拥有的信息,并在其中以一个public字段保存原先的数组。
修改数组的所有用户,让它们改用新类的实例。
编译,测试。
逐一为数组元素添加取值/设值函数。根据元素的用途,为这些访间函数命名。修改客户端代码,让它们通过访问函数取用数组内的元素。每次修改后,编译并测试。
当所有对数组的直接访问都转而调用访问函数后,将新类中保存该数组的字 段声明为private。
编译。
对于数组内的每一个元素,在新类中创建一个类型相当的字段。修改该元素的访问函数,令它改用上述的新建字段。
每修改一个元素,编译并测试。
数组的所有元素都有了相应字段之后,删除该数组。
你有一些领域数据 置身于GUI控件中,而领域函数需要访问这些数据。 将该数据复制到一个领域对象中。建立一个Observer模式,用以同步领域对象和GUI对象内的重复数据。
做法
修改展现类,使其成为领域类的Observer[(GoF]。
如果尚未有领城类,就建立一个。
如果没有“从展现类到领城类”的关联,就将领域类保存于展现类的一个字段中。
针对GUI类中的领域数据,使用Self Encapsulate Field (171)。
编译,测试。
在事件处理函数中调用设值函数,直接更新GUI组件。
在事件处理函数中放一个设值函数,利用它将GUI组件更新为领城数据的当前值。当然这其实没有必要,你只不过是拿它的值设定它自己。但是这样使用设值函数,便是允许其中的任何动作得以于日后被执行起来,这是这一步骤的意义所在。
进行这个改变时,对于组件,不要使用取值函数,应该直接取用,因为稍后我们将修改取值函数,使其从领城对象(而非GUI组件)取值。设值函数也将做类似修改。
确保测试代码能够触发新添加的事件处理机制。
编译,测试。
在领城类中定义数据及其相关访问函数。
确保领域类中的设值函数能够触发Observer模式的通报机制。 与对于被观察的数据,在领域类中使用与展现类所用的相同类型(通常是字符串)来保存。后续重构中你可以自由改变这个数据类型。
修改展现类中的访问函数,将它们的操作对象改为领域对象(而非GUI组件)。
修改Obscrver的update(), 使其从相应的领域对象中将所需数据复制给GUI组件。
编译,测试。
两个类都需要使用对方特性,但其间只有一条单向连接。
添加一个反向指针,并使修改函数,能够同时更新两条连接。
做法
在被引用类中增加一个字段,用以保存反向指针。
决定由哪个类———引用端还是被引用端———控制关联关系。
在被控端建立一个辅助函数,其命名应该清楚指出它的有限用途。
如果既有的修改函数在控制端,让它负责更新反向指针。
如果既有的修改函数在被控端,就在控制端建立一个控制函数,并让既有的修改函数调用这个新建的控制函数。
两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性。去除不必要的关联。
做法
找出保存“你想去除的指针”的字段。检查它的每一个用户。判断是否可以去除该指针。
不但要检查直接访问点,也要检查调用这些直接访问点的函数。
考虑有无可能不通过指针取得被引用对象.如果有可能,你就可以对取值函数使用Subtinule Algorihm(139),从而让客户在没有相针的情况下也可以使用值取值函教。
对于使用该字段的所有函数。考虑将被引周时象作为参数传进去。
如果客户使用了取值消数,先运Self Encapsulate Field (171)将待删除字段自我封装起来,然后使用Sabtitute Algoritim (139)对付取值函数,它不再使用该字段。然后编译,测试。
如果客户并未使用取值函数,那就直按修改待删除字段的所有被引用点:改以其他途径获得该字段所保存的对象。每次修改后,编译并测试。
如果已经没有任何函数使用待删除字段,移除所有对该字段的更新逻辑,然后移除该字段。
如果有许多地方时此宇段赋值,先达用Self Encapsulate Field (171)使这些地点改用同一个设值函教.编译。测试。而后得这个设值函数的本体清空。再编译,再测试。如果这些都可行,就可以将此字段和其设值函教,连同对设值函数数的所有调用,全部移除,
编评,测试。
你有一个字面数值,带有特别含义。
创造一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量。
做法
声明一个常量,令其值为原本的魔法值。
找出这个魔法值的所有引用点。
检查是否可以使用这个新声明的常量来替换该魔法值。如果可以,便以此常量替换之。
编译。
所有魔法值都被替换完毕后,编译并测试。此时整个程序应该运转如常,就像没有做任何修改一样。
有个不错的测试办法: 检查现在的程序是否可以被你轻松地修改常量值
(这可能意味某些预期结果将有所改变,以配合这一新值。实际工作中并 非总是可以进行这样的测试)。如果可行,这就是一个不错的手法。
你的类中存在一个public字段。将它声明为private,并提供相应的访问函数。
做法
为public字段提供取值/设值函数。
找到这个类以外使用该字段的所有地点。如果客户只是读取该字段,就把引用替换为对取值函数的调用:如果客户修改了该字段值,就将此引用点替换为对设值函数的调用。.
如果这个字段是个对象,而客户只不过是调用该对象的某个函数,那么无论该函数是否改变对象状态,都只能算是读取该字段。只有当客户为该字.段赋值时,才能将其替换为设值函数.
每次修改之后,编译并测试。
将字段的所有用户修改完毕后,把字段声明为private。
编译,测试。
有个函数返回一个集合。让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数。
做法
加入为集合添加/移除元素的函数。
将保存集合的字段初始化为一个空集合。
编译。
找出集合设值函数的所有调用者。你可以修改那个设值函数,让它使用上述 新建立的“添加/移除元素”函数:也可以直接修改调用端,改让它们调用上述新建立的“添加/移除元素"函数.
两种情况下需要用到集合设值函数: (1)集合为空时: (2)准备将原有集合替换为另一个集合时。
你或许会想运用Rename Method (273)为集合设值函数改名:从setXxx() 改为initializexxx()或replaceXxx()。
编译,测试。
找出所有"通过取值函数获得集合井作改其内容"的函数。逐一修改这些函数,让它们改用添加/移除函数。每次修改后,编译并测试。
修改完上述所有“通过取值函数获得集合井修改集合内容"的函数后,修改 取值函数白身,使它返回该集合的个只读副本。
在Java 2中,你可以使用collect ion , unmodi fiablexx()得到该集合的只读副本。
在Java 1中,你应该返回集合的一份副本。
编译,测试。
找出取值函散的所有用户,从中找出应该存在于集合所属对象内的代码.运用Extract Method (110)和Move Method (142)将这些代码移到宿主对象去。
如果你使用Java 2, 那么本项重构到此为止。 如果你使用Java 1.1,那么用户也许会喜欢使用收举,为了提供这个枚举,你应该值如下这样做。
修改现有取值函数的名字,然后添加一个新取值函数,使其返回个枚举。找出旧取值函数的所有被使用点。将它们都改为使用新取值函数。
如果这一步跨度太大,你可以先使用Rename Method (273)修改原取值函数的名称:再建立一个新取值函数用以返回枚举:最后再修改所有调用者,使其调用新取值函数。
编译,测试。
你需要面对传统编程环境中的记录结构。
为该记录创建一个“哑”数据对象。
做法
新建一个类,表示这个记录。
对于记录中的每一项数据,在新建的类中建立对应的一个private字段,并提供相应的取值/设值函数。
类之中有一个数值类型码,但它并不影响类的行为,以一个新的类替换该数值类型码。
做法
为类型码建立一个类。
这个类需要一个用以记录类型码的字段,其类型应该和类型码相同,并应该有对应的取值函数。此外还应该用一组静态变量保存允许被创建的实例,并以一个静态函数根据原本的类型码返回合适的实例。
修改源类实现,让它使用上述新建的类。
维持原先以类型码为基础的函数接口,但改变静态字段,以新建的类产生代码。然后,修改类型码相关函数,让它们也从新建的类中获取类型码。
编译,测试。
此时,新建的类可以对类型码进行运行期检查。
对于源类中每一个使用类型码的函数,相应建立一个函数,让新函数使用新建的类。
你需要建立“以新类实例为自变量”的函数,用以替换原先“直接以类型码为参数”的函数。你还需要建立一个“返回新类实例”的函数,用以替换原先“直接返回类型码”的函数。建立新函数前,你可以使用Rename Method(273)修改原函数名称,明确指出哪些函数仍然使用旧式的类型码,这往往是个明智之举。
逐一修改源类用户,让它们使用新接口。
每修改一个用户,编译并测试。
你也可能需要一次性修改多个彼此相关的函数,才能保持这些函数之间的一致性,才能顺利地编译、测试。
删除使用类型码的旧接口,并删除保存旧类型码的静态变量。
编译,测试。
你有一个不可变的类型码,它会影响类的行为。以子类取代这个类型码。
做法
使用Self Encapsulate Field (171)将类型码自我封装起来。
如果类型码被传递给构造函数,就需要将构造函数换成工厂函数。
为类型码的每一个数值建立一个相应的子类。在每个子类中覆写类型码的取值函数,使其返回相应的类型码值。
这个值被硬编码于return句 中(例如,return1).这看起来很肮脏,但只是权宜之计。当所有case子句都被替换后,问题就解决了。
每建立一个新的子类,编译并测试。
从超类中删掉保存类型码的字段。将类型码访问函数声明为抽象函数。
编译,测试。
你有一个类型码,它会影响类的行为,但你无法通过继承手法消除它。以状态对象取代类型码。
做法
使用Self Encapsulate Field (171)将类型码自我封装起来。
新建一个类,根据类型码的用途为它命名。这就是一个状态对象。
为这个新类添加子类,每个子类对应一种类型码。
比起逐一添加,一次性加入所有必要的子类可能更简单些。
在超类中建立一个抽象的查询函数,用以返回类型码。在每个子类中覆写该函数,返回确切的类型码。
编译。
在源类中建立一个字段,用以保存新建的状态对象。
调整源类中负责查询类型码的函数,将查询动作转发给状态对象。
调整源类中为类型码设值的函数,将一个恰 当的状态对象子类赋值给“保存状态对象”的那个字段。
编译,测试。
你的各个子类的唯一差别只在 “返回常量数据”的函数身上。修改这些函数,使它们返回超类中的某个(新增)字段,然后销毁子类。
做法
对所有子类使用Replace Constructor with Factory Method (304)。
如果有任何代码直接引用子类,令它改而引用超类。
针对每个常量函数,在超类中声明一个final字段。
为超类声明一个protected构造函数,用以初始化这些新增字段。
新建或修改子类构造函数,使它调用超类的新增构造函数。
编译,测试。
在超类中实现所有常最函数,令它们返回相应字段值,然后将该函数从子类中删掉。
每删除一个常量函数,编译并测试。
子类中所有的常量函数都被删除后,使用Inline Method(17)将子类构造函数内联到超类的工厂函数中。
编译,测试。
将子类删掉。
编译,测试。
重复“内联构造函数、删除子类"过程,直到所有子类都被删除。
你有一个复杂的条件(if-then-else) 语句。从if、then、else三 个段落中分别提炼出独立函数。
做法
将if段落提炼出来,构成一个独立函数。
将then段落和else段落都提炼出来,各自构成一个独立函数。
你有一系列条件测试,都得到相同结果。将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数。
做法
确定这些条件语句都没有副作用。
如果条件表达式有副作用,你就不能使用本项重构。
使用适当的逻辑操作符,将一系列相关条件表达式合并为一个。
编译,测试。
对合并后的条件表达式实施Extract Method (110)。
在条件表达式的每个分支上有着相同的一段代码。将这段重复代码搬移到条件表达式之外。
做法
鉴别出“执行方式不随条件变化而变化”的代码。
如果这些共通代码位于条件表达式起始处,就将它移到条件表达式之前。
如果这些共通代码位于条件表达式尾端,就将它移到条件表达式之后。
如果这些共通代码位于条件表达式中段,就需要观察共通代码之前或之后的代码是否改变了什么东西。如果的确有所改变,应该首先将共通代码向前或 向后移动,移至条件表达式的起始处或尾端,再以前面所说的办法来处理。
如果共通代码不止一条语句, 应该首先使用Extract Method (110)将共通代码提炼到一个独立函数中,再以前面所说的办法来处理。
在一系列布尔表达式中,某个变量带有“控制标记" (control flag)的作用。以break语句或return语句取代控制标记。
做法
对控制标记的处理,最显而易见的办法就是使用Java提供的break语旬或 cont inue语句。
找出让你跳出这段逻辑的控制标记值。
找出对标记变量赋值的语句,代以恰当的break语句或continue语句。
每次替换后,编译并测试。
在未能提供break和cont inue语句的编程语言中,可以使用下述办法。
运用Extract Method (110),将整段逻辑提炼到一个独立函数中。
找出让你跳出这段逻辑的控制标记值。
找出对标记变量赋值的语句,代以恰当的return语句。
每次替换后,编译并测试。
函数中的条件逻辑使人难以看清正常的执行路径。使用卫语句表现所有特殊情况。
做法
对于每个检查,放进一个卫语句。
卫语句要不就从函数中返回,要不就抛出一个异常。
每次将条件检查替换成卫语句后,编译并测试。
如果所有卫语句都导致相同结果,请使用Consolidate Conditional Expressions(240)。
你手上有个条件表达式,它根据对象类型的不同而选择不同的行为。
将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。
做法
如果要处理的条件表达式是一个更大函数中的一部分,首先对条件表达式进行分析,然后使用Extract Method (10)将它提炼到一个独立函数去。
如果有必要,使用Move Method (142)将条件表达式放置到继承结构的顶端。
任选一个子类,在其中建立一个函数,使之覆写超类中容纳条件表达式的那个函数。将与该子类相关的条件表达式分支复制到新建函数中,并对它进行适当调整
为了顺利进行这一步骤,你可能需要将超类中的某些private字段声明为protected.
编译,测试。
在超类中删掉条件表达式内被复制了的分支。
编译,测试。
针对条件表达式的每个分支,复上:述过程,直到所有分支都被移到子类内的函数为止。
将超类之中容纳条件表达式的函数声明为抽象函数。
你需要再三检查某对象是否为null。将null值替换为null对象。
做法
为源类建立一个子类,使其行为就像是源类的null版本。在源类和null子类中都加上isNu1l()函数,前者的isNull()应该返回false,后者的isNull()应该返回true。
下面这个办法也可能对你有所帮助:建立一个nullable接口,将isNu11()函数放在其中,让源类实现这个接口.
另外,你也可以创建一个测试接口,专门用来检查对象是否为null.
编译。
找出所有“索求源对象却获得一个null” 的地方。修改这些地方,使它们改而获得一个空对象。
找出所有“将源对象与null做比较”的地方。修改这些地方,使它们调用isNull()函数。
你可以每次只处理一个源对象及其客户程序,编译并测试后,再处理另一个源对象。
你可以在“不该再出现nul1”的地方放上一些断言,确保null的确不再出现。这可能对你有所帮助。
编译,测试。
找出这样的程序点:如果对象不是nu1l,做A动作,否则做B动作。
对于每一个上述地点,在null类中覆写A动作,使其行为和B动作相同。
使用上述被覆写的动作,然后删除“对象是否等于nul1”的条件测试。编译:并测试。
某一段代码需要对程序状态做出某种假设。以断言明确表现这种假设。
做法
如果你发现代码假设某个条件始终为真,就加入一个断言明确说明这种情况。
你可以新建一个Assert类,用于处理各种情况下的断言。
函数的名称未能揭示函数的用途。修改函数名称。
做法
检查函数签名是否被超类或子类实现过。如果是,则需要针对每份实现分别进行下列步骤。
声明一个新函数,将它命名为你想要的新名称。将旧函数的代码复制到新函数中,并进行适当调整。
编译。
修改旧函数,令它将调用转发给新函数。
如果只有少数几个地方引用旧函数,你可以大胆地跳过这一步骤。
编译,测试。
找出旧函数的所有被引用点,修改它们,令它们改而引用新函数。每次修改后,编译并测试。
删除旧函数。
如果旧函数是该类public接口的一部分,你可能无法安全地删除它。这种情况下,将它保留在原处,并将它标记为deprecated (建议不使用)。
编译,测试。
某个函数需要从调用端得到更多信息。为此函数添加一个对象参数,让该对象带进函数所需信息。
做法
Add Parameter (275)的做法和Rename Method (273)非常相似。
检查函数签名是否被超类或子类实现过。如果是,则需要针对每份实现分别进行下列步骤。
声明一个新函数,名称与原函数同,只是加上新添参数。将旧函数的代码复制到新函数中。
如果需要添加的参数不止-一个,将它们一次性添加进去比较容易。
编译。
修改旧函数,令它调用新函数。
如果只有少数几个地方引用旧函数,你大可放心地跳过这一步骤.
此时,你可以给参数提供任意值。但一般来说,我们会给对象参数提供null,给内置型参数提供一个明显非正常值。对于数值型参数,我建议使用0以外的值,这样你比较容易将来认出它.
编译,测试。
找出旧函数的所有被引用点,将它们全部修改为对新函数的引用。每次修改后,编译并测试。
删除旧函数。
如果旧函数是该类public接口的一部分,你可能无法安全地删除它。这种情况下,请将它保留在原地,并将它标示为deprecated (建议不使用)。
编译,测试。
函数本体不再需要某个函数。将该参数去除。
做法
Remove Parameter (277)的做法和Rename Method (273)、Add Parameter (275)非常相似。.
检查函数签名是否被超类或子类实现过。如果是,则需要针对每份实现分别进行下列步骤。
声明一个新函数,名称与原函数同,只是去除不必要的参数。将旧函数的代码复制到新函数中。
如果需要去除的参数不止一个,将它们一次性去除比较容易。
编译。
修改旧函数,令它调用新函数。
如果只有少数几个地方引用旧函数,你大可放心地跳过这一步骤。
编译,测试。
找出旧函数的所有被引用点,将它们全部修改为对新函数的引用。每次修改后,编译并测试。
删除旧函数。
如果旧函数是该类public接口的一部分,你可能无法安全地删除它。这种情况下,将它保留在原处,并将它标记为deprecated (建议不使用)。
编译,测试。
某个函数既返回对象状态值,又修改对象状态。建立2个不同的函数,其中一个负责查询,另一个负责修改。
做法
新建一个查询函数,令它返回的值与原函数相同。
观察原函数,看它返回什么东西。如果返回的是一个临时变量,找出临时变量的位置。
修改原函数,令它调用查询函数,并返回获得的结果。
原函数中的每个return句都应该像这样: return newQuery(),而不应该返回其他东西。
如果调用者将返回值赋给了一个临时变量,你应该能够去除这个临时变量。
编译,测试。
将调用原函数的代码改为调用查询函数。然后,在调用查询函数的那一行之前,加上对原函数的调用。每次修改后,编译并测试。
将原函数的返回值改为void,并删掉其中所有的return语句。
若干函数做了类似的工作,但在函数本体中却包含了不同的值。建立一个单一函数,以参数表达那些不同的值。
做法
新建一个带有参数的函数,使它可以替换先前所有的重复性函数。
编译。
将调用旧函数的代码改为调用新函数。
编译,测试。
对所有旧函数重复上述步骤,每次替换后,修改并测试。
你有一个函数,其中完全取决于参数值而采取不同香味。针对该参数的每个可能值,建立一个独立函数。
做法
针对参数的每一种可能值,新建一个明确函数。
修改条件表达式的每个分支,使其调用合适的新函数。
修改每个分支后,编译并测试。
修改原函数的每一个被调用点,改而调用上述的某个合适的新函数
编译,测试。
所有调用端都修改完毕后,删除原函数。
你从某个对象中取出若干值,将它们作为某一次函数调用时的参数。改为传递整个对象。
做法
对你的目标函数新添一个参数项,用以代表原数据所在的完整对象。
编译,测试。
判断哪些参数可被包含在新添的完整对象中。
选择上述参数之一,将被调用函数中原来引用该参数的地方,改为调用新添参数对象的相应取值函数。
删除该项参数。
编译,测试。
针对所有可从完整对象中获得的参数,重复上述过程。
删除调用端中那些带有被删除参数的代码。
当然,如果调用端还在其他地方使用了这些参数,就不要删除它们。
编译,测试。
对象调用某个函数,并将所得结果作为参数,传递给另一个函数。而接受该参数的函数本身也能够调用前一个函数。让参数接受者去除该项参数,并直接调用前一个函数。
做法
如果有必要,将参数的计算过程提炼到一个独立函数中。
将函数本体内引用该参数的地方改为调用新建的函数。
每次替换后,修改并测试。
全部替换完成后,使用Remove Parameter (277)将该参数去掉。
某些参数总是很自然地同时出现。以一个对象取代这些参数。
做法
新建一个类,用以表现你想替换的一组参数。将这个类设为不可变的。
编译。
针对使用该组参数的所有函数,实施Add Parameter (275), 传入上述新建类的实例对象,并将此参数值设为null.
如果你所修改的函数被其他很多函数调用,那么可以保留修改前的旧函数,并令它调用修改后的新函数。你可以先对旧函数进行重构,然后逐一修改调用端使其调用新函数,最后再将旧函數删除.
对于Data Clumps中的每一项(在此均为参数),从函数签名中移除之,并修改调用端和函数本体,令它们都改而通过新的参数对象取得该值。
每去除一个参数,编译并测试。
将原先的参数全部去除之后,观察有无适当函数可以运用Move Method(142)搬移到参数对象之中。
被搬移的可能是整个函数,也可能是函数中的一个段落。如果是后者,首先使用Extract Method(110)将该段落提炼为一个独立函数,再搬移这一新建函数。
类中的某个字段应该在对象创建时被设值,然后就不再改变。去掉该字段的所有设值函数。
做法
检查设值函数被使用的情况,看它是否只被构造函数调用,或者被构造函数所调用的另一个函数调用。
修改构造函数,使其直接访问设值函数所针对的那个变量。
如果某个子类通过设值函数给超类的某个private字段设了值,那么你就不能这样修改。这种情况下你应该试着在超类中提供一个protected函数(最好是构造函数)来给这些字段设值。不论你怎么做,都不要给超类中的函数起一个与设值函数混淆的名字。
编译,测试。
移除这个设值函数,将它所针对的字段设为final。
编译,测试。
有一个函数,从来没有被其他任何类用到。将这个函数修改为private。
做法
经常检查有没有可能降低某个函数的可见度。
使用lint类的工具,尽可能频繁地检查。当你在另一个类中移除对某个函数的调用时,也应该进行检查。
特别对设值函数进行上述的检查。
尽可能降低所有函数的可见度。
每完成一组函数的隐藏之后,编译并测试。
如果有不适当的隐藏,编译器很自然会检验出来,因此不必每次修改后都进行编译。如有任何错误出现,很容易被发现。
你希望在创建对象时不仅仅是做简单的建构动作。将构造函数替换为工厂函数。
做法
新建一个工厂函数,让它调用现有的构造函数。
将调用构造函数的代码改为调用工厂函数。
每次替换后,编译并测试。
将构造函数声明为private。
编译。
某个函数返回的对象,需要由函数调用者执行向下转型(downcast)。将向下转型动作移到函数中。
做法
找出必须对函数调用结果进行向下转型的地方。
这种情况通常出现在返回一个集合或迭代器的函数中。
将向下转型动作搬移到该函数中。
针对返回集合的函数,使用Encapsulate Collection (208)。
某个函数返回一个特定的代码,用以表示某种错误情况。改用异常。
做法
决定应该抛出受控(checked)异常还是非受控(unchecked) 异常。
如果调用者有责任在调用前检查必要状态,就抛出非受控异常。
如果想抛出受控异常,你可以新建一个异常类,也可以使用现有的异常类.
找到该函数的所有调用者,对它们进行相应调整,让它们使用异常.
如果函数抛出非受控异常,那么就调整调用者,使其在调用函数前做适当检查。每次修改后,编译并测试。
如果函数抛出受控异常,那么就调整调用者,使其在try区段中调用该函数。
修改该函数的签名,令它反映出新用法。
如果函数有许多调用者,上述修改过程可能跨度太大。你可以将它分成下列数个步骤. .
决定应该抛出受控异常还是非受控异常。
新建一个函数,使用异常来表示错误状况,将旧函数的代码复制到新函数中,并做适当调整。
修改旧函数的函数本体,让它调用上述新建函数。
编译,测试。
逐一修改旧函数的调用者,令其调用新函数。每次修改后,编译并测试。
移除旧函数。
面对一个调用者可以预先检查的条件,你抛出一个异常。修改调用者,使它在调用函数之前先做检查。
做法
在函数调用点之前,放置一个测试语句,将函数内catch区段中的代码复制到测试句的适当if分支中。
在catch区段起始处加入一个断言, 确保catch区段绝对不会被执行。
编译,测试。
移除所有catch区段,然后将try区段内的代码复制到try之外,然后移除try区段。
编译,测试。
两个子类拥有相同的字段。
将该字段移至超类。
做法
针对待提升之字段,检查它们的所有被使用点,确认它们以同样的方式被使用。
如果这些字段的名称不同,先将它们改名,使每一个名称都和你想为超类字段取的名称相同。
编译,测试。
在超类中新建一个字段。
如果这些字段是private的,你必须将超类的字段声明为protected,这样子类才能引用它。
移除子类中的字段。
编译,测试。
考虑对超类的新建字段使用Self Encapsulate Field (171)。
有些函数,在各个子类中产生完全相同的结果。将该函数移至超类。
做法
检查待提升函数,确定它们是完全一致的。
如果这些函数看上去做了相同的事,但并不完全一致,可使用Substitute Algorithm (139)让它们变得完全一致。
如果待提升函数的签名不同,将那些签名都修改为你想要在超类中使用的签名。
在超类中新建一个函数,将某一个待提升函数的代码复制到其中,做适当调整,然后编译。
如果你使用的是一种强类型语言,而待提升函数又调用了一个只出现于子类而未出现于超类的函数,你可以在超类中为被调用函数声明一个抽象函数。
如果待提升函数使用了子类的一个字段,你可以使用Pull Up Field (320)将该字段也提升到超类;或者也可以先使用Self Encapsulate Field (171),然后在超类中把取值函数声明为抽象函数。
移除一个待提升的子类函数。
编译,测试。
逐一移除待提升的子类函数,直到只剩下超类中的函数为止。每次移除之后都需要测试。
观察该函数的调用者,看看是否可以改为使用超类类型的对象。
你在各个子类中拥有一些构造函数,它们的本体几乎完全一致。在超类中新建一个构造函数,并在子类构造函数中调用它。
做法
在超类中定义一个构造函数。
将子类构造函数中的共同代码搬移到超类构造函数中。
被搬移的可能是子类构造函数的全部内容。
首先设法将共同代码搬移到子类构造函数起始处,然后再复制到超类构造函数中。
将子类构造函数中的共同代码删掉,改而调用新建的超类构造函数。
如果子类构造函数中的所有代码都是一样的,那么子类构造函数就只需要调用超类构造函数。
编译,测试。
如果日后子类构造函数再出现共同代码,你可以首先使用Extract Method(110)将那一部分提炼到一个独立函数,然后使用Pull Up Method (322 )将该函数上移到超类。
超类中的某个函数只与部分(而非全部)子类有关。将这个函数移到相关的那些子类去。
做法
在所有子类中声明该函数,将超类中的函数本体复制到每一个子类函数中。
你可能需要将超类的某些字段声明为protected,让子类函数也能够访问它们。如果日后你也想把这些字段下移到子类,通常就可以那么做;否则应该使用超类提供的访问函数。如果访问函数并非public,你得将它声明为protected.
删除超类中的函数。
你可能必须修改调用端的某些变量声明或参数声明,以便能够使用子类。
如果有必要通过一个超类对象访问该函数,或你不想把该函数从任何子类中移除,再或超类是抽象类,那么你就可以在超类中把该函数声明为抽象函数。
编译,测试。
将该函数从所有不需要它的那些子类中删掉。
编译,测试。
超类中的某个字段只被部分(而非全部)子类用到。将这个字段移到需要它的那些子类去。
做法
在所有子类中声明该字段。
将该字段从超类中移除。
编译,测试。
将该字段从所有不需要它的那些子类中删掉。
编译,测试。
类中的某些特性只被某些(而非全部)实例用到。
新建一个子类,将上面所说的那一部分特性移到子类中。
做法
为源类定义一个新的子类。
为这个新的子类提供构造函数。
简单的做法是:让子类构造函数接受与超类构造函数相同的参数,并通过super调用超类构造函数。
如果你希望对用户隐藏子类的存在,可使用Replace Constructor withFactory Method (304).
找出调用超类构造函数的所有地点。如果它们需要的是新建的子类,令它们改而调用新构造函数。
如果子类构造函数需要的参数和超类构造函数的参数不同,可以使用Rename Method (273)修改其参数列.如果子类构造函数不需要超类构造函数的某些参数,可以使用Rename Method (273)将它们去除。
如果不再需要直接创建超类的实例,就将超类声明为抽象类.
逐一使用Push Down Method (328)和Push Down Field (329)将源类的特性移到子类去。
和Extract Class (149)不同的是,先处理函数再处理数据,通常会简单一些。
当一个public函数被下移到子类后,你可能需要重新定义该函数的调用端的局部变量或参数类型,让它们改而调用子类中的新函数。如果忘记进行这一步骤,编译器会提醒你。
找到所有这样的字段:它们所传达的信息如今可由继承体系自身传达(这一类字段通常是boolean变量或类型码)。以Self Encapsulate Field (171)避免直接使用这些字段,然后将它们的取值函数替换为多态常量函数。所有使用这些字段的地方都应该以 Replace Conditional with Polymorphism (255)重构。
任何函数如果位于源类之外,而又使用了上述字段的访问函数,考虑以Move Method (142)将它移到源类中,然后再使用Replace Conditional withPolymorphism (255).
每次下移之后,编译并测试。
两个类有相似特性。为这两个类建立一个超类,将相同特性移至超类。
做法
为原本的类新建一个空白的抽象超类。
运用Pull Up Field (320)、Pull Up Method (322)和Pull Up Constructor Body(325)逐一将子类的共同元素上移到超类。
先搬移字段,通常比较简单。
如果相应的子类函数有不同的签名,但用途相同,可以先使用Rename Method (273)将它们的签名改为相同,然后再使用Pull Up Method (322)。
如果相应的子类函数有相同的签名,但函数本体不同,可以在超类中把它们的共同签名声明为抽象函数。
如果相应的子类函数有不同的函数本体,但用途相同,可试着使用Substitute Algorithm (139)把其中-一个函数的函数本体复制到另一个函数中。如果运转正常,你就可以使用Pull Up Method (322)。
每次上移后,编译并测试。
检查留在子类中的函数,看它们是否还有共通成分。如果有,可以使用Extract Method(110)将共通部分再提炼出来,然后使用Pull Up Method (322)将提炼出的函数上移到超类。如果各个子类中某个函数的整体流程很相似,你也许可以使用Form Template Method (345)。
将所有共同元素都上移到超类之后,检查子类的所有用户。如果它们只使用共同接口,你就可以把它们请求的对象类型改为超类。
若干客户使用类接口中的同一子集,或者两个类的接口有部分相同。将相同的子集提炼到一个独立接口中。
做法
新建一个空接口。
在接口中声明待提炼类的共通操作。
让相关的类实现上述接口。
调整客户端的类型声明,令其使用该接口。
超类和子类之间无太大区别。将它们合为一体。
做法
选择你想移除的类:是超类还是子类?
使用Pull up Field (320)和Pull up Method (322),或者Push Down Method (328)和Push Down Field (329),把想要移除的类的所有行为和数据搬移到另一个类。
每次移动后,编译并测试。
调整即将被移除的那个类的所有引用点,令它们改而引用合并后留下的类。这个动作将会影响变量的声明、参数的类型以及构造函数。
移除我们的目标;此时的它应该已经成为一个空类。
编译,测试。
你有一些子类,其中相应的某些函数以相同顺序执行类似的操作,但各个操作的细节上有所不同。
将这些操作分别放进独立函数中,并保持它们都有相同的签名,于是原函数也就变得相同了。然后将原函数上移至超类。
做法
在各个子类中分解目标函数,使分解后的各个函数要不完全相同,要不完全不同。
运用Pull Up Method (322)将各子类内完全相同的函数上移至超类。
对于那些(剩余的、存在于各子类内的)完全不同的函数,实施Rename Method(273),使所有这些函数的签名完全相同。
这将使得原函数变为完全相同,因为它们都执行同样一组函数调用;但各子类会以不同方式响应这些调用。
修改上述所有签名后,编译并测试。
运用Pull Up Method (322)将所有原函数逐一上移至超类。在超类中将那些代表各种不同操作的函数定义为抽象函数。
编译,测试。
移除其他子类中的原函数,每删除一个,编译并测试。
某个子类只使用超类接口中的一部分, 或是根本不需要继承而来的数据。
在子类中新建一个字段用以保存超类;调整子类函数,令它改而委托超类;然后去掉两者之间的继承关系。
做法
在子类中新建一个字段,使其引用超类的一个实例, 并将它初始化为this.
修改子类内的所有函数,让它们不再使用超类,转而使用上述那个受托字段。每次修改后,编译并测试。
你不能这样修改子类中通过super调用超类函数的代码,否则它们会陷入无限递归。这种函数只有在继承关系被打破后才能修改。
去除两个类之间的继承关系,新建一一个受托类的对象赋给受托字段。
针对客户端所用的每一个超类函数,为它添加一个简单的委托函数。
编译,测试。
你在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数。让委托类继承受托类。
做法
让委托端成为受托端的一个子类。
编译。
此时,某些函数可能会发生冲突:它们可能有相同的名称,但在返回类型、异常指定或可见程度方面有所差异。你可以使用Remane Method (273)解决此类问题。
将受托字段设为该字段所处对象本身。
去掉简单的委托函数。
编译并测试。
将所有其他涉及委托关系的代码,改为调用对象自身。
移除受托字段。
某个继承体系同时承担两项责任。建立两个继承体系,并通过委托关系让其中一个可以调用另一个。
做法
首先识别出继承体系所承担的不同责任,然后建立一个二维表格(或者三维乃至四维表格,如果你的继承体系够混乱而你的绘图工具够酷的话),并以坐标轴标示出不同的任务。我们将重复运用本重构,处理两个或两个以上的维度(当然,每次只处理一个维度)。
判断哪一项责任更重要些,并准备将它留在当前的继承体系中。准备将另一项责任移到另一个继承体系中。
使用Exract Class (149)从当前的超类提炼出一个新类, 用以表示重要性稍低的责任,并在原超类中添加一个实例变量,用以保存新类的实例。
对应于原继承体系中的每个子类,创建上述新类的一个子类。在原继承体系的子类中,将前一步骤所添加的实例变量初始化为新建子类的实例。
针对原继承体系中的每个子类,使用Move Method (142)将其中的行为搬移到与之对应的新建子类中。
当原继承体系中的某个子类不再有任何代码时,就将它去除。
重复以上步骤,直到原继承体系中的所有子类都被处理过为止。观察新继承体系,看看是否有可能对它实施其他重构手法,例如Pull Up Method (322)或Pull Up Field (320)。
你手上有一些传统过程化风格的代码。将数据记录变成对象,将大块的行为分成小块,并将行为移入相关对象之中。
做法
针对每一个记录类型,将其转变为只含访问函数的哑数据对象。
如果你的数据来自关系式数据库,就把数据库中的每个表变成一个哑数据对象。
针对每一处过程化风格,将该处的代码提炼到一个独立类中。
你可以把提炼所得的类做成一个Singleton(为了方便重新初始化),或是把提炼所得的函数声明为static.
针对每一段长长的程序,实施Extract Method (110)及其他相关重构将它分解。再以MoveMethod(142)将分解后的函数分别移到它所相关的哑数据类中。
重复上述步骤,直到原始类中的所有函数都被移除。如果原始类是一个完全过程化的类,将它拿掉将大快人心。
某些GUI类之中包含了领域逻辑。将领域逻辑分离出来,为它们建立独立的领域类。
做法
为每个窗口建立一个领域类。
如果窗口内有一张表格,新建一个类来表示其中的行,再以窗口所对应之领域类中的一一个集合来容纳所有的行领域对象。
检查窗口中的数据。如果数据只被用于UI,就把它留着;如果数据被领域逻辑使用,而且不显示于窗口上,我们就以Move Field (146)将它搬移到领域类中;如果数据同时被UI和领域逻辑使用,就对它实施Duplicate Observed Data(189),使它同时存在于两处,并保持两处之间的同步。
检查展现类中的逻辑。实施Extract Method (110)将展现逻辑从领域逻辑中分开。一旦隔离了领域逻辑,再运用Move Method (142)将它移到领域类。
以上步骤完成后,你就拥有了两组彼此分离的类:展现类用以处理GUI,领域类包含所有业务逻辑。此时的领域类组织可能还不够严谨,更进一步的重构将解决这些问题。
你有某个类做了太多工作,其中一部分工作是以大量条件表达式完成的。
建立继承体系,以一个子类表示一种特殊情况。
做法
我们为你准备了两组重构做法。第一种情况是:你无法确定哪些地方会发生变化。这时候你会希望每次一小步地前进。
鉴别出一种变化情况。
如果这种变化可能在对象生命周期的不同阶段而有不同体现,就运用Extract Class (149)将它提炼为一个独立的类。
针对这种变化情况,新建一个子类,并对原始类实施Replace Constructor with Factory Method (304)。再修改工厂函数,令它返回适当的子类实例。
将含有条件逻辑的函数,一次一个,逐一复制到子类,然后在明确情况下(对子类明确,对超类不明确),简化这些函数。
如有必要隔离函数中的条件逻辑和非条件逻辑,可对超类实施ExtractMethod (110)。
重复上述过程,将所有变化情况都分离出来,直到可以将超类声明为抽象类为止。
删除超类中那些被所有子类覆写的函数本体,并将它们声明为抽象函数。
如果你非常清楚原始类会有哪些变化情况,可以使用另一种做法。
针对原始类的每一种变化情况,建立一个子类。
使用Replace Constructor with Factory Method (304)将原始类的构造函数转变成工厂函数,并令它针对每一种变化情况返回适当的子类实例。
如果原始类中的各种变化情况是以类型码标示,先使用ReplaceTypeCode with Subelasses (223);如果那些变化情况在对象生命周期的不同阶段会有不同体现,请使用Replace Tjype Code with State/Strategy (227).
针对带有条件逻辑的函数,实施Replace Conditional with Polymorphism (255)。
如果并非整个函数的行为有所变化,而只是函数一部分有所变化,请先运用Extract Method (110)将变化部分和不变部分隔开来。