《重构_改善既有代码的设计》这本书还没有读完,因为内容太多了。但是项目已重构完成。因此,有一些感悟,顺便查阅一些资料,写下这篇文章,加深一下自己对重构的认知。
认识重构
所谓重构,就是在不改变软件系统外部行为的前提下,改善它的内部结构。
重构是对软件内部的一种调整,目的是在不改变软件可观察行为的前提下,提高可理解性,降低其修改成本。
重构是一种经千锤百炼形成的有条不紊的程序整理方法,可以最大限度地减少整理过程中引入错误的几率。
本质上说,重构就是在代码写好之后改进它的设计。
重构不会改变软件可观察的行为 —— 重构之后软件功能一如以往。
为什么要重构
重构有风险,它必须修改运行中的程序,这可能引入一些不易察觉的错误。那么,为什么我们还要重构呢?
我们希望的程序是这样的:
- 容易阅读
- 所有的逻辑都只在唯一地点指定 (单一原则,去重,提取)
- 新的改动不会危及现有行为 (可拓展性,重用性)
- 尽可能简单表达条件逻辑
重构是这样一个过程:它在一个目前可运行的程序上进行,在不改变程序行为的前提下使其具备上述的美好性质,使我们能够继续保持高速开发,从而增加程序的价值。
重构的目的
- 重构改进软件设计
- 重构使软件更容易理解
- 重构帮助找到bug
- 重构提高编程速度
总结,为了高效率的编程,为了减少bug率,为了提高代码质量;越是复杂的项目,重构的好处就越明显。
重构有助于软件的迭代开发和二次开发。
重构的原则
- 随时可以停止
- 对外表现的功能一致,相同输入得到相同输出
- 重构时不要添加新的功能
- 合适就行,考虑具体情况,不要太执着于重构
- 需要对重构的代码进行测试
- 符合编程语言代码规范
何时重构
- 三次法则:事不过三,三则重构
- 添加功能时重构
- 修补错误时重构
- 复审代码时重构
重构的难点
- 数据库:程序与数据库结构紧密耦合在一起;数据迁移
- 修改接口:谨慎修改接口,如果接口已发布,必须维护旧的接口
何时不该重构
- 重写:现有代码根本不能正常运作
- 代码太混乱
- 项目已接近最后期限
关于测试
- 确保所有测试都完全自动化,让它们检查自己的测试结果
- 考虑可能出错的边界条件,把测试火力集中在那儿
- 编写未完善的测试并运行,好过对完美测试的无尽等待
- 当事情被认为应该出错时,别忘了检查是否抛出了预期的异常
- 不要因为测试无法捕捉所有的bug就不写测试,因为测试的确可以捕捉到大多数bug
代码的坏味道
名称 | 备注 |
---|---|
重复代码 | 同一个类的两个函数有相同表达式,提取方法到超类或独立类 |
过长函数 | 当需要用注释来说明一段代码时,就需要把这部分代码写入一个独立的函数中 |
过大的类 | 为每一种使用方式提取出一个接口 |
过长的参数列 | 将参数设置为对象 |
发散式变化 | 一个类受到多种变化的影响 |
散弹式修改 | 一个变化引起多个类修改 |
依恋情结 | 一个函数对某个类的兴趣高于对自己所处类的兴趣,通常是过多访问其它类的数据 |
数据泥团 | 有些数据经常一起出现,比如两个类具有相同的字段、许多函数有相同的参数 |
基本类型偏执 | 使用类往往比使用基本类型更好 |
switch 惊悚现身 | 面向对象中的多态概念可为此带来优雅的解决方法 |
平行继承体系 | 每当为某个类增加一个子类,必须也为另一个类相应增加一个子类 |
冗余类 | 如果一个类没有做足够多的工作,就应该消失 |
夸夸其谈未来性 | 有些内容是用来处理未来可能发生的变化,但是往往会造成系统难以理解和维护 |
令人迷惑的暂时字段 | 某个字段仅为某种特定情况而设,这样的代码不易理解,因为通常认为对象在所有时候都需要它的所有字段 |
过度耦合的消息链 | 一个对象请求另一个对象,然后再向后者请求另一个对象,然后...,这就是消息链 |
中间人 | 中间人负责处理委托给它的操作,如果一个类中有过多的函数都委托给其它类,那就是过度运用委托 |
狎昵关系 | 两个类多于亲密,花费太多时间去探讨彼此的 private 成分。 |
异曲同工的类 | 两个函数做同一件事,却有着相同的签名 |
不完美的类库 | 类库往往不可能满足我们所有的工作 |
纯稚的数据类 | 它只拥有一些数据字段。 |
被拒绝的馈赠 | 子类继承超类的所有函数和数据,但是它只想要一部分。 |
过多的注释 | 糟糕的代码导致过多的注释 |
重构的方法
重新组织函数
名称 | 解释 | 动机 |
---|---|---|
提炼函数 | 将这段代码放进一个独立函数中,并让函数名称解释该函数的用途。 | 函数过长;多次出现的代码块 |
内联函数 | 在函数调用点插入函数本体,然后移除该函数。 | 重用率不高且简单的代码、太多间接层 |
内联临时变量 | 将所有对该变量的引用动作,替换为对他赋值的那个表达式自身。 | 临时变量妨碍到其他重构手法时 |
以查询取代临时变量 | 将表达式提炼到一个独立的函数中,将有改表达式的地方替换为新函数 | 类中可全局查询,表达式可能变动时 |
引入解释变量 | 将一个复杂的表达式或其一部分的结果放进临时变量,以此变量来解释表达式的用途。 | 表达式非常复杂、难以阅读时;表达式太长时 |
分解临时变量 | 针对每次赋值,创造一个独立、对应的临时变量 | 临时变量有不同用途且被多次赋值时;临时变量承担多个责任时 |
移除对参数的赋值 | 以一个临时变量取代该参数的位置 | 代码对参数进行赋值时 |
以函数对象取代函数 | 将这个函数放进一个单独的对象中,如此一来局部变量就成了对象内的字段,然后就可以将这个大型函数分解为多个小型的函数。 | 一个大型的函数,有太多局部变量时 |
替换算法 | 将函数本体替换为另一种算法 | 想要将某个算法替换为另一个更清晰的算法时 |
在对象之间搬移特性
名称 | 解释 | 动机 |
---|---|---|
搬移函数 | 将函数迁移到最常引用的类中,旧函数变成单纯的委托或者移除掉。 | 一个类有太多行为,或与另一个类有太多合作形成高度耦合时 |
搬移字段 | 将字段迁移到目标类中,将源字段所有引用都改用为目标类的新字段 | 在其所驻之类之外的另一个类更多的使用到该字段时 |
提炼类 | 建立新类,将相关的字段和函数迁移到新类中 | 某个类做了应该由两个类做的事时 |
将类内联化 | 将这个类的所有特性搬移到另一个类中,然后移除原类 | 当一个类不再承担足够责任、不再有单独存在的理由时 |
隐藏“委托关系” | 在服务类上建立客户所需的所有函数,用以隐藏委托关系 | 客户通过一个委托类调用另一个对象 |
移除中间人 | 让客户直接调用委托类 | 某个类做了过多的简单的委托动作 |
引入外加函数 | 在客户类中建立一个函数,并以第一参数形式传入一个服务类实例。 | 你需要为提供服务的类增加一个函数,但你无法修改这个类 |
引入本地扩展 | 建立一个新类,使他包含这些额外函数 | 你需要为提供服务的类提供一些额外的函数,但你无法修改这个类 |
重新组织数据
名称 | 解释 | 动机 |
---|---|---|
自封装字段 | 为这个字段设置set/get函数,并且以这些函数来访问字段 | 降低与字段之间的耦合 |
以对象取代数据值 | 将数据项变成对象 | 一些数据项需要和其他数据和行为一起使用才有意义 |
将值对象改成引用对象 | 将这个值对象变成引用对象 | 给对象增加一些可修改数据,并确保对任何一个对象的修改都能影响到所有的引用此对象的地方时 |
将引用对象改为值对象 | 将它变成一个值对象 | 一个引用对象很小且不可变且不易管理时 |
以对象取代数组 | 已对象替换数组,对于数组的每一个元素都以一个字段表示 | 一个数组中的元素各自代表不同东西时 |
赋值“被监视数据” | 将数据复制到一个领域对象中,建立Observer模式,用以同步领域对象和GUI对象内的重复数据 | 一些领域对象数据置身于GUI控件中,而领域对象函数需要访问这些数据 |
将单向关联改为双向关联 | 添加一个反向指针,并使修改函数同时更新两条连接 | 两个类都需要使用到对方特性时 |
将双向关联改为单向关联 | 去除不必要的关联 | 双向关联的类变成单向依赖时 |
以字面常量取代魔法数 | 为字面数值设置常量,并将数值替换为这个常量 | 如果有特殊意义的字面数值时 |
封装字段 | 将它声明为private,并提供相应的访问函数 | 类中存在public字段时 |
封装集合 | 让函数返回只读副本,并在这个类上提供增加/删除集合元素的函数 | 降低集合拥有者与用户之间的耦合 |
以数据类取代记录 | 为该记录创建一个“哑”数据对象 | 面对一个遗留程序程序时;需要与传统API交流时;处理从数据库读出来的记录时 |
以类取代类型码 | 以一个新的类替换该类型数值码 | 类中有数值类型码但不影响类的行为 |
以子类取代类型码 | 以子类取代类型码 | 有一个不可变的类型码,且会影响到类的行为时 |
以 State/Strategy 取代类型码 | 以状态取代类型码 | 有一个不可变的类型码,且会影响到类的行为,但无法用继承手法消除时 |
以字段取代子类 | 修改这些函数,使他们返回超类的某个字段,然后销毁子类 | 当各个子类唯一的差别只在“返回数据常量”的函数身上时 |
简化条件表达式
名称 | 解释 | 动机 |
---|---|---|
分解条件表达式 | 从if/then/else三个段落中分别提炼出独立函数 | 当条件语句太复杂时 |
合并条件表达式 | 将处理结果一致条件合并为一个条件,并提炼成为一个独立的函数 | 有一系列条件,得到相同的处理方式时 |
合并重复的条件片段 | 将这段重复的代码搬移到条件表达式之外 | 在条件表达式的每一个分支有着相同的一段代码时 |
移除控制标记 | 以break或return取代控制标志 | 某个变量带有“控制标记”的作用 |
以卫语句取代嵌套条件表达式 | 时使用卫语句表现所有的特殊情况 | 当条件逻辑有太多嵌套,难以看清执行路径时 |
以多态取代条件表达式 | 将条件表达式的每一个分支放进一个子类内的复写函数中,然后将原始函数声明为抽象函数 | 有一个条件表达式,根据对象类型的不同而选择不同的行为时 |
引入Null对象 | 将null值替换为null对象 | 你需要再三检查某对象是否为null时 |
引入断言 | 以断言明确表现这种假设 | 某一段代码需要对程序状态做出某种假设时 |
简化函数调用
名称 | 解释 | 动机 |
---|---|---|
函数改名 | 修改函数名称 | 函数名称未能揭示函数的用途时 |
添加参数 | 为函数添加一个参数对象参数,让该对象带进函数所需的信息 | 某个函数需要从调用端得到更多的信息 |
移除参数 | 将该参数去掉 | 函数本体不再需要某个参数时 |
将查询函数和修改函数分离 | 建立两个不同的函数,其中一个负责查询,另一个负责修改 | 某个函数既返回对象状态值,又修改对象状态时 |
令函数携带参数 | 建立单一函数,以参数表达那些不同的值 | 若干函数做了类似的工作,但函数本体中却包含了不同的值 |
以明确函数取代参数 | 针对参数的每一个可能值,建立一个独立的函数 | 有一个函数,行为的区别完全取决于不同的参数 |
保持对象完整 | 改为传递整个对象 | 当需要从某个对象取出若干值作为某个函数的参数时 |
以函数取代参数 | 让参数接受者去除该项参数,并直接调用前一个函数 | 当一个函数的返回值作为另一个函数的参数,且另一函数能调用该函数时 |
引入参数对象 | 以一个对象取代这些参数 | 某些参数总是很自然地同时出现时 |
移除设值函数 | 去掉该字段的所有设值函数 | 类中某个字段应该在该对象创建时被设值,然后不再改变 |
隐藏函数 | 将这个函数设置为private | 有一个函数,从来没有被其他任何类用到 |
以工厂函数取代构造函数 | 将构造函数替换为工厂函数 | 当创建对象时不仅仅是需要做简单的构建动作时 |
封装向下转型 | 将向下转型动作移到函数中 | 某个函数转型的对象,需要由函数调用者向下转型,如抽象类强制转换为具体类时 |
以异常取代错误码 | 改用异常 | 某个函数返回一个特定的代码,用以表示某种错误情况 |
以测试取代异常 | 修改调用者,使它在调用函数之前先做检查 | 面对调用者可以预先检查的条件,你抛出了异常时 |
处理概括关系
名称 | 解释 | 动机 |
---|---|---|
字段上移 | 将该字段移至超类 | 子类拥有相同的字段时 |
函数上移 | 将该函数移至超类 | 有些函数,在子类中产生相同的结果 |
构造函数本体上移 | 在超类中新建一个构造函数,并在子类构造函数中调用它 | 各个子类的构造函数拥有几乎一致的本体时 |
函数下移 | 将函数移到相关子类中去 | 超类中的某个函数只与部分子类有关 |
字段下移 | 将字段移到需要它的那些子类中去 | 超类中的某个字段只被部分子类用到 |
提炼子类 | 新建一个子类,将上面所说的那一部分特性移到子类中去 | 类中的某些特性只被某些实例用到 |
提炼超类 | 为相似的类建议一个超类,将相同的特性移至超类 | 一些类有相似的特性时 |
提炼接口 | 将相同的子集提炼到一个独立的接口中 | 一些类具有相同的行为时 |
折叠继承体系 | 将子类和超类合为一体 | 子类和超类无太大区别时 |
塑造模板函数 | 将这些操作分别放进独立函数中,并保持他们都有相同的签名,于是原函数也变得相同了,再将原函数移至超类 | 一些子类相应的某些函数以相同的顺序执行某些操作,但各个操作的细节上有些不同时 |
以委托取代继承 | 在子类新建一个字段用以保存超类,调整子类函数,改为委托超类,然后去掉两者之间的继承关系 | 某个子类只使用了超类接口中的一部分,或是根本不需要继承而来的数据时 |
以继承取代委托 | 让委托继承受托类 | 两个类之间存在委托关系,且极简的委托函数太多时 |
大型重构
四个大型重构
名称 | 解释 | 动机 |
---|---|---|
梳理并分解继承体系 | 建立两个继承体系,并通过委托关系让其中一个可以调用另一个 | 某个继承体系同时承担两项责任 |
将过程设计转化为对象设计 | 将数据记录变成对象,将大块的行为分成小块,并将行为移入相关对象中 | 有一些传统过程化的代码 |
将领域和表述/显示分离 | 将领域逻辑分离出来,为他们建立独立的领域类 | 某些GUI类之中包含了领域逻辑 |
提炼继承体系 | 建立继承体系,以一个子类表示一种特殊情况 | 某个类做了太多工作,其中一部分工作是以大量条件表达式完成的 |
总结
总之,重构得分情况,需要了解动机。某些重构手法是对立的,这时候就更加需要具体情况具体分析了,适合的才是最好的。