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