重构是不改变软件可观察行为的前提下改善其内部结构(在代码写好之后改进它的设计)
如果在最初开发和整个开发过程中一直遵循一些良好的设计原则,那么重构过程会更轻松,软件的进化会更容易
导致程序难以修改的原因
- 难以阅读
- 逻辑重复
- 添加新行为时需要修改已有代码
- 带复杂条件逻辑
为什么重构
- 重构改进软件设计
- 由于弥补设计错误所需的成本降低了,需要预先设计也就更少了
- 预先设计是一项带有预测性质的工作,因为项目激活之时,需求往往还不明确,所以正确的设计方式是:尽力那个简化需求尚未明朗的那一部分代码
- 重构使软件更容易理解
- 重构帮助找到bug
- 重构提高编程速度
- 调整程序结构以使(短期内)添加新功能更容易
间接层的价值
间接层是一把双刃剑
- 缺点:
- 每次把一个东西分成两份,你就需要多管理一个东西
- 如果某个对象委托另一对象,层级较多后程序会愈加难以阅读
- 优点:(解释能力、共享能力、选择能力)
- 实现逻辑共享
- 分开解释意图和实现
- 类名、函数名和功能分解帮助我们理解程序和意图
- 隔离变化
- 封装条件逻辑(如状态模式代替switch)
重构难题
- 数据库
- 在对象模型和数据库模型之间插入一个分割层,隔离两个模型各自的变化
- 接口
- 让就接口调用新接口
- 接口异常使用自定义的基类异常,这样避免日后子类异常类型带来的接口修改负担
- 重写
- 现在代码根本不能正常运作(大部分情况下)作为一个清楚讯号
- 拆分组件后考虑各模块重构还是重写
- 现在代码根本不能正常运作(大部分情况下)作为一个清楚讯号
- 性能
- 重构有助于找到影响性能的一小段代码进行优化
重构时机
- 三次法则
- 第一次做某事时只管去做
- 第二次做类似的事情会反感,但无论如何还是可以去做
- 第三次再做类似的事,你就应该重构
- 添加功能时
- 如果你发现自己需要程序添加一个特性,而代码结构使你无法很方便的达成目的,那就先重构那个程序,是特性的添加比较容易进行,然后再添加特性
- 修补错误时
- 如果收到一份错误报告,这就是需要重构的信号,因为程序没有清晰到让你一眼看出bug
- 代码复审时
- 重构可以帮助代码复审工作得到更具体的结果,不仅获得建议,而其中许多建议能立刻实现
什么样的代码需要重构
- 重复代码(Duplicated Code)
- 重复代码应该封装成一个方法并放置到一个该处的类中
- 过长函数(Long Method)
- 每当感觉需要用注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数,并以用途命名
- 过大的类(Large Class)
- 相同或相近的变量和方法放到共同的类中,对大类进行拆分
- 过长参数列表(Long Parameter List)
- 直接传递对象或封装参数对象
- 发散式变化(Divergent Change)(一个类受多种变化影响)
- 将引起变化的不同原因封装至不同的类中
- 散弹式修改(Shotgun Surgery)(一个变化引发多个类修改)
- 讲一个原因引起的变化封装到同一个类中
- 依恋情结(Feature Envy)(函数对某类的兴趣高于所处类的兴趣)
- 将数据和数据操作封装到一起,而不是由其他类大量调用get方法(除非使用访问者模式和策略模式)
- 将总一起变化的东西放到一块,使变化再一地发生
- 数据泥团(Data Clumps)(两个类中的相同字段、参数列表相同的参数出现)
- 删掉众多数据想中的一项,如果其他数据项因此失去意义,你应该把这些数据项封装到一个对象中
- 可以有效帮助缓解依恋情结(Feature Envy)
- 基本类型偏执(Primitive Obsession)(新手通常不愿意在现任务上运用小对象,如数据类型和数据区间等)
- 结构类型允许你将数据组织成有意义的形式;基本数据类型则是组成结构类型的积木块
- switch语句(Switch Statements)
- 考虑使用子类、Strategy模式、Strate模式替换switch语句
- 平行继承体系(Parallel Inheritance Hierarchies)(为某个类增加子类,也必须为另外一个类增加子类)
- 使用一个类实例引用另外一个,或直接合并两个类
- 是散弹式修改(Shotgun Surgery)的特例
- 冗赘类(Lazy Class)
- 如果某个类或者子类预先设计的变化没有发生,删除它
- 夸夸其谈未来性(过度设计的类和方法 Speculative Generality)(当前用不到的设计过度预留)
- 如果函数或类的用户只有测试用例,而又没有辅助检测正常功能,删除它们
- 令人迷惑的临时字段(Temporary Field)(某个变量仅为特定情况而设)
- 将不一定用到的变量和算法提取并放到合适的类中
- 过度耦合的消息链(Message Chains)(过长的委托链)
- 将消息链底层的对象调用封装成方法并放置在链中合适的位置
- 中间人(Middle Man)(过度委托造成中间类作用有限)
- 删除中间类或使其变成子类以保留功能
- 狎昵关系(过度亲密的类 Inappropriate Intimacy)(过分探究private成分的两个类)
- 重新规划方法和变量所处位置
- 提取公共部分
- 转变为单向调用而不是双向调用
- 将继承变为委托
- 异曲同工的类(Alternative Classes with Diffent Interface)
- 重命名并合并两个类及其接口
- 不完美的类库(Incomplete Library Class)(类库不能满足需求)
- 自定义方法补足或或者使用继承体系等合并两个类库
- 纯稚的数据类(Data Class)(public字段的数据类)
- 为数据类的字段增加访问控制和必要的集合操作
- 拒绝馈赠(Refused Bequest)(继承了不必要的方法或属性)
- 父类拆分方法至子类(传统做法,不推荐)
- 使用委托处理本不开出现在继承体系的成员
- 过多注释(Comments)(使用注释解释代码,而掩盖代码的不足)
- 重构代码,删除多余的解释
- 当你尝试写代码时,尝试重构,试着让注释变得多余
- 注释应该用作打草稿,或者记录没有十足把我的区域
Bad Tastes | 常用重构 |
---|---|
重复代码 | Extract Method Extract Class Pull Up Method Form Template Method |
过长函数 | Extract Method Replace Temp with Query Replace Method with Method Object Decompose Conditional |
过大的类 | Extract Class Extract Subclass Extract Interface Replace Data Value with Object |
过长参数列 | Replace Parameter with Method Introduce Parameter Object Preserve Whole Object |
发散式变化 | Extract Class |
依恋情结 | Move Method Move Field Extract Method |
散弹式修改 | Move Method Move Field Inline Class |
数据泥团 | Extract Class Introduce Parameter Object Preserve Whole Object |
基本类型偏执 | Replace Data Value with Object Extract Class Introduce Parameter Object Replace Array with Object Replace Type Code with Class Replace Type Code with Strate/Strategy Replace Type Code with Subclasses |
switch语句 | Replace Conditional with Polymorphism Replace Type Code with Strate/Strategy Replace Type Code with Subclasses Replace Parameter with Explicit Method Introduce Null Object |
冗赘类 | Inline Class Collapse Hierarchy |
平行继承体系 | Move Method Move Field |
夸夸其谈未来性 | Collapse Hierarchy Inline Class Remove Parameter Rename Method |
过度耦合的消息链 | Hide Delegate |
令人迷惑的临时字段 | Extract Class Introduce Null Object |
异曲同工的类 | Rename Method Move Method |
狎昵关系 | Move Method Move Field Change Bidirectional Association to Unidirectional Replace Inheritance with Delegation Hide Delegate |
中间人 | Remove Middle Man Inline Method Replace Delegation with Inheritance |
纯稚的数据类 | Move Method Encapsulate Field Encapsulate Collection |
不完美的类库 | Introduce Foreign Method Introduce Local Extension |
过多注释 | Extract Method Introduce Assertion |
拒绝馈赠 | Replace Inheritance with Delegation |
构筑测试体系
重构之前,首先检查自己是否有一套可靠的测试机制,这些测试必须有自我检验能力
编写良好的测试程序可以极大提高编程速度,编写代码通常占开发的小部分时间,一些时间用来决定下一步干什么,另一些时间花在设计上,最多的时间是用来调试
确保所有测试都完全自动化,让它们检查自己的测试结果
频繁的运行测试,每次编译请把测试也考虑进去,每天至少运行每个测试一次
每当你收到一个测试报告,清写要给单元测试来暴漏bug
观察类所做的事,然后针对任何一种可能失败的情况,进行测试(测试应该是一种风险驱动行为,测试的目的是希望找出现在或未来可能出现的错误)
测试你最担心出错的部分
考虑可能出错的边界
当事情被认为应该会出错时,别忘了检查是否抛出了预期的异常
重构手法
重新组织函数
- Extract Method(提炼函数)
- 处理过长函数
- Inline Method(内联函数)
- 去除无用的未得到复用的函数
- Inline Temp(内联临时变量)
- 去除无用的未得到复用的变量Query
- Replace Temp with Query(以查询取代临时变量)
- 使用查询方法去除临时变量
- Introduce Explaining Variable(引入解释性变量)
- 降低表达式的复杂程度
- Split Temporary Variable(分解临时变量)
- 分解临时变量而不是每次使用一个变量赋值
- Remove Assignment to Parameters(移除对参数的赋值)
- 对临时变量赋值替代对输入参数赋值
- Replace Method with Method Object(以函数对象取代函数)
- 分解混乱的函数,代价是引入一个新的类
- Substitute Algorithm(替换算法)
- 引入更清晰的算法替代现有算法
Extract Method(提炼函数)
表现:你有一段代码可以被组织在一起并独立出来
手法:将这段代码放进一个独立函数中,并让函数名称(清晰到可以解释函数用途)解释函数的用途
函数短小且良好命名的好处
- 每个函数的粒度都很小,那么函数被复用的机会都更大
- 这会使高层函数读起来就像一系列注释
- 如果函数都是细粒度,那么函数的覆写也会更容易些
局部变量问题(在被提炼函数中被赋值)
- 只在被提炼代码段中使用
- 将生命也提炼到新方法中
- 被提炼代码之外的代码也使用了这个变量
- 在被提炼代码之后未使用
- 直接在被提炼代码中修改
- 在被提炼代码之后未使用
- 让目标函数返回该变量修改后的值
- 如果返回值较多可以分开提炼代码块保持一个返回值是一个好实践
- 在被提炼代码之后未使用
注意事项:如果入参被直接赋值,请使用 Remove Assignment to Parameters
Inline Method(内联函数)
表现:一个函数的本体与名称同样很容易弄懂
手法:在函数调用点插入函数主体,然后删除函数
Inline Temp(内联临时变量)
表现:一个临时变量,只被一个简单表达式赋值一次,而他妨碍了其他重构手法
手法:将所有对该变量的引用动作,替换为对它赋值的那个表达式自身
Tips:将变量声明为final检查是否只被赋值一次
注意事项:Inline Temp会造成性能问题(被计算多次)
Replace Temp with Query(以查询取代临时变量)
表现:程序以一个临时变量保存某一表达式的运算结果
手法:将这个表达式提炼到一个独立函数中。将这个临时变量的所有引用替换为对新函数的调用
Tips:如果变量被多次赋值,考虑使用Split Temporary Variable
Tips:操作后对变量执行Inline Temp
Introduce Explaining Variable(引入解释性变量)
表现:一个复杂的表达式
手法:将复杂表达式的部分结果放进一个临时变量
Tips:这个方法并不常用,取而代之的是Extract Method,只有Extract Method工作量很大是使用它替代
Split Temporary Variable(分解临时变量)
表现:程序有某个临时变量被赋值超过一次(承担了多个责任),它既不是循环变量,也不被用于收集计算结果
手法:针对每次赋值,创造一个独立、对应的临时变量
Remove Assignment to Parameters(移除对参数的赋值)
表现:代码对一个参数进行赋值
手法:以一个临时变量取代参数的位置
注意事项:对参数的赋值在引用传递的参数上指改变其指向,是无意义的,而对参数对象的操作是没问题的
Tips:如果代码语义是引用传递,要检查调用后是否还使用了这个参数
Tips:尽量以return方式返回一个值
Replace Method with Method Object(以函数对象取代函数)
表现:有个大型函数,其中对局部变量的使用使你无法采用Extract Method
手法:将这个函数放进一个单独的对象中,如此局部变量变成了成员变量,然后你可以使用Extract Method分解它为多个小函数
做法:
- 在新类中建立一个final字段的当前类引用(源对象)
- 在新类中生命函数用到的临时变量为成员变量
- 建立构造函数入参为上面建立的所有变量
- 建立一个compute()函数,讲原函数的内容拷贝进去,并将原函数替换为对象函数调用
- 将原来函数中引用了其他函数的部分委托给源对象
- 重构这个大方法而不必考虑参数传递的问题
Substitute Algorithm(替换算法)
表现:把某个算法替换为另一个更清晰的算法
手法:将函数体替换为另一个算法
Tips:对于新旧算法分别用所有测试用例执行,找出问题*
在对象之间搬移特性
- Move Method(搬移函数)
- 移动行为
- Move Field(搬移字段)
- 移动属性
- Extract Class(提炼类)
- 分离职责
- Inline Class(将类内联化)
- 去除职责过少的类
- Hide Delegate(隐藏委托关系)
- 降低委托类变化的影响范围
- Remove Middle Man(移除中间人)
- 去除单一委托职责的类
- Introduce Foreign Method(引入外加函数)
- 加入不可修改的类的一两个方法
- Introduce Local Extension(引入本地扩展)
- 加入不可修改的类的多个方法
Move Method(搬移函数)
表现:有函数与起所驻类之外的另一个类进行许多交流,调用后者,或被后者调用
手法:在该函数最常引用的类中建立一个有着类似行为的新函数,将酒函数变成一个单纯的委托函数,或是完全移除
做法:
- 检查源类中被源函数所使用的一切特性,考虑它是否也该被搬移
- 检查源类和超类,是否有函数的其他声明
- 再目标类生命这个函数,可以选择一个新的名字
- 复制函数内容至新函数,检查对源类调用依赖如何引用(如传递对象引用)和异常处理
- 将依赖特性移动至目标类
- 建立或使用一个从目标类到源类的引用关系
- 将源对象作为参数传递给目标类
- 将引用变量作为参数传递给目标类
- 决定源类如何调用新的引用
- 修改源函数为委托函数
- 决定是否删除源函数
Tips:如果某个特性只有打算搬移的函数用到,那也应该一并搬移
Tips:如果目标函数设计过多源函数引用,需要考虑拆分函数再搬移
Move Field(搬移字段)
表现:某个字段被其所驻类之外的类更多的用到
手法:再目标类建立一个新字段,修改源字段的所有用户,令他们改用新的字段
Tips:使用Self-Encapsulation(自我封装)的字段,可以通过修改访问函数后迁移字段,引用点无需修改
Extract Class(提炼类)
表现:某个类做了应该由两个类做的事
手法:建立一个新类,将相关字段和函数从旧类中搬移过去
做法:
- 决定如何分解一个类所负的责任
- 建立一个新类用以表现旧类分离出来的责任
- 如果剩余的类名称不符,就改名
- 建立新类和旧类的链接关系
- 你可能需要双向链接,但是在真正需要它之前,不要建立
- 搬移字段
- 搬移方法,优先搬移被依赖的或者被依赖多的
- 检查并精简类的接口
- 如果有双向链接,检查是否可以替换为单向链接
- 决定是否公开新类
- 允许任何对象修改新类的任何部分,新类与旧类一对多时应该考虑使用Change Value to Reference
- 不允许任何人不通过旧类修改新类对象,设置新类为不可修改的,提供不可修改的接口
- 传递一个复制的新对象给用户,但是会造成迷惑
Tips:一个类应该是一个清楚的抽象,处理一些明确的责任
Tips:如果一个类需要通过多个子类化的方式分解特性,你需要分解这个类
Tips:Extract Class是改善并发的一种常用技术,它可以为两个类分别加锁,同时也造成两个类需要同时加锁的事务问题
Inline Class(将类内联化)
表现:某个类没有做太多事情(通常因为重构移走了部分类的责任)
手法:将这个类所有特性搬移到另一个类(最频繁用户)中,然后移除原类
做法:
- 在目标类中声明源类的public函数,并将功能委托给源类
- 考虑是否提炼接口
- 将所有源类引用点,改为目标类
- 使用Move Field和Move Method,移动源类特性
- 删除源类
Hide Delegate(隐藏委托关系)
表现:客户通过一个委托类调用另一个对象
手法:在服务类上建立客户所需的所有函数,用以隐藏委托关系
如果一个对象先通过一个服务对象的字段得到另一个对象,然后调用后者的函数,那么客户就必须直销这一层委托关系
万一委托关系发生变化,客户端也得相应变化
可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,去除这种依赖
注意事项:代价是每当用户要使用委托类的新特性时,就必须在服务端添加一个简单委托函数,随着委托的增加,服务类就变成了一个中间人
Remove Middle Man(移除中间人)
表现:某个类做了过多的简单委托动作
手法:让客户直接调用委托类
Introduce Foreign Method(引入外加函数)
表现:需要为提供服务的类增加一个函数,但你无法修改这个类
手法:在客户类中建立一个函数,并以第一参数形式传入一个服务类实例
做法:
- 在客户类中建立一个函数,用来提供你需要的功能
- 这个而函数不应该调用客户端的任何特性,如果它需要一个值,把值作为参数传给它
- 以服务类实例作为该函数的第一个参数
- 将函数注释为外加函数,以便以后修改
注意事项:如果你以外加函数实现一项功能,那说明函数元贝应该在提供服务的类中实现
注意事项:如果大量的外加函数需要添加,使用Introduce Local Extension
Introduce Local Extension(引入本地扩展)
表现:你需要为服务类提供一些额外函数,但你无法修改这个类
手法:建立一个新类,使它包含这些额外函数,让这个扩展品成为源类的子类或包装类
做法:
- 建立扩展类,将它作为原始类的子类或包装类
- 在扩展类中加入转型构造函数
- 子类应调用合适的父类构造
- 扩展类应接受原始类对象参数用于委托
- 在扩展类中加入新特性
- 替换原对象的调用为扩展类
- 迁移外加函数到本地扩展
注意事项:扩展类是一个独立的类,它提供源类的一切特性,同时额外添加新特性,再任何使用源类的地方,都应可以使用本地扩展
注意事项:包装类的扩展需要委托源类中的方法,推荐使用方法重载分开处理以原始类和包装类为参数的方法如equals()等
注意事项:子类扩展不建议覆写原始类方法,会出现上面的问题,也需要重载
Tips:在可以接管源类创建过程时,首选子类,否则选择包装类
重新组织数据
- Self Encapsulate Field(自封装字段)
- 改变数据访问形式以求灵活的数据数据管理方式
- Replace Data Value with Object(以对象取代数据值)
- 增加简单数据类型的行为和属性
- Change Value to Reference(将值对象改为引用对象)
- 共享值对象
- Change Reference to Value(将引用对象转变为值对象)
- Replace Array with Object(以对象取代数组)
- 处理存储不同对象的数组
- Duplicate Observed Data(复制被监视数据)
- 通过观察者模式将数据对象及操作从GUI层独立出来
- Change Unidirectional Association to Bidirectional(将单向关联改为双向关联)
- 增加双向引用
- Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)
- 去除无用的双向引用
- Replace Magic Number with Symbolic Constant(以字面常量取代魔法数)
- 统一常量引用及修改
- Encapsulate Field(封装字段)
- 取值函数控制字段修改行为
- Encapsulate Collection(封装集合)
- 防止不通过所属对象修改集合
- Replace Record with Data Class(以数据取代记录)
- 将数据记录映射为对象
- Replace Type Code with Class(以类取代类型码)
- 数值类型码约束
- Replace Type Code with Subclasses(以子类取代类型码)
- 封装状态码及相关的行为
- Replace Type Code with Strate/Strategy(以状态模式或策略模式取代类型码)
- 不能使用继承时使用它封装状态码及相关的行为
- Replace Subclass with Fields(以字段取代子类)
- 处理仅有常量字段差异的子类
Self Encapsulate Field(自封装字段)
表现:你直接访问一个字段,但与字段之间的耦合关系逐渐变的笨拙
手法:为这个字段建立取值/设值函数,并且只以这些函数来访问字段
字段访问方式
- 直接访问
- 代码容易阅读
- 间接访问
- 子类可以通过覆写一个函数而改变获取数据的途径
- 支持灵活的数据管理方式,如延迟加载
Tips:需要时使用间接,否则使用直接,在恰当的时机重构它
Tips:设值函数被认为应该在对象创建后使用,初始化时行为和设值函数行为不同时,直接访问字段
Replace Data Value with Object(以对象取代数据值)
表现:你有一个数据项,需要与其他数据和行为一起使用才有意义(简单数据变得需要有行为和属性)
手法:将数据项变为对象
做法:
- 为待替换的数值新建一个类,在其中声明一个final字段,并增加get、set方法和构造方法
- 将源类中的数值字段替换为新类
- 修改源类中的取值函数,使它调用新类的取值函数
- 如果构造函数中使用这个待替换字段,我们就修改构造函数调用新类的构造函数
- 修改设值函数,令他为新类创建一个实例
- 查看是否需要使用Change Value to Reference
Tips:值对象应该是不可修改内容的,保证多个对象表示同一事物
Change Value to Reference(将值对象改为引用对象)
表现:你从一个类衍生出许多彼此相等的实例,希望将它们替换为同一个对象
手法:将值对象变更为引用对象
做法:
- 使用Replace Constructor with Factory Method
- 决定由什么对象提供新对象的访问路径
- 决定引用对象应该预先创建还是动态创建
- 修改工厂函数,令他返回引用对象
- 对象不存在的处理
- 测试
注意事项:引用对象有同步问题
Change Reference to Value(将引用对象转变为值对象)
表现:你有一个引用对象,很小且不可变(不可变指值对象本身不可变,与值对象关系可变,可关联不同值对象),且不易管理
手法:把它变成值对象
做法:
- 检查重构目标是否可以变为不可变对象,或者是否可以修改为不可变对象
- 如果可修改,使用Remove Setting Method直至它变成不可变对象为止
- 如果无法将对象变成不可变对象,放弃修改
- 建立equals()和hashCode()方法
- 测试
- 考虑是否可以删除工厂行数,并将构造函数声明为public
Replace Array with Object(以对象取代数组)
表现:一个数组的元素各自代表不同的东西
手法:以对象替换数组,用字段表示数组中每一个元素
Duplicate Observed Data(复制被监视数据)
表现:你有一些领域数据置身于GUI组件中,而领域函数需要访问这些数据(同步GUI对象中的数据到Model中,对Model数据的操作同步回GUI对象,使操作从GUI对象中解耦)
手法:将该数据复制到一个领域对象中,建立一个Observer模式,用以同步领域对象和GUI对象内的重复数据
Change Unidirectional Association to Bidirectional(将单向关联改为双向关联)
表现:两个类都需要使用对方的特性,但其间只有一个单向连接
手法:添加一个反向指针,并修改函数使之函数能够同时更新两条连接
做法:
- 在被引用类中增加一个字段,用来保存反向指针
- 决定由哪个类控制关联关系
- 如果是一对多关系,就让单一引用方(多)控制关联关系
- 如果某个对象是组成另一个对象的部件,由后者控制关联关系
- 如果晾着都是引用对象,多对多关系,随意其中一方控制关联关系
- 在控制端建立一个辅助函数,其命名应该清楚的指出它的有限用途
- 如果既有的修改函数在控制端,让它负责更新反向指针
- 如果既有的修改函数在被控制端,就在控制端建立一个控制函数,并让既有的修改函数调用这个新建的控制函数
注意事项:在处理关联关系时,尤其时一对多关系时,添加关系的同时需要先考虑解除之前的关系(维护双向连接,确保对象被正确的创建和删除)
注意事项:大量的双向连接也很容易造成“僵尸对象”,因为引用没有完全清除
注意事项:双向关联也迫使两个类之间有了依赖,一个类修改会引发另一个类的变化
Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)
表现:两个类之间有双向关联,但其中一个类如今不在需要另一个类的特性
手法:去除不必要的关联
做法:
- 找出保存“你想去除的指针”的字段,检查它的每一个用户,判断是否可以去除该指针
- 检查访问点及调用这些访问点的函数
- 考虑不使用指针的情况获取引用对象,使用Subsitute Algorithm对付取值函数
- 如果用户使用了取值函数,先使用Self Encapsulate Field将待删除字段自我封装起来,然后用Subsitute Algorithm对付取值函数,令它不在使用该字段
- 如果客户未使用取值函数,那就直接修改待删除字段的所有被引用点:改用其他途径获得该字段所保存的对象
- 如果已经没有任何函数使用待删除字段,移除所有对该字段的更新逻辑,然后删除该字段
- 使用Self Encapsulate Field处理设值函数,统一调用后清空函数一同删除
Replace Magic Number with Symbolic Constant(以字面常量取代魔法数)
表现:有一个字面数值,带有特别含义
手法:创造一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量
Tips:观察魔法数的使用,发现一种更好的使用方式,如使用Replace Type Code with Class
Encapsulate Field(封装字段)
表现:你的类中存在一个public字段
手法:将它声明为private,并提供相应的访问函数
Tips:新建访问函数后,尝试使用Move Method重构它
Encapsulate Collection(封装集合)
表现:函数返回一个集合
手法:让这个函数返回该集合的一个只读副本,并在这个类中提供添加、删除集合元素的函数
封装集合(只能由所属对象管理结合的添加/删除):
取值函数不应该返回集合自身,因为这回让用户得以修改集合内容而集合拥有者却一无所悉 不应该为整个集合提供设值函数,但应该提供用以为集合添加/移除元素的函数
做法:
- 加入为集合添加/移除元素的函数
- 将保存集合的字段初始化为一个空集合
- 找出集合设值函数的所有调用者,修改设值函数或者调用端,调用新建立的方法
- 找出通过集合取值函数获得集合并修改起内容的函数,调用新建立的方法
- 修改取值函数自身,返回一个不可修改的集合或者返回一个副本
- Collection.unmodifiableXxx()方法返回一个不可修改集合
- 返回副本通常是clone对象,这样会造成迷惑,用户误以为可以修改
- 找出取值函数的所有用户,使用Extract Method和Move Method处理应该存在集合所属对象中的代码
Replace Record with Data Class(以数据取代记录)
表现:你需要面对传统变成结构中的记录结构
手法:为该记录创建一个哑数据对象
做法:
- 建立一个表示这个记录的类
- 对于记录中的每一项数据,用一个private字段表示,建立get、set方法
Replace Type Code with Class(以类取代类型码)
表现:类中有一个数值类型码,但它并不影响类的行为
手法:以一个新的类替换该数值的类型码
把数值换成一个类,编译器就可以对这个类进行类型检验
做法:
- 为类型码建立一个新类
- 修改源类实现,让他使用新建的类
- 对于源类中每一个使用类型码的函数,建立一个使用新类的函数
- 修改源类的用户,调用新建的函数
- 删除旧类型码的接口和变量
Replace Type Code with Subclasses(以子类取代类型码)
表现:类中有一个不可变的型码,但它会影响类的行为
手法:以子类替换该类型码
如果类型码不会影响类宿主类的行为,使用Replace Type Code with Class,否则借助多态
Replace Type Code with Subclasses通常服务于Replace Conditional with Polymorphism
另一个场景使用它是出现了“只与具有特定类型码对象相关”的特性,使用Push Down Method和Push Down Field将这些特性推到子类中
它把“对不同行为的了解”从用户那转移到了类自身
做法:
- 使用Self Encapsulate Field将类型码自我封装起来
- 为类型码的每一个数值建立一个相应的子类,每个子类中覆写类型码的取值函数,返回类型码的值
- 硬编码在return语句中的值使用Replace Conditional with Polymorphism处理
- 删除超类中保存类型码的字段,声明取值函数为抽象函数
注意事项:类型码在对象创建后发生了改变或者已有子类的情况下,不适用此手法,而换用Replace Type Code with State/Strategy
Replace Type Code with Strate/Strategy(以状态模式或策略模式取代类型码)
表现:类中有一个不可变的型码,但它会影响类的行为,但是无法使用继承消除它
手法:以状态对象替换该类型码
如果打算重构后使用Replace Conditional with Polymorphism,策略模式更合适 如果都打算搬移状态相关的数据,将新对象视为一种变迁状态,状态模式更合适
做法:
- 使用Self Encapsulate Field将类型码自我封装起来
- 新建一个类,根据类型码的用途为他命名,这就是一个状态对象。
- 为类型码的每一个数值建立一个相应的子类,每个子类中覆写类型码的取值函数,返回类型码的值
- 在源类中建立一个字段,保存新建的状态对象
- 调整源类中为类型码设值的函数,将一个恰当的状态对象子类赋值给新建的保存状态的字段
Replace Subclass with Fields(以字段取代子类)
表现:你的各个子类的唯一差别只在“返回常量数据”的函数身上(常量函数)
手法:修改这些函数,使他们返回超类中的某个(新增)字段,然后销毁子类
建立子类的目的是增加新特性或变化起行为
做法:
- 对所有的子类使用Replace Constructor with Factory Method
- 如果有任何代码直接引用子类,令它改而引用超类
- 针对每个不同的常量函数,在超类中声明一个final字段
- 为超类声明一个protected构造函数,用以初始化这些新增字段
- 新建或修改子类构造函数,使它调用超类的新增构造函数
- 在超类中实现所有常量函数,令他们返回相应的字段值,然后删除子类的实现
- 全部删除后,使用Inline Method将子类构造函数内联到超类的工厂函数中
- 删除子类
Tips:使用工厂函数返回父类的特定构造对象,而不是使用子类
简化条件表达式
- Decompose Conditional(分解条件表达式)
- 分离分支逻辑和操作细节
- Consolidate Conditional Expressions(合并条件表达式)
- 合并处理逻辑相同的条件判断,使逻辑更清晰
- Consolidate Duplicate Conditional Fragments(合并重复条件片段)
- 清楚的表达哪些因条件变化而哪些不是
- Remove Control Flag(移除控制标记)
- 使用break、continue和return替代其他控制标记
- Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件表达式)
- 遵循fast fail原则的条件快速返回,而不是使用大量else逻辑
- Replace Conditional with Polymorphism(以多态取代条件表达式)
- 替换条件语句
- Introduce Null Object(引入Null对象)
- 去除Null值检验
- Introduce Assertion(引入断言)
- 用断言标明程序的假设内容而不是注释
Decompose Conditional(分解条件表达式)
表现:有一个复杂的if-then-else语句
手法:从if、then、else三个段落(条件表达式和段落)分别提炼出独立函数
复杂的条件逻辑是最常导致复杂度上升的点之一 分解成新函数可吐出条件逻辑,更清楚地表明每个分支的作用,并且吐出每个分支的原因
Tips:嵌套的条件逻辑观察是否可以使用Replace Nested Conditional with Guard Clauses,如果不行,才开始分解每一个条件
Consolidate Conditional Expressions(合并条件表达式)
表现:你有一系列条件测试,都得到相同的结果
手法:将这些测试合并为一个条件表达式,并将这个条件表达式提炼为一个独立函数(与或逻辑合并不同条件)
Tips:这个手法为Extract Method做好准备
Consolidate Duplicate Conditional Fragments(合并重复条件片段)
表现:条件表达式的每个分支上有相同的一段代码
手法:将这段重复的代码搬移到表达式之外
Tips:注意共通代码的位置决定移动到表达式值钱或之后移动
Tips:如果共通代码再表达式中间,判断是否会受表达式执行前后影响,决定是否可以移动
Tips:如果共通代码是很多句,Extract Method后决定向前或向后移动
Tips:同样的手法也可以用于catch块中的代码移动到finally中
Remove Control Flag(移除控制标记)
表现:一些列布尔表达式中,某个变量带有“控制标记”的作用
手法:以break、continue语句或return语句取代控制标记
注意事项:如果控制标记会影响这段逻辑的最终结果,需要保留控制标记,这是可以考虑使用Extract Method并配合return语句
Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件表达式)
表现:函数中的条件逻辑使人难以看清正常的执行路径
手法:使用卫语句表现所有特殊情况
Tips:卫语句指为罕见条件单独检查的语句,语句为真时立即在函数中返回
Tips:卫语句要不就返回,要不就抛异常(契合fast fail原则)
Replace Conditional with Polymorphism(以多态取代条件表达式)
表现:条件表达式根据对象类型不同选择不同的行为
手法:将表达式每一个分支放进一个子类的覆写函数中,然后将原始函数声明为抽象函数
多态最根本的好处就是:如果你需要根据对象的不同类型而采取不同的行为,多态使你不必编写明显的条件表达式
做法:
- 首先你必须得有一个继承机构,如果没有,你需要建立它
- 参见Replace Type Code with Subclass/Replace Type Code with State/Strategy
- 如果表达式是一个大函数的一部分,使用Extract Method提炼它到一个独立函数
- 如果有必要,使用Move Method将表达式放置在继承结构顶端
- 任选一个子类,覆写超类中条件表达式所在的函数,并将对应的分支拷贝下来,适当调整
- 在超类中删除被子类处理了的分支
- 重复处理每个分支
- 将超类函数声明为抽象函数
Introduce Null Object(引入Null对象)
表现:你需要再三检查对象是否为null
手法:将null值替换为null对象
做法:
- 为源类建立一个子类,其行为就像是源类的null版本,在源类和子类中都加上isNull()函数,返回是否为null对象
- 建立一个nullable接口
- 修改可能返回null的地方,返回一个空对象
- 找到对象与null比较的地方,调用isNull()函数
- 在不该出现null的地方放一些断言
- 找出null影响逻辑的地方,在null类中覆写动作用于区分正常动作
- 去除null逻辑的判断,直接调用null类的方法,并测试
注意事项:空对象模式会造成问题的侦测和查找上的困难
注意事项:空对象一定是常量,因为它们的任何成分都不会发生变化,可以使用单利模式实现
注意事项:大多数客户代码要求空对象做出相同的响应时,这样的行为搬运才有意义,而哪些特殊的部分需要使用isNull()函数
Tips:你常常见到空对象返回其他的空对象,这很有意义
Tips:如果不能修改源类,让新类实现一个Null接口,使用instanceof操作符来判断空对象
Tips:多种有业务逻辑的如空对象一样的对象,构成了一个Special Case模式
Introduce Assertion(引入断言)
表现:某一段代码需要对程序状态做出某种假设
手法:以断言明确表现出这种假设
断言永远不会影响程序的行为
注意事项:请不要用它来检查应该为真的条件,只用它来检查一定为真的条件
注意事项:真正程序运行的时候断言都是默认关闭的,所以用于单元测试
Tips:JDK1.4引入断言机制,默认关闭因为老程序有可能使用assert关键字做了兼容,使用-ea开启
Tips:断言更多的意义是帮助程序员理解代码正确运行的必要条件
简化函数调用
并发变成通常需要一个很长的参数列表,保证传递的对象是不可修改的(内置对象和值对象不可变), 通常,你可以使用不可变对象取代这样的长参数列, 但另一方面你也必须对此类重构保持谨慎
- Rename Method(函数改名)
- Add Parameter(增加参数)
- Remove Parameter(移除参数)
- Separate Query from Modifier(将查询和修改函数分离)
- 分离查询和修改函数
- Parameterize Method(令函数携带参数)
- 将多个略有不同的函数通过增加参数合并为一个函数
- Replace Parameter with Explicit Method(以明确函数取代参数)
- 将根据参数判断不同逻辑的函数分解为多个函数,去除那个参数
- Preserve Whole Object(保持对象完整)
- 替换参数的对象多个值为整个对象,缩短参数列
- Replace Parameter with Method(以函数取代参数)
- 避免传递参数
- Introduce Parameter Object(引入参数对象)
- 替换多参数为新建对象,缩短参数列
- Remove Setting Method(移除设值函数)
- 关闭数据的修改
- Hide Method(隐藏函数)
- 降低不被外部类调用函数的访问级别
- Replace Constructor with Factory Method(以工厂函数取代构造函数)
- 隐藏复杂的构造函数,或者希望做构建外的更多操作
- 隐藏子类
- Encapsulate Downcast(封装向下转型)
- 封装向下转型(泛型比这个手法好)
- Replace Error Code with Exception(以异常取代错误码)
- 能够分离普通程序和错误处理
- Replace Exception with Test(以测试取代异常)
Rename Method(函数改名)
表现:函数的名称未能揭示函数的用途
手法:修改函数名称
将复杂的处理过程分解成小函数,给小函数起一个好名称是关键
重新安排参数位置也有助于理解程序意图
注意事项:如果旧函数是public接口的一部分,令它调用新函数,并标记为deprecated
Tips:首先考虑应该给这个函数写上一句怎样的注释,然后想办法将注释变成函数名称
Add Parameter(增加参数)
表现:某个函数需要从调用端得到更多信息
手法:为此函数添加一个对象参数,让该对象带走函数所需信息
注意事项:如果有其他选择,其他选择比添加参数好,因为它会增加参数列表长度,往往伴随Data Clumps
注意事项:也许你应该使用Introduce Parameter Object
Remove Parameter(移除参数)
表现:函数本体不在需要某个参数
手法:将该参数去除
注意事项:如果多态中有实现使用了这个参数,你应该保留它。如果调用者为这个参数费力,你需要新建一个函数去除这个参数,否则什么也不做
Separate Query from Modifier(将查询和修改函数分离)
表现:函数既返回对象状态值,又修改对象状态
手法:建立两个不同的函数,其中一个负责查询,另一个负责修改
任何有返回值的函数,都不应该有看得到的副作用(查询外的其他功能)
在多线程系统中,惯用操作是在一个动作中完成检查和赋值,你需要将查询和修改动作分开,共三个函数来完成这件事,并使用synchronized或者限制访问方式保持线程安全
Parameterize Method(令函数携带参数)
表现:若干函数做了类似的操作,但函数本体却包含了不同的值(仅少数几个值不同使行为不同)
手法:建立单一函数,以参数表达那些不同的值
你可以处理整个方法中的一部分时,使用Extract Method,再使用此手法
Replace Parameter with Explicit Method(以明确函数取代参数)
表现:函数完全取决于参数值采取不同的行为(如工厂函数,建立相应的工厂方法)
手法:针对该参数的每一个可能值,建立一个独立函数
以参数值决定函数行为,那么函数用户不但需要观察该函数,而且还要判断参数值是否合法,而参数值是否合法往往很少在文档中被清楚的提出
函数分开后可以获得编译期检查好处
而且接口也更清楚
Preserve Whole Object(保持对象完整)
表现:你从某个对象中取出若干值,讲它们作为某一次函数调用时的参数
手法:改为传递整个对象
传递参数对象可以提高代码的可读性,同时也在一定程度上提高了代码复用(复用对象中的计算方法)
同时,函数对象会依赖传递的对象,造成依赖结构恶化
注意事项:如果函数依赖很多来自对象的数据,考虑使用Move Method Tips:还有一种常见情况是传递this对象取代传递多个成员变量
Replace Parameter with Method(以函数取代参数)
表现:对象调用某个函数,并将所得结果作为参数,传递给另一个函数,而接受该参数的函数本身也能够调用前一个函数
手法:让参数接受者去除该项参数,并直接调用前一个函数
做法:
- 如果有必要,将参数的计算过程提炼到一个独立函数中
- 将函数本体内引用该参数的地方改为调用新建的函数
- 最后使用Remove Parameter去除参数
Introduce Parameter Object(引入参数对象)
表现:某些参数总是很自然的同时出现
手法:以一个对象取代这些参数
总是同时出现的作为参数的一组数据就是所谓的Data Clumps
Tips:将参数封装到同一对象的的同时,你会发现针对这些参数的共同处理也可以进行移动到这个对象中
Remove Setting Method(移除设值函数)
表现:类中的某个字段应该在对象创建时被设值,然后就不在改变
手法:去掉该字段的所有设值函数(并标记为final)
注意事项:如果构造函数使用了这个设值函数,改为直接引用
注意事项:如果子类使用了这个设值函数,提供一个protect的设值函数,但是最好还是构造函数
Hide Method(隐藏函数)
表现:有一个函数,从来没有被其他类用到
手法:将函数改为private
一个过于丰富、提供了很多行为的接口,就值得将非必要的取值和设值函数隐藏起来,甚至是删除
Replace Constructor with Factory Method(以工厂函数取代构造函数)
表现:希望在创建对象时不仅仅是做简单的构建动作
手法:将构造函数替换为工厂函数
最常见动机:在派生子类过程中以工厂函数取代类型码
做法:
- 新建工厂函数,修改用户调用为工厂函数
- 将构造函数声明为private
Encapsulate Downcast(封装向下转型)
表现:函数返回的对象,需要由函数调用者执行向下转型
手法:将转型动作移动到函数中(如果可以,使用泛型)
Replace Error Code with Exception(以异常取代错误码)
表现:某个函数返回一个特定的代码,用以表示某种错误情况
手法:改用异常
如果调用者有责任在调用前检查必要状态,就抛出非受检异常说明,这是一个编程错误
如果抛出受控异常,就要求调用者注意这个异常,并采取响应措施
Replace Exception with Test(以测试取代异常)
表现:面对一个调用者可以预先检查的条件,抛出了一个异常
手法:修改调用者,使它在调用函数之前先做检查
注意事项:异常只应该被用于异常的、罕见的行为,也就是哪些产生意料之外的错误和行为,而不应该成为条件检查的替代品
处理包括关系(继承关系)
- Pull Up Field(字段上移)
- Pull Up Method(函数上移)
- Pull Up Constructor Body(构造函数本体上移)
- Pull Down Field(字段下移)
- Pull Down Method(函数下移)
- Extract Subclass(提炼子类)
- Extract Superclass(提炼超类)
- Extract Interface(提炼接口)
- Collapse Hierarchy(折叠继承体系)
- 去除没价值的子类或父类
- Form Template Method(塑造模版函数)
- 分离函数共同点和不同点(模版方法模式)
- Replace Inheritance with Delegation(以委托代替继承)
- Replace Delegation with Inheritance(以继承代替委托)
Pull Up Field(字段上移)
表现:两个子类拥有相同(行为类似)的字段
手法:将该字段移至超类
Pull Up Method(函数上移)
表现:一些子类中的函数产生完全相同的效果
手法:将函数移至超类
Tips:如果被提升函数引用了子类特性,可以一同提升,或者使用父类抽象函数解决
Pull Up Constructor Body(构造函数本体上移)
表现:在各个子类中拥有一些构造函数,他们的本体几乎完全一致
手法:在超类中新建一个构造函数,并在子类构造函数中调用它
Pull Down Field(字段下移)
表现:超类中的某个字段只被部分子类用到
手法:将这个字段移到需要它的子类中去
Pull Down Method(函数下移)
表现:超类中某个函数只与部分子类有关
手法:将这个函数移动到相关的子类中去
Extract Subclass后你可能需要它
Tips:修改访问修饰符或使用抽象函数达到访问需求
Extract Subclass(提炼子类)
表现:类中的某些特性只被某些实例用到
手法:新建一个子类,将上面说的部分特定移动到子类中
Extract Class是Extract Subclass之外的另一种选择,本质是在委托和继承之间抉择
如果你想表现多组变化,你需要使用委托
Tips:使用Repalce Conditional with Polymorphism处理可以使用继承体系解决的原字段
Extract Superclass(提炼超类)
表现:两个类有相似特性
手法:为这两个类建立一个超类,将相同特性移至超类
Tips:相同函数提炼到父类,不相同实现可以使用抽象方法
Tips:部分相同的函数,Extract Method后Pull Up Method
Tips:整体流程相同的函数,使用Form Template Method
Extract Interface(提炼接口)
表现:若干客户使用类接口中的同一子集,或者两个类的接口有部分相同
手法:将相同的接口提炼到一个独立接口中
使用一个类通常意味着使用一个类的所有责任区
某一客户只使用类职责区的一个特定子集
类需要与所有协助处理某些特定请求的类合作
后两种情况将职责分离出来很有意义,这样可以使系统的用法更清晰,同时也更容易看清系统的责任划分
如果某个类在不同环境下扮演截然不同的角色,使用接口是个好主意,针对不同角色提供接口
另一个情况是你想要描述一个类的外部依赖接口(这个类要求服务提供方提供的操作),方便扩展服务对象
Collapse Hierarchy(折叠继承体系)
表现:超类和子类之间无太大区别
手法:将它们合为一体
进行字段上下移动后,发现子类或父类并未带来该有的价值
继承体系容易变得过分复杂
Form Template Method(塑造模版函数)
表现:一些子类的某些函数以相同的顺序执行类似操作,但各个操作在细节上不同
手法:将这些操作分别放进独立函数中,并保持它们都有相同的签名,于是原函数也就变得相同了,然后将原函数上移至超类
Replace Inheritance with Delegation(以委托代替继承)
表现:某个子类只使用超类接口中的一部分,或是根本不需要继承而来的数据
手法:在子类中新建一个字段用以保存超类;调整子类函数,令它改而委托超类,然后去掉两者的继承关系
Java中的Stack继承Vector是一个滥用继承的例子,应该使用委托
Replace Delegation with Inheritance(以继承代替委托)
表现:两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数
手法:让委托类继承受托类
注意事项:如果你并没有使用受托类的所有函数,那就不要使用此重构,可以使用Remove Middle Man,Extract Superclass,Extract Interface等手法替代
注意事项:如果委托关系是用于共享数据,被多个对象共享,且是可变的,就不能使用继承关系
大型重构
- Tease Apart Inheritance(梳理分解继承体系)
- 处理混乱的继承体系
- Convert Procedural Design to Objects(将过程化程序转化为对象设计)
- 弥补对面向对象设计的不足
- Separate Domain from Presentation(将领域和表述/显示分离)
- 分离业务逻辑和用户界面
- Extract Hierarchy(提炼继承体系)
- 简化系统,将复杂的类转化为一群子类
Tease Apart Inheritance(梳理分解继承体系)
表现:某个继承体系同时承担两项责任
手法:建立两个继承体系,并通过委托关系让其中一个调用另外一个
如果继承体系中的某一特定层级上的所有类,其子类名称都以相同的形容词开始,那么这个体系很可能就是承担着两项不同的责任
做法:
- 判断哪一项责任更重要,将它留在当前继承体系,而不重要的责任移到另一个继承体系中
- 使用Extract Class从当前的超类提炼出一个新类用以表示要提炼的责任,并在原超类中添加一个实例变量,用于保存新类的实例
- 对于原继承体系中的每个子类,创建新类的一个子类,在原继承体系的子类中,将前一步所添加的实例变量初始化为新建子类的实例
- 针对原继承体系中的每个子类,使用Move Method将其中的行为搬移到与之对应的新建子类中
- 当原继承体系中的某个子类不具有任何代码时,将它去除
- 重复上述步骤分离其他子类,并观察是否可以使用Pull Up Method和Push Up Field
- 重复处理剩余的继承体系
Tips:实际工作中,我们可能会将代码较多的职责留在原地,这样一来需要搬移的代码数量会比较少
Convert Procedural Design to Objects(将过程化程序转化为对象设计)
表现:传统过程化风格的代码
手法:将数据记录变成对象,将大块的行为分成小块,并将行为移入相关对象中
做法:
- 针对每个记录类型,将其转变为只含访问函数的哑数据对象
- 针对每一处过程化风格,将该处的代码提炼到一个独立类中
- 针对每一个长长的程序,试试Extract Method及其他手法将它分解,再以Move Method移动到相关的哑数据类中
- 重复上述过程,直至原始类中函数都被移除,删除原始类
Separate Domain from Presentation(将领域和表述/显示分离)
表现:某系GUI类之中包含了领域逻辑
手法:将领域逻辑分离出来,为它们建立独立的领域类
Extract Hierarchy(提炼继承体系)
表现:你有某个类做了太多工作,其中一部分工作是以大量条件表达式完成的(标记变量和条件表达式遍布各处)
手法:建立继承体系,以一个子类表示一种特殊情况
重构,改善既有代码的设计