重构

2017/08/07 JavaEE

重构是不改变软件可观察行为的前提下改善其内部结构(在代码写好之后改进它的设计)
如果在最初开发和整个开发过程中一直遵循一些良好的设计原则,那么重构过程会更轻松,软件的进化会更容易

导致程序难以修改的原因

  • 难以阅读
  • 逻辑重复
  • 添加新行为时需要修改已有代码
  • 带复杂条件逻辑

为什么重构

  • 重构改进软件设计
    • 由于弥补设计错误所需的成本降低了,需要预先设计也就更少了
    • 预先设计是一项带有预测性质的工作,因为项目激活之时,需求往往还不明确,所以正确的设计方式是:尽力那个简化需求尚未明朗的那一部分代码
  • 重构使软件更容易理解
  • 重构帮助找到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分解它为多个小函数

做法:

  1. 在新类中建立一个final字段的当前类引用(源对象)
  2. 在新类中生命函数用到的临时变量为成员变量
  3. 建立构造函数入参为上面建立的所有变量
  4. 建立一个compute()函数,讲原函数的内容拷贝进去,并将原函数替换为对象函数调用
  5. 将原来函数中引用了其他函数的部分委托给源对象
  6. 重构这个大方法而不必考虑参数传递的问题
Substitute Algorithm(替换算法)

表现:把某个算法替换为另一个更清晰的算法
手法:将函数体替换为另一个算法

Tips:对于新旧算法分别用所有测试用例执行,找出问题*

在对象之间搬移特性

  • Move Method(搬移函数)
    • 移动行为
  • Move Field(搬移字段)
    • 移动属性
  • Extract Class(提炼类)
    • 分离职责
  • Inline Class(将类内联化)
    • 去除职责过少的类
  • Hide Delegate(隐藏委托关系)
    • 降低委托类变化的影响范围
  • Remove Middle Man(移除中间人)
    • 去除单一委托职责的类
  • Introduce Foreign Method(引入外加函数)
    • 加入不可修改的类的一两个方法
  • Introduce Local Extension(引入本地扩展)
    • 加入不可修改的类的多个方法
Move Method(搬移函数)

表现:有函数与起所驻类之外的另一个类进行许多交流,调用后者,或被后者调用
手法:在该函数最常引用的类中建立一个有着类似行为的新函数,将酒函数变成一个单纯的委托函数,或是完全移除

做法:

  1. 检查源类中被源函数所使用的一切特性,考虑它是否也该被搬移
  2. 检查源类和超类,是否有函数的其他声明
  3. 再目标类生命这个函数,可以选择一个新的名字
  4. 复制函数内容至新函数,检查对源类调用依赖如何引用(如传递对象引用)和异常处理
    • 将依赖特性移动至目标类
    • 建立或使用一个从目标类到源类的引用关系
    • 将源对象作为参数传递给目标类
    • 将引用变量作为参数传递给目标类
  5. 决定源类如何调用新的引用
  6. 修改源函数为委托函数
  7. 决定是否删除源函数

Tips:如果某个特性只有打算搬移的函数用到,那也应该一并搬移
Tips:如果目标函数设计过多源函数引用,需要考虑拆分函数再搬移

Move Field(搬移字段)

表现:某个字段被其所驻类之外的类更多的用到
手法:再目标类建立一个新字段,修改源字段的所有用户,令他们改用新的字段

Tips:使用Self-Encapsulation(自我封装)的字段,可以通过修改访问函数后迁移字段,引用点无需修改

Extract Class(提炼类)

表现:某个类做了应该由两个类做的事
手法:建立一个新类,将相关字段和函数从旧类中搬移过去

做法:

  1. 决定如何分解一个类所负的责任
  2. 建立一个新类用以表现旧类分离出来的责任
    • 如果剩余的类名称不符,就改名
  3. 建立新类和旧类的链接关系
    • 你可能需要双向链接,但是在真正需要它之前,不要建立
  4. 搬移字段
  5. 搬移方法,优先搬移被依赖的或者被依赖多的
  6. 检查并精简类的接口
    • 如果有双向链接,检查是否可以替换为单向链接
  7. 决定是否公开新类
    • 允许任何对象修改新类的任何部分,新类与旧类一对多时应该考虑使用Change Value to Reference
    • 不允许任何人不通过旧类修改新类对象,设置新类为不可修改的,提供不可修改的接口
      • 传递一个复制的新对象给用户,但是会造成迷惑

Tips:一个类应该是一个清楚的抽象,处理一些明确的责任
Tips:如果一个类需要通过多个子类化的方式分解特性,你需要分解这个类
Tips:Extract Class是改善并发的一种常用技术,它可以为两个类分别加锁,同时也造成两个类需要同时加锁的事务问题

Inline Class(将类内联化)

表现:某个类没有做太多事情(通常因为重构移走了部分类的责任)
手法:将这个类所有特性搬移到另一个类(最频繁用户)中,然后移除原类

做法:

  1. 在目标类中声明源类的public函数,并将功能委托给源类
    • 考虑是否提炼接口
  2. 将所有源类引用点,改为目标类
  3. 使用Move Field和Move Method,移动源类特性
  4. 删除源类
Hide Delegate(隐藏委托关系)

表现:客户通过一个委托类调用另一个对象
手法:在服务类上建立客户所需的所有函数,用以隐藏委托关系

如果一个对象先通过一个服务对象的字段得到另一个对象,然后调用后者的函数,那么客户就必须直销这一层委托关系
万一委托关系发生变化,客户端也得相应变化
可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,去除这种依赖

注意事项:代价是每当用户要使用委托类的新特性时,就必须在服务端添加一个简单委托函数,随着委托的增加,服务类就变成了一个中间人

Remove Middle Man(移除中间人)

表现:某个类做了过多的简单委托动作
手法:让客户直接调用委托类

Introduce Foreign Method(引入外加函数)

表现:需要为提供服务的类增加一个函数,但你无法修改这个类
手法:在客户类中建立一个函数,并以第一参数形式传入一个服务类实例

做法:

  1. 在客户类中建立一个函数,用来提供你需要的功能
    • 这个而函数不应该调用客户端的任何特性,如果它需要一个值,把值作为参数传给它
  2. 以服务类实例作为该函数的第一个参数
  3. 将函数注释为外加函数,以便以后修改

注意事项:如果你以外加函数实现一项功能,那说明函数元贝应该在提供服务的类中实现
注意事项:如果大量的外加函数需要添加,使用Introduce Local Extension

Introduce Local Extension(引入本地扩展)

表现:你需要为服务类提供一些额外函数,但你无法修改这个类
手法:建立一个新类,使它包含这些额外函数,让这个扩展品成为源类的子类或包装类

做法:

  1. 建立扩展类,将它作为原始类的子类或包装类
  2. 在扩展类中加入转型构造函数
    • 子类应调用合适的父类构造
    • 扩展类应接受原始类对象参数用于委托
  3. 在扩展类中加入新特性
  4. 替换原对象的调用为扩展类
  5. 迁移外加函数到本地扩展

注意事项:扩展类是一个独立的类,它提供源类的一切特性,同时额外添加新特性,再任何使用源类的地方,都应可以使用本地扩展
注意事项:包装类的扩展需要委托源类中的方法,推荐使用方法重载分开处理以原始类和包装类为参数的方法如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(以对象取代数据值)

表现:你有一个数据项,需要与其他数据和行为一起使用才有意义(简单数据变得需要有行为和属性)
手法:将数据项变为对象

做法:

  1. 为待替换的数值新建一个类,在其中声明一个final字段,并增加get、set方法和构造方法
  2. 将源类中的数值字段替换为新类
  3. 修改源类中的取值函数,使它调用新类的取值函数
  4. 如果构造函数中使用这个待替换字段,我们就修改构造函数调用新类的构造函数
  5. 修改设值函数,令他为新类创建一个实例
  6. 查看是否需要使用Change Value to Reference

Tips:值对象应该是不可修改内容的,保证多个对象表示同一事物

Change Value to Reference(将值对象改为引用对象)

表现:你从一个类衍生出许多彼此相等的实例,希望将它们替换为同一个对象
手法:将值对象变更为引用对象

做法:

  1. 使用Replace Constructor with Factory Method
  2. 决定由什么对象提供新对象的访问路径
  3. 决定引用对象应该预先创建还是动态创建
  4. 修改工厂函数,令他返回引用对象
    • 对象不存在的处理
  5. 测试

注意事项:引用对象有同步问题

Change Reference to Value(将引用对象转变为值对象)

表现:你有一个引用对象,很小且不可变(不可变指值对象本身不可变,与值对象关系可变,可关联不同值对象),且不易管理
手法:把它变成值对象

做法:

  1. 检查重构目标是否可以变为不可变对象,或者是否可以修改为不可变对象
    • 如果可修改,使用Remove Setting Method直至它变成不可变对象为止
    • 如果无法将对象变成不可变对象,放弃修改
  2. 建立equals()和hashCode()方法
  3. 测试
  4. 考虑是否可以删除工厂行数,并将构造函数声明为public
Replace Array with Object(以对象取代数组)

表现:一个数组的元素各自代表不同的东西
手法:以对象替换数组,用字段表示数组中每一个元素

Duplicate Observed Data(复制被监视数据)

表现:你有一些领域数据置身于GUI组件中,而领域函数需要访问这些数据(同步GUI对象中的数据到Model中,对Model数据的操作同步回GUI对象,使操作从GUI对象中解耦)
手法:将该数据复制到一个领域对象中,建立一个Observer模式,用以同步领域对象和GUI对象内的重复数据

Change Unidirectional Association to Bidirectional(将单向关联改为双向关联)

表现:两个类都需要使用对方的特性,但其间只有一个单向连接
手法:添加一个反向指针,并修改函数使之函数能够同时更新两条连接

做法:

  1. 在被引用类中增加一个字段,用来保存反向指针
  2. 决定由哪个类控制关联关系
    • 如果是一对多关系,就让单一引用方(多)控制关联关系
    • 如果某个对象是组成另一个对象的部件,由后者控制关联关系
    • 如果晾着都是引用对象,多对多关系,随意其中一方控制关联关系
  3. 在控制端建立一个辅助函数,其命名应该清楚的指出它的有限用途
  4. 如果既有的修改函数在控制端,让它负责更新反向指针
  5. 如果既有的修改函数在被控制端,就在控制端建立一个控制函数,并让既有的修改函数调用这个新建的控制函数

注意事项:在处理关联关系时,尤其时一对多关系时,添加关系的同时需要先考虑解除之前的关系(维护双向连接,确保对象被正确的创建和删除)
注意事项:大量的双向连接也很容易造成“僵尸对象”,因为引用没有完全清除
注意事项:双向关联也迫使两个类之间有了依赖,一个类修改会引发另一个类的变化

Change Bidirectional Association to Unidirectional(将双向关联改为单向关联)

表现:两个类之间有双向关联,但其中一个类如今不在需要另一个类的特性
手法:去除不必要的关联

做法:

  1. 找出保存“你想去除的指针”的字段,检查它的每一个用户,判断是否可以去除该指针
    • 检查访问点及调用这些访问点的函数
    • 考虑不使用指针的情况获取引用对象,使用Subsitute Algorithm对付取值函数
  2. 如果用户使用了取值函数,先使用Self Encapsulate Field将待删除字段自我封装起来,然后用Subsitute Algorithm对付取值函数,令它不在使用该字段
  3. 如果客户未使用取值函数,那就直接修改待删除字段的所有被引用点:改用其他途径获得该字段所保存的对象
  4. 如果已经没有任何函数使用待删除字段,移除所有对该字段的更新逻辑,然后删除该字段
    • 使用Self Encapsulate Field处理设值函数,统一调用后清空函数一同删除
Replace Magic Number with Symbolic Constant(以字面常量取代魔法数)

表现:有一个字面数值,带有特别含义
手法:创造一个常量,根据其意义为它命名,并将上述的字面数值替换为这个常量

Tips:观察魔法数的使用,发现一种更好的使用方式,如使用Replace Type Code with Class

Encapsulate Field(封装字段)

表现:你的类中存在一个public字段
手法:将它声明为private,并提供相应的访问函数

Tips:新建访问函数后,尝试使用Move Method重构它

Encapsulate Collection(封装集合)

表现:函数返回一个集合
手法:让这个函数返回该集合的一个只读副本,并在这个类中提供添加、删除集合元素的函数

封装集合(只能由所属对象管理结合的添加/删除):

取值函数不应该返回集合自身,因为这回让用户得以修改集合内容而集合拥有者却一无所悉 不应该为整个集合提供设值函数,但应该提供用以为集合添加/移除元素的函数

做法:

  1. 加入为集合添加/移除元素的函数
  2. 将保存集合的字段初始化为一个空集合
  3. 找出集合设值函数的所有调用者,修改设值函数或者调用端,调用新建立的方法
  4. 找出通过集合取值函数获得集合并修改起内容的函数,调用新建立的方法
  5. 修改取值函数自身,返回一个不可修改的集合或者返回一个副本
    • Collection.unmodifiableXxx()方法返回一个不可修改集合
    • 返回副本通常是clone对象,这样会造成迷惑,用户误以为可以修改
  6. 找出取值函数的所有用户,使用Extract Method和Move Method处理应该存在集合所属对象中的代码
Replace Record with Data Class(以数据取代记录)

表现:你需要面对传统变成结构中的记录结构
手法:为该记录创建一个哑数据对象

做法:

  1. 建立一个表示这个记录的类
  2. 对于记录中的每一项数据,用一个private字段表示,建立get、set方法
Replace Type Code with Class(以类取代类型码)

表现:类中有一个数值类型码,但它并不影响类的行为
手法:以一个新的类替换该数值的类型码

把数值换成一个类,编译器就可以对这个类进行类型检验

做法:

  1. 为类型码建立一个新类
  2. 修改源类实现,让他使用新建的类
  3. 对于源类中每一个使用类型码的函数,建立一个使用新类的函数
  4. 修改源类的用户,调用新建的函数
  5. 删除旧类型码的接口和变量
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将这些特性推到子类中
它把“对不同行为的了解”从用户那转移到了类自身

做法:

  1. 使用Self Encapsulate Field将类型码自我封装起来
  2. 为类型码的每一个数值建立一个相应的子类,每个子类中覆写类型码的取值函数,返回类型码的值
    • 硬编码在return语句中的值使用Replace Conditional with Polymorphism处理
  3. 删除超类中保存类型码的字段,声明取值函数为抽象函数

注意事项:类型码在对象创建后发生了改变或者已有子类的情况下,不适用此手法,而换用Replace Type Code with State/Strategy

Replace Type Code with Strate/Strategy(以状态模式或策略模式取代类型码)

表现:类中有一个不可变的型码,但它会影响类的行为,但是无法使用继承消除它
手法:以状态对象替换该类型码

如果打算重构后使用Replace Conditional with Polymorphism,策略模式更合适 如果都打算搬移状态相关的数据,将新对象视为一种变迁状态,状态模式更合适

做法:

  1. 使用Self Encapsulate Field将类型码自我封装起来
  2. 新建一个类,根据类型码的用途为他命名,这就是一个状态对象。
  3. 为类型码的每一个数值建立一个相应的子类,每个子类中覆写类型码的取值函数,返回类型码的值
  4. 在源类中建立一个字段,保存新建的状态对象
  5. 调整源类中为类型码设值的函数,将一个恰当的状态对象子类赋值给新建的保存状态的字段
Replace Subclass with Fields(以字段取代子类)

表现:你的各个子类的唯一差别只在“返回常量数据”的函数身上(常量函数)
手法:修改这些函数,使他们返回超类中的某个(新增)字段,然后销毁子类

建立子类的目的是增加新特性或变化起行为

做法:

  1. 对所有的子类使用Replace Constructor with Factory Method
  2. 如果有任何代码直接引用子类,令它改而引用超类
  3. 针对每个不同的常量函数,在超类中声明一个final字段
  4. 为超类声明一个protected构造函数,用以初始化这些新增字段
  5. 新建或修改子类构造函数,使它调用超类的新增构造函数
  6. 在超类中实现所有常量函数,令他们返回相应的字段值,然后删除子类的实现
  7. 全部删除后,使用Inline Method将子类构造函数内联到超类的工厂函数中
  8. 删除子类

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(以多态取代条件表达式)

表现:条件表达式根据对象类型不同选择不同的行为
手法:将表达式每一个分支放进一个子类的覆写函数中,然后将原始函数声明为抽象函数

多态最根本的好处就是:如果你需要根据对象的不同类型而采取不同的行为,多态使你不必编写明显的条件表达式

做法:

  1. 首先你必须得有一个继承机构,如果没有,你需要建立它
    • 参见Replace Type Code with Subclass/Replace Type Code with State/Strategy
  2. 如果表达式是一个大函数的一部分,使用Extract Method提炼它到一个独立函数
  3. 如果有必要,使用Move Method将表达式放置在继承结构顶端
  4. 任选一个子类,覆写超类中条件表达式所在的函数,并将对应的分支拷贝下来,适当调整
  5. 在超类中删除被子类处理了的分支
  6. 重复处理每个分支
  7. 将超类函数声明为抽象函数
Introduce Null Object(引入Null对象)

表现:你需要再三检查对象是否为null
手法:将null值替换为null对象

做法:

  1. 为源类建立一个子类,其行为就像是源类的null版本,在源类和子类中都加上isNull()函数,返回是否为null对象
    • 建立一个nullable接口
  2. 修改可能返回null的地方,返回一个空对象
  3. 找到对象与null比较的地方,调用isNull()函数
    • 在不该出现null的地方放一些断言
  4. 找出null影响逻辑的地方,在null类中覆写动作用于区分正常动作
  5. 去除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(以函数取代参数)

表现:对象调用某个函数,并将所得结果作为参数,传递给另一个函数,而接受该参数的函数本身也能够调用前一个函数
手法:让参数接受者去除该项参数,并直接调用前一个函数

做法:

  1. 如果有必要,将参数的计算过程提炼到一个独立函数中
  2. 将函数本体内引用该参数的地方改为调用新建的函数
  3. 最后使用Remove Parameter去除参数
Introduce Parameter Object(引入参数对象)

表现:某些参数总是很自然的同时出现
手法:以一个对象取代这些参数

总是同时出现的作为参数的一组数据就是所谓的Data Clumps

Tips:将参数封装到同一对象的的同时,你会发现针对这些参数的共同处理也可以进行移动到这个对象中

Remove Setting Method(移除设值函数)

表现:类中的某个字段应该在对象创建时被设值,然后就不在改变
手法:去掉该字段的所有设值函数(并标记为final)

注意事项:如果构造函数使用了这个设值函数,改为直接引用
注意事项:如果子类使用了这个设值函数,提供一个protect的设值函数,但是最好还是构造函数

Hide Method(隐藏函数)

表现:有一个函数,从来没有被其他类用到
手法:将函数改为private

一个过于丰富、提供了很多行为的接口,就值得将非必要的取值和设值函数隐藏起来,甚至是删除

Replace Constructor with Factory Method(以工厂函数取代构造函数)

表现:希望在创建对象时不仅仅是做简单的构建动作
手法:将构造函数替换为工厂函数

最常见动机:在派生子类过程中以工厂函数取代类型码

做法:

  1. 新建工厂函数,修改用户调用为工厂函数
  2. 将构造函数声明为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(梳理分解继承体系)

表现:某个继承体系同时承担两项责任
手法:建立两个继承体系,并通过委托关系让其中一个调用另外一个

如果继承体系中的某一特定层级上的所有类,其子类名称都以相同的形容词开始,那么这个体系很可能就是承担着两项不同的责任

做法:

  1. 判断哪一项责任更重要,将它留在当前继承体系,而不重要的责任移到另一个继承体系中
  2. 使用Extract Class从当前的超类提炼出一个新类用以表示要提炼的责任,并在原超类中添加一个实例变量,用于保存新类的实例
  3. 对于原继承体系中的每个子类,创建新类的一个子类,在原继承体系的子类中,将前一步所添加的实例变量初始化为新建子类的实例
  4. 针对原继承体系中的每个子类,使用Move Method将其中的行为搬移到与之对应的新建子类中
  5. 当原继承体系中的某个子类不具有任何代码时,将它去除
  6. 重复上述步骤分离其他子类,并观察是否可以使用Pull Up Method和Push Up Field
  7. 重复处理剩余的继承体系

Tips:实际工作中,我们可能会将代码较多的职责留在原地,这样一来需要搬移的代码数量会比较少

Convert Procedural Design to Objects(将过程化程序转化为对象设计)

表现:传统过程化风格的代码
手法:将数据记录变成对象,将大块的行为分成小块,并将行为移入相关对象中

做法:

  1. 针对每个记录类型,将其转变为只含访问函数的哑数据对象
  2. 针对每一处过程化风格,将该处的代码提炼到一个独立类中
  3. 针对每一个长长的程序,试试Extract Method及其他手法将它分解,再以Move Method移动到相关的哑数据类中
  4. 重复上述过程,直至原始类中函数都被移除,删除原始类
Separate Domain from Presentation(将领域和表述/显示分离)

表现:某系GUI类之中包含了领域逻辑
手法:将领域逻辑分离出来,为它们建立独立的领域类

Extract Hierarchy(提炼继承体系)

表现:你有某个类做了太多工作,其中一部分工作是以大量条件表达式完成的(标记变量和条件表达式遍布各处)
手法:建立继承体系,以一个子类表示一种特殊情况


重构,改善既有代码的设计

Search

    Post Directory