01.重构-介绍

1. 定义

  • 重构(refactoring)是在不改变软件可观察行为的前提下改善其内部结构。
  • 重构是这样一个过程:在不改变代码外在行为的前提下,对代码进行修改,以改进程序的内部结构。
  • 重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高可理解性,降低其修改成本。
  • 重构(动词):使用一系列重构首发,在不改变软件可观察行为的前提下,调整其结构。

2. 原则

  • 不改变软件行为只是重构的最基本要求;
  • 不需了解软件行为才是目的;
  • 个人理解:封装完整性?调用 API 而无需了解 API 内部实现和行为。
  • 设计模式为重构提供了目标;
  • 判断哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数据放在一起。
    • ps:有些设计模式破坏这个原则,如 Strategy 和 Visitor,Self Delegation,这是为了对抗坏味道 Divergent Change。
    • 最根本的原则:将总是一起变化的东西放在一起。

3. 目的

  • 重构改进软件设计
    • 消除重复代码
  • 重构使软件更容易理解
  • 重构帮助找到 bug
  • 重构提高编程速度
  • 重构使得设计思路更详细明确;
  • 重构被用于开发框架、抽取可复用组件、使软件架构更清晰、使新功能增加更容易;
  • 重构可以减少重复劳动,使得程序更加简洁有力;

4. 经验

  • 重构前需建立一组可靠的测试环境,且必须能自我检验;
  • 重构技术就是以微小的步伐修改程序,如果出错,可以很容易发现;
  • 良好的名称是代码清晰的关键;
  • 尽量除去临时变量;
  • 最好不要在另一个对象的属性基础上运用 switch 语句,如果不得不使用,也应该在对象自己的数据上使用,而不是在别人的数据上使用;
  • 技术复审是减少错误、提高开发速度的一条重要途径。

5. 何时重构

  • 反对专门拨出时间进行重构;
  • 重构应该随时随地进行;
  • 不应该为了重构而重构;
  • 三次法则(事不过三,三则重构)
    • 第一次做某件事时只管去做;
    • 第二次做类似的事会产生反感,但无论如何还是可以去做;
    • 第三次再做类似的事,就应该重构。
  • 添加功能时重构;
  • 修补错误时重构;
  • 复审代码时重构;

6. 为什么重构有用 – Kent Beck

6.1 问题

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

6.2 期望

  • 容易阅读;
  • 所有逻辑都只在唯一地点指定;
  • 新的改动不会危及现有行为;
  • 尽可能简单表达条件逻辑;

7. 间接层

计算机科学是这样一门科学:它相信所有问题都可以通过增加一个间接层来解决。

Dennis DeBruler

8. 重构难题

8.1 数据库

  • 绝大多数程序都与其背后的数据库结构紧密耦合在一起;
  • 数据迁移(migration);
  • 在非对象数据库中,解决办法之一是:在对象模型和数据库模型之间插入一个分割层,隔离两个模型各自的编号;

8.2 修改接口

只有当需要修改的接口被哪些“找不到,即使找到也不能修改”的代码使用时,接口的修改才会成为问题。此时,这个接口是个已发布接口(published interface),比公开接口(public interface)更进一步。

  • 必须同时维护新旧两个接口,直到所有用户都有时间对这个变化做出反应;
    • 让旧接口调用新接口,千万不要复制函数实现;
    • 将旧接口标记为 deprecated(java 的已过时),例如 Java 容器类;
  • 尽量不要发布接口,除非真的有必要;
  • Java 还有一种特别的接口修改:在 throws 子句中增加一个异常。此时,需要新建函数,然后在旧函数调用它,并处理这个异常。

ps:不要过早发布接口。请修改你的代码所有权政策,使重构更顺畅。

8.3 难以通过重构手法完成的设计改动

先想象重构的情况,考虑候选设计方案难度,而后在设计上投入更多力气。

9. 何时不该重构

  • 有时候代码是在太混乱,重构还不如重新重写一个;
  • 重写(而非重构)的一个清楚讯号就是:现有代码根本不能正常运作;
  • 折中办法:将“大块头软件”重构为封装良好的小型组件;
  • 项目已近最后期限,此刻应该避免重构;

10. 代码的坏味道(Bad Smell)

10.1 Duplicated Code(重复代码)

  • Extract(提取) Method:重复代码;
  • Pull Up Method:多个子类重复代码,提取代码放到超类;
  • Form Template Method:各个子类有所差异,使用 Template Method(模板方法)模式;
  • Substitute(替代) Algorithm:不同算法做同样的事;
  • Extract Class:若两个毫不相关的类出现,则将重复代码提炼到一个独立类;

10.2 Long Method(过长函数)

“间接层”所能带来的全部利益–解释能力、共享能力、选择能力–都是由小型函数支持的。小函数真正容易理解的关键之处在于一个好名字。

  • Extract Method
  • Replace Temp with Query
  • Introduce Parameter Object
  • Preserve(保持) Whole Object
  • Replace Method with Method Object
  • Decompose(分解) Conditional

10.3 Large Class(过大的类)

  • Extract Class
  • Extract Subclass
  • Extract Interface:先确定客户端如何使用它们,而后为每种使用方式提炼出一个接口。
  • Duplicate Observed Data

10.4 Long Parameter List(过长参数列)

  • Replace Parameter with Method
  • Preserve Whole Object
  • Introduce Parameter Object:如果某些数据缺乏合理的对象归属,可为它们制造一个“参数对象”;
    -ps:例外情况,有时候不希望造成“被调用对象”与“较大对象”间的某种依赖关系。这时候将数据从对象中拆解出来单独作为参数,也很合情合理。但需注意引发的代价。若参数列太长或变化太频繁,就需要重新考虑依赖结构。

10.5 Divergent Change(发散式变化)

某个类因为不同原因在不同方向发生变化。此时也许将对象拆分更好,这么一来每个对象就可以只因一种变化而修改。

  • Extract Class

10.6 Shotgun Surgery(散弹式修改)

某种变化引发在许多不同的类内做出许多小修改。考虑把所有需要修改的代码放进同一个类。

  • Move Method
  • Move Field
  • Inline Class

ps:Divergent Change 是指“一个类受多种变化的影响”,Shotgun Surgery 则是指“一种变化引发多个类相应修改”。

10.7 Feature Envy(依恋情结)

  • Extract Method
  • Move Method

10.8 Data Clumps(数据泥团)

两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现的数据真应该拥有数据它们自己的对象。

  • Extract Class
  • Introduce Parameter Object
  • Preserve Whole Object

10.9 Primitive Obsession(基本类型偏执)

  • Replace Data Value with Object
  • Replace Type Code with Class
  • Replace Type Code with Subclass
  • Replace Type Code with State/Strategy
  • Extract Class
  • Introduce Parameter Object
  • Replace Array with Object

10.10 Switch Statements(switch表达式)

面向对象程序的一个最明显的特征就是:少用 switch(或 case)语句,问题在于重复,同样 switch 语句散布不同地方,需同时修改。解决方案是多态

  • Extract Method
  • Extract Class
  • Replace Type Code with Subclass
  • Replace Type Code with State/Strategy
  • Replace Conditional with Polymorphism(多态性)
  • Replace Parameter with Explicit(明确的) Methods
  • Introduce Null Object

10.11 Paraller Inheritance Hierarchies(平行继承体系)

Shotgun Surgery 的特殊情况,每当你为某个类增加一个子类,必须也为另一个类相应增加一个子类。
策略:让一个继承体系的实例引用另一个继承体系的实例。

  • Move Method
  • Move Field

10.12 Lazy Class(冗赘类)

你所创建的每一个类,都得有人去理解和维护它,若一个类重构后完全没啥意义,就应该考虑删除它。

  • Collapse(崩溃、折叠) Hierarchy
  • Inline Class

10.13 Speculative Generality(夸夸其谈未来性)

  • Collapse Hierarchy
  • Inline Class
  • Remove Parameter
  • Rename Method

10.14 Temporary Field(令人迷惑的临时字段)

  • Extract Class
  • Introduce Null Object

10.15 Message Chains(过度耦合的消息链)

  • Hide Delegate
  • Extract Method
  • Move Method

10.16 Middle Man(中间人)

过度委托

  • Remove Middle Man
  • Inline Method
  • Replace Delegation with Inheritance

10.17 Inappropriate Intimacy(不恰当的亲密关系,即狎昵关系)

  • Move Method
  • Move Field
  • Change Bidirectional(双向) Association(关联) to Unidirectional(单向)
  • Extract Class
  • Hide Delegate
  • Replace Inheritance with Delegation

10.18 Alternative Classes with Different Interfaces(异曲同工的类)

  • Rename Method
  • Move Method
  • Extract SuperClass

10.19 Incomplete Library Class(不完美的类库)

  • Introduce Foreign Method
  • Introduce Local Extension

10.20 Data Class(纯粹的数据类)

指拥有一些字段以及访问(读写)这些字段的函数的类,即 Model 实体类。

  • Encapsulate(封装) Field
  • Encapsulate Collection
  • Remove Setting Method
  • Move Method
  • Extract Method
  • Hide Method

10.21 Refused Bequest(被拒绝的遗赠)

子类不需要超类的函数和数据,意味着继承体系设计错误。

  • Push Down Method
  • Push Down Field
  • Replace Iniheritance with Delegation

10.22 Comments(过多的注释)

注释太长太多意味着代码很糟糕。

  • Extract Method
  • Rename Method
  • Introduce Assertion
    ps:当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余。

11 测试

如果你想进行重构,首要前提就是拥有一个可靠的测试环境。
ps

  • 确保所有测试都完全自动化,让它们检查自己的测试结果。
  • 一套测试就是一个强大的 bug 侦测器,能够大大缩减查找 bug 所需要的时间。
  • 当事情被认为应该会出错时,别忘了检查是否抛出预期的异常。

其他

示例

  • Replace Type Code with State/Strategy
  • Move Method
  • Replace Conditional with Polymorphism
  • Form Template Method
  • Replace Temp with Query
  • Extract Method
  • Self Encapsulate(自封装) Field

重构 vs 性能优化

类型 重构 性能优化
内部结构 改变 改变
外部行为 不改变或很小改变 不改变(除了执行速度)
目的 软件更容易被理解和修改,性能没有变化 性能提升,但往往使代码较难理解

观点

  • 怎么对经理说重构:很多经理嘴巴上说自己“质量驱动”,实际是“进度驱动”,这种情况建议是:不要告诉经理!
  • 学习一种可以大幅提高生产力的新技术时,你总是难以察觉其不适用的场合。通常你在一个特定场景中学习它,这个场景往往是个项目。这种情况下你很难看出什么会造成这种新技术成效不彰或形成危害。
  • 他把未完成的重构工作形容为“债务”。很多公司都需要借债来使自己更有效的运转,但借债就得付利息,过于复杂的代码所造成的维护和扩展的额外成本就是利息。你可以承受一定程度的利息,但如果利息太高,你就会被压垮。把债务管理好是很重要的,你应该随时通过重构来偿还一部分债务。

    Ward Cunningham
  • 重构与设计互补。
  • 如果在所有可能的变化出现地点都建立起灵活性,整个系统的复杂度和维护难度都会大大提高。当然,如果最后发现所有这些灵活性都毫无必要,这才是最大的失败。
  • 重构可以带来更简单的设计,同时又不损失灵活性,也降低了设计过程的难度,减轻了设计压力。
  • 哪怕你完全了解系统,也请实际度量它的性能,不要瞎猜。瞎猜可能会让你学到一些东西,但十有八九是错的。

    Ron Jeffries
  • 如何确定该提炼哪一段代码:寻找注释,它们通常能指出代码用途和实现手法之间的语义距离。
  • 对象技术的全部要点:将数据和对数据的操作行为包装到一起。

参考

  • 《重构-改善既有代码的设计》(第1-4章)
坚持原创技术分享,您的支持将鼓励我继续创作!