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. 间接层
计算机科学是这样一门科学:它相信所有问题都可以通过增加一个间接层来解决。
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 性能优化
类型 | 重构 | 性能优化 |
---|---|---|
内部结构 | 改变 | 改变 |
外部行为 | 不改变或很小改变 | 不改变(除了执行速度) |
目的 | 软件更容易被理解和修改,性能没有变化 | 性能提升,但往往使代码较难理解 |
观点
- 怎么对经理说重构:很多经理嘴巴上说自己“质量驱动”,实际是“进度驱动”,这种情况建议是:不要告诉经理!
- 学习一种可以大幅提高生产力的新技术时,你总是难以察觉其不适用的场合。通常你在一个特定场景中学习它,这个场景往往是个项目。这种情况下你很难看出什么会造成这种新技术成效不彰或形成危害。
他把未完成的重构工作形容为“债务”。很多公司都需要借债来使自己更有效的运转,但借债就得付利息,过于复杂的代码所造成的维护和扩展的额外成本就是利息。你可以承受一定程度的利息,但如果利息太高,你就会被压垮。把债务管理好是很重要的,你应该随时通过重构来偿还一部分债务。
- 重构与设计互补。
- 如果在所有可能的变化出现地点都建立起灵活性,整个系统的复杂度和维护难度都会大大提高。当然,如果最后发现所有这些灵活性都毫无必要,这才是最大的失败。
- 重构可以带来更简单的设计,同时又不损失灵活性,也降低了设计过程的难度,减轻了设计压力。
哪怕你完全了解系统,也请实际度量它的性能,不要瞎猜。瞎猜可能会让你学到一些东西,但十有八九是错的。
- 如何确定该提炼哪一段代码:寻找注释,它们通常能指出代码用途和实现手法之间的语义距离。
- 对象技术的全部要点:将数据和对数据的操作行为包装到一起。
参考
- 《重构-改善既有代码的设计》(第1-4章)