何时

  • 事不过三,三则重构
  • 当我需要理解其工作原理时,对其进行重构才会有价值
  • 重写比重构容易

重构的时机(1)——见机行事

大部分重构应该是不起眼的、见机行事的。重构不是与编程割裂的行为

预备性重构

  • 重构的最佳时机就在添加新功能之前

帮助理解的重构

捡垃圾式重构

  • 不想从眼下正要完成的任务上跑题太多,但我也不想把垃圾留在原地,给将来的修改增加麻烦
  • 如果我发现的垃圾很容易重构,我会马上重构它;如果重构需要花一些精力,我可能会拿一张便笺纸把它记下来,完成当下的任务再回来重构它。

重构的时机(2)——长期重构

  • 每当有人靠近“重构区”的代码,就把它朝想要改进的方向推动一点
  • 每次小改动之后,整个系统仍然照常工作

重构方法

测试用例

重构之前,先保证一组可靠的测试用例(有自我检验的能力)

重构清单

  1. 函数
重构方法 说明
提炼 过长的函数,或者一段需要注释才能理解的代码,将其中一段代码提取到一个独立函数中,并让函数名称解释该函数的用途
内联 1. 内联函数:如果一个函数的名称和函数体一样清晰易懂,则去掉函数调用,在调用点直接使用函数体
2.内联临时变量:如果一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其他重构手法。则将所有对该变量的引用替换为对应的赋值表达式
临时变量 1.以查询取代临时变量:临时变量保存了一个表达式的运算结果,将这个表达式提取到一个函数中,将对这个临时变量的所有引用替换为对新函数的调用。该新函数也可以被其它函数调用
2.引入解释型变量:将复杂表达式(或一部分)赋值给一个临时变量,用变量的名称来解释表达式的意图
3.分解:一个临时变量被多次赋值,但它既不是循环变量,也不是用于收集计算结果,说明它承担了多个责任,有多个含义,则应该在每次赋值的时候使用单独的临时变量
4.移除对参数的赋值:如果需要在函数内对参数赋值,请使用一个临时变量取代参数
以函数对象取代函数 1.大型函数中对局部变量的使用使你无法采用 提取函数(Extract Method)。 将这个函数放进一个单独对象中,局部变量就成了对象内的字段
2.然后你可以在同一个对象中将这个大型函数分解为多个小型函数
替换算法 用一个更好的算法直接替换原有的算法
函数改名 复杂的处理过程分解成小函数
添加参数 1.函数需要额外的信息,可以考虑给函数添加新的参数
2.添加新参数之前考虑:现有参数是否无法满足需要?是否可以通过其它函数调用获得需要的数据?
移除参数 函数不再需要某个参数,将其移除
查询函数和修改函数分离 1.某个函数既返回对象状态,又修改对象状态,建议分离成查询和修改两个独立的函数
2.任何有返回值的函数,都不应该有看得到的副作用
令函数携带参数 两个函数做着类似的工作,但因少数几个值导致行为略有不同,可以考虑合并为一个函数,通过参数处理变化的部分
以明确函数取代参数 一个函数,根据参数的值不同采取不同的行为,建议针对每一个参数值,建立独立的函数,调用方可以直接调用对应的函数,就可以避免条件表达式
以函数取代参数 通过其它途径(如调用其它的函数)获得参数值,就应该去掉参数值,缩短参数列的长度
引入参数对象 一组参数总是同时出现在不同的函数参数列表,建议使用一个对象将这些数据组织到一起,可以缩短参数列的长度
保持对象完整 一个对象的若干数据作为参数传给一个函数,可以考虑直接将该对象作为参数传递
传递对象不能导致依赖关系恶化,且调用函数不能使用了对象的很多项数据
移除设值函数 类中的某个字段在对象创建后不应该改变,去掉该字段的设值函数
隐藏函数 某个函数没有被外部类使用到,将该函数设置为private
以工厂函数取代构造函数 创建对象时还需要执行一些额外的操作,建议将构造函数替换为工厂函数
封装向下转型 函数的返回值需要调用者进行向下转型(downcast),建议在该函数内执行向下转型,返回调用者需要的类型
以异常取代错误码 果某个函数返回特定的错误码表示某种异常情况,建议直接抛出异常
以测试取代异常 1.面对调用者可以预先检查的条件,调用者应该先检查该条件
2.不要通过捕获异常去处理可以预见的逻辑。不要滥用异常,异常应该只用于异常的、罕见的行为
  1. 对象
重构方法 说明
搬移 1.函数:如果一个函数与另一个类的交流比所在类更多,应该考虑将该函数搬移到另一个类。如果一个类有太多行为,或一个类与另一个类有太多合作而形成高度耦合,就应该考虑是否可以通过搬移函数进行重构
2.字段:如果一个类中的字段,被另一个类更多地用到,应该考虑将该字段搬移到另一个类中
提炼类 某个类做了多个事情,数据和函数总是一起出现或一起变化,应该将相应的数据和函数提炼到新的类中。
内联类 某个类没有承担什么责任,不再有单独存在的理由,将这个类的所有特性内联到另一个类中,将原类移除
隐藏委托 1.通过封装,对外部客户隐藏内部的委托细节,避免内部的委托发生变化波及客户
2.可以在服务对象(中介,客户通过其得到另一对象)上放置委托函数,将委托关系隐藏起来,从而去除这种依赖。这么一来,即便将来发生委托关系上的变化,变化也将被限制在服务对象中,不会波及客户。
移除中间人 服务类做了太多的简单委托,移除服务类,让客户直接调用委托类
引入外部函数 客户类需要的少数几个功能,服务类不能提供,而且不能修改服务类源码,则可以在客户类创建函数提供所需的功能
引入本地扩展 需要在客户类建立大量的外部函数,则应该考虑将这些函数组织到新的类中,该新类应该是源类的子类,即本地扩展
  1. 数据
重构方法 说明
自封装字段 1.在一个类中,可以直接访问
2.如果为了在子类中改变获取数据的方式(如延迟获取)等,则可以通过取值函数/设置函数访问
以对象取代数据值 一个数据项,需要与其它的数据与行为放在一起才有意义,将数据项变成对象
以对象取代数组 一个数组,其中每个元素代表都是不同的东西,建议以对象代替数组,将数组中的每个元素作为对象的字段
复制“被监视数据” 将处理用户界面和处理业务逻辑的代码分开
以字面常量取代魔法数 一个字面数值,带有特殊含义,将其替换为有意义的常量,通过命名表达其含义
封装字段 有public的字段,将其改为private,并提供相应的访问函数
封装集合 1.一个函数返回一个集合,建议返回该集合的一个只读副本
2.不要提供对集合的设值(setter)函数,应该提供给集合添加/删除元素的函数
以类取代类型码 类中有一个数值类型码,但并不影响类的行为,以一个新的类替换类型码
以字段取代子类 各个子类的唯一差别是返回常量值的函数上,建议在父类中添加表示该常量值的字段,并通过函数返回,然后移除所有的子类
  1. 简化条件表达式
重构方法 说明
分解条件表达式 一个复杂的条件表达式(if-else),将每部分都提炼成单独的函数
合并条件表达式 1.多个条件表达式返回同样的结果,建议试用&或
合并重复的条件片段 条件表达式的每个分支上都有相同的代码,则应该将重复代码移到条件表达式之外
移除控制标记 用break或return语句替换控制标记(根据不同的条件给布尔变量赋予不同的值的布尔表达式),提前返回或退出
卫语句取代嵌套条件表达式 条件表达式中,有些分支是特殊情况,建议试用卫语句提前返回
多态取代条件表达式 使用多态替换根据对象类型的不同执行不同的行为的条件表达式:为每个不同类型建立一个子类,将分支中的内容放到子类的覆写方法中
引入Null对象 需要检查对象是否为null,可以考虑引入null对象。null对象是正常对象的一个子类,覆写的方法使用空实现,一般是单例,不可变
引入断言 1.对程序状态做出某种假设,以断言明确表明这种假设。断言应该总是为真,如果它失败,表明程序员犯了错误,应该抛出异常
2.只用于检查一定必须为真的条件,而不是用于检查你认为应该为真的条件
3. 生产环境的代码应该将断言全部都删掉
  1. 概括关系
重构方法 说明
字段上移 两个子类有相同的字段,将该字段移到超类中去
函数上移 函数在各个子类中产生完全相同的结果,将该函数移到超类
构造函数本体上移 子类中构造函数函数体几乎完全一致,在超类中新建一个构造函数,并在各个子类的构造函数中调用它
函数下移 超类中的某个函数只是被部分子类用到,将这个函数移到需要它的子类中去
字段下移 超类中的某个字段只是被部分子类用到,将这个字段移到需要它的子类中去
提炼子类 类的某些特性只被某些(不是全部)实例用到,新建一个子类,将特定的属性移到子类中去
提炼超类 两个类有相似特性,建立一个超类,将相同的特性移到超类
折叠继承体系 子类和超类并无太大区别,将它们合并
塑造模板函数 1.子类,其中的某个函数以相同顺序执行大致相近的操作,但是各操作不完全相同
2.将这些操作分别放进独立函数中,并保持它们都有相同的签名,然后将原函数上移至超类,子类重写实现不同的逻辑。
以委托取代继承 子类只使用了超类接口中的一部分,或者子类从超类继承了一大堆并不需要的数据,建议将继承改为委托
以继承取代委托 某个类使用了委托类中的所有函数,需要编写所有简单的委托函数,建议将委托改为继承

示例

  • 把意图和实现分开
void printOwing(double amount) {
printBanner();
printDetails(amount);
}
void printDetails (double amount) {
System.out.println ("name:" + _name);
System.out.println ("amount" + amount);
}
  • 用一个良好命名的临时变量来解释对应条件子句的意义,使语义更加清晰
final boolean isMacOs     = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
final boolean wasResized = resize > 0;
if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {
// do something
}
  • 以管道(filter+map)取代循环
const names = input
.filter(i => i.job === "programer")
.map(i => i.name)
;
  • 合理的封装。能够帮助我们隐藏细节并且,能够更好的应对变化,当我们发现我们的类太大而不容易理解的时候,可以考虑使用提炼类的方法

  • 分解条件式: 把一段 「复杂的条件逻辑」 分解成多个独立的函数

//原代码
if (date.before (SUMMER_START) || date.after(SUMMER_END))
charge = quantity * _winterRate + _winterServiceCharge;
else charge = quantity * _summerRate;
//分解
if (notSummer(date))
charge = winterCharge(quantity);
else charge = summerCharge (quantity);
  • 以卫语句(guard clauses,给某一条分支以特别的重视)取代嵌套条件式