《重构》的读书笔记
学习基础
熟悉《设计模式》的基本概念,熟悉基本的 Java 语法,熟悉 Eclipse 和 JUnit 的使用,有相对较好的英语基础。
学习过程
- 先看第 1 章,手工输入实例程序,了解重构的方法和过程。重点是理解重构的思路,最好的理解方式就是通过实践的方式理解。
- 再看第 2~4 章,内容为选择性阅读,没兴趣或者看不懂的都可以跳过,因为后面还可以回头再读。
- 接着第 5~12 章,最好按顺序把代码一个个输入,再按照作者的步骤重构操作一次,并结合自己以往工作中的实践来理解。
学习目的
使自己编写的代码更容易被人读懂。
学习感悟
- 代码的重构应该是一步步完成的,每次重构的部分不要超过自己的理解能力的 5%。虽然这样操作略显繁琐,但是可以减轻头脑重构过程中的记忆强度,减少代码出错的机会。
- 代码的重构一定要配合 JUnit ( TDD,测试驱动开发 ) 完成,再加上 Git ( 版本管理 ) 和 Eclipse ( IDE 的重构工具 ) 那就事半功倍了。
学习代码
总览
- 第 1 章 ( 必读 ) ,从实例程序出发,了解重构的方法和过程。
- 第 2 章,讨论重构的一般性原则、定义和进行重构的原因。
- 第 3 章,介绍如何判断问题代码,以及如何用重构改善它们。
- 第 4 章,在代码中构建 java 的测试环境
- 第 5~12 章,具体面对的问题和重构的方法。
- 第 13 章,Bill Opdyke 在商业开发中应用重构
- 第 14 章,自动化重构工具 ( 今天看来,已经不是太大问题,Eclipse 的 Refactor 已经非常好用 )
- 第 15 章,重构的哲学思想
第 1 章 重构,第一个案例
1.1 起点 P_1
因为代码的结构无法满足添加新功能的需要,因此先进行重构,使代码方便添加新功能,然后再添加新功能。
1.2 重构的第一步 P_7
首先确认自己拥有一套可靠的测试机制,因为重构有可能引入问题,通过测试保证重构没有改变程序功能。
第 2 章 重构原则
2.1 何谓重构 P_53
重构 ( 名词 ) : 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高理解性和降低修改成本。
重构 ( 动词 ) : 使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
定义的扩展 :
- 重构让软件更容易理解和修改
- 重构不会改变软件的可观察行为,即使改变也只能是微小的影响,软件功能一如既往。
重构的目标 : 只改变程序内部结构,不添加新的功能
不要一心二用 :
- 添加新功能的时候,不重构已有代码。
- 重构代码时,不增加新功能。
2.2 (P55)为何重构
- 重构改进软件设计 :
- 程序的设计质量在没有持续重构的情况下逐渐变差,功能的增加或者修改都可能使代码越来越难以理解和维护,就越难保证最初的设计目标
- 消除重复的代码一方面是程序运行更快,一方面是方便未来的修改,例如 : 重构减少代码重复,避免功能的改变需要修改多处代码。
- 重构使软件更容易理解 :
- 及时填补“想要它做什么”和“告诉它做什么”之间的缝隙。重构的核心就是要“准确说出我所要的”
- 重新阅读代码的人有可能是自己,也可能是他人。
- 通过重构可以把自己不熟悉的代码的用途梳理一遍,加深对代码的理解
- 重构帮助找出 bug :
- 这个是建立在代码容易理解之上的
- 重构提高编程速度 :
- 重构达到良好的设计,而良好的设计更容易修改代码、增加功能、问题调试。
2.3 (P57)何时重构
重构的三次法则 :
- 第一次开发某个功能的时候以实现为目标。
- 第二次开发相同功能的时候,克制自己的反感,继续重复实现。
- 第三次开发相同功能的时候,应该重构。
重构的时间点 :
- 添加功能时重构 :
- 一方面可以帮助理解需要修改的代码
- 一方面是使现在以及未来增加新功能更加容易。
- 修补错误时重构 :
- 出现 bug 的时候,难以找出问题所在的时候,很有可能是代码不清晰导致查找 bug 的困难。
- 复审代码时重构 :
- 复审代码有助于知识的传播,有利于代码被编写者之外的人理解。
- 重构加深了对代码的理解,有利于提升复审代码的能力
复审团队 : 只要代码作者和一个审查者者。较大的项目可以通过 UML 图去展示代码的逻辑。
程序难以修改的原因 :
- 难以阅读的程序
- 逻辑重复的程序
- 添加新特性需要修改已有代码的程序
- 带复杂逻辑判断的程序
重构的目标 :
- 代码容易阅读
- 所有逻辑都只有唯一地点指定
- 新的改动不会危及现有行为
- 尽可能简单表达逻辑
2.4 (P60)怎么对经理说
- 懂技术的经理,很容易沟通;
- 追求质量的经理,介绍重构对质量的帮助;
- 追求进度的经理,则自己安静地重构。因为重构可以最快的完成任务,就是对经理最大的帮助。
间接访问
很多时候重构都为程序引入间接访问 :
- 把大型对象拆分成小对象
- 把大型函数拆分为小型函数。
间接访问的价值 :
- 允许逻辑共享 : 一个函数在不同地点被调用。子类共享超类的方法。
- 分开解释意图和实现 : 通过类名和函数名解释自己的意图
- 隔离变化 : 在不同地方使用同一个对象,需要修改一处逻辑,那么可以做出子类,并在需要的时候修改这个子类。
- 封装条件逻辑 : 运用多态。将条件逻辑转化为消息模式。
减少间接层的条件: 当间接层只在一处使用,那么需要将其消除。
2.5 (P62)重构的难题
数据库重构 :
- 存在问题 :
- 程序与数据库耦合在一起。
- 数据迁移。
- 解决方案 :
- 在非关系型数据库,可以在数据库和对象模型中插入一个分离层,隔离两者之间的变化
接口重构
- 对于已经发布的接口需要可能需要维护旧接口和新接口,用 deprecated 修饰旧接口。
- 不发布新接口,在旧接口中调用新接口。
- 假如新接口抛出编译时异常,那么可以在旧接口中调用新接口并将编译时异常转化为运行时异常。
不重构的条件 :
- 重构之前,代码在大部分情况下都能够正常运行,就可以重构,否则应该是重写。
- 到了 Deadline,应该避免重构。
2.6 (P66)重构与设计
重构与设计是彼此互补的 :
- 设计应该在编码之前,但是设计总有缺陷,随着对问题认识的逐渐深入,通过重构可以改善设计的质量。
- 重构减轻了设计的难度和压力,在程序不断修改的过程中逐步完善程序的设计。
2.7 (P69)重构与性能
重构是有可能导致程序运行变慢的,但是不需要在设计和编码时就考虑性能问题。
例如 : 实时程序的编写 :
- 首先写出可调的程序
- 然后调整它以达到性能的要求。
- 经过分析大部分程序的主要时间是消耗在小部分代码上,所以不用对所有代码进行优化。
- 性能优化放在开发的后期,利用分析工具找出消耗大量时间的代码,然后集中优化。
第 3 章 代码的坏味道
3.1 (P76)Duplicated Code ( 重复代码 )
- 同个类两个函数存在相同表达式 : Extract Method ( 提炼函数 )
- 互为兄弟类内存在相同表达式 :
- Extract Method→PullUp Method ( 函数上移 )
- 如果代码只是相似 : 先运用 Extract Method ( 提炼函数 ) 分开再 Form TemPlate Method ( 塑造模板函数 )
- 两个毫不相干的类存在重复代码 : Extract Class ( 提炼类 )
3.2 (P76)Long Method ( 过长函数 )
原则 : 尽量利用函数名称来解释用途,而不是注释。
关键 : 代码主要用来描述“做什么”,而不是描述“怎么做”。例如 : getAge()表达获取年龄,而 today-birthday 就增加了理解的间接性,虽然看代码的人也能明白含义,但是就需要多想一下,并且 birthday 有可能表达的不是某个人的出生日期呢,而是某个买回来的产品的呢?那可能表达的就是使用时长了。
具体情况 :
- 函数有大量参数和临时变量 : Extract Method ( 提炼函数 )
- 用 Replace Temp with Query ( 以查询取代临时变量 ) 消除临时变量
- 用 Introduce Parameter Object ( 引入参数对象 ) 或者 Preserve Whole Object ( 保持对象完整 ) 来将多长的参数列表变得简洁一点。
- 如果按照上述步骤还存在太多变量和参数就需要用到 Replace Method with Method Object ( 以函数对象取代函数 )
- 条件表达式可以用 Decompose Conditional ( 分解条件表达式 ) 解决
- 可以将循环内的代码提炼为函数。
3.3 (P78)Large Class ( 过大的类 )
有时候类并非在所有时刻都使用实例变量 : 使用 Extract Method 和 Extract Subclass ( 提炼子类 )
类中有太多代码 :
- Extract Class ( 提炼类 )
- Extract Subclass ( 提炼子类 )
- Extract Interface ( 提供接口 ) 分解类的行为。
存在 GUI 的时候,可以 Duplicate Observed Data ( 复制“被监视数据” ) ,分离数据和行为到业务模型中去。
3.4 (P78)Long Parameter List ( 过长参数列 )
- 如果可以调用已有对象获取的话可以使用 Replace Parameter with Methods ( 以函数取代参数 )
- 将来自同一对象的数据收集起来,以该对象替代 : Preserve Whole Object ( 保持对象完整 )
- 如果几个参数总是同时出现,那么可以考虑 Introduce Parameter Object ( 引入参数对象 )
3.5 (P79)Divergent Change ( 发散式变化 )
不同的变化影响着相同的类发生改变,即变化的认知有分歧 ( Divergent ) 。
通过 Extract Class 把不同的功能封装到不同的类中,使每个类只因一种变化而需要修改
3.6 (P80)Shotgun Surgery ( 霰弹式修改 )
相同的变化会涉及到多个类发生修改,类似霰弹枪射击的效果。
可以通过 Extract Method,Move Method,Inline Class 把一种变化产生的多个修改移到同一个类中。
对比 :
- Divergent Change ( 发散式变化 ) 是一个类受到的多个变化影响;
- Shotgun Surgery ( 霰弹式修改 ) 是一个变化引起多个类需要修改。
3.7 (P80)Feature Envy ( 依恋情结 )
类中的某个函数对其他类的依赖度过高,则应该通过 Move Method ( 移动函数 ) 将它搬移到合适的类中。
3.8 (P81)Data Clumps ( 数据泥团 )
数据项总是成群结队出现,通过 Extract Class 将它们提炼到一个独立对象中,从而缩短参数列表,简化函数调用。
判断数据项是否相关的方法 :
如果这些数据项不在一起时就失去了明确的含义,那么就可以把它们提炼成一个新的对象。
3.9 (P81)Primitive Obsession ( 基本类型偏执 )
- 有些字段可以用对象表示更准确 Replace Data Value with Object ( 以对象取代数据值 )
- 对于不影响行为的类型码可以 Replace Type Code with Class ( 以类取代类型码 )
- 影响行为的类型码可以 Replace Type Code with Subclasses ( 以子类取代类型码 ) ,类型码在运行时会变化就用 Replace Type Code with State/Strategy ( 以 State/Strategy 取代类型码 )
3.10 (P82)Switch Statements ( switch 惊悚现身 )
- 使用 Replace Type Code with Subclasses ( 以子类取代类型码 ) 或者 Replace Type Code with State/Strategy ( 以 State/Strategy 取代类型码 )
- 轻量级的解决方法 : Replace Parameter with Explicit Methods ( 以明确函数取代参数 )
3.11 (P83)Parallel Inheritance Hierarchies ( 平行继承体系 )
每当为一个类增加子类必须也为另外一个类增加一个子类,那么就让一个继承体系的实例引用另一个继承体系的实例。
3.12 (P83)Lazy Class ( 冗赘类 )
没用的类,使用 Inline Class ( 内联类 ) 或者 Collapse Hierarchy ( 折叠继承体系 ) 来解决
3.13 (P83)Speculative Generality ( 夸夸其谈未来性 )
- 为未来设计的类,使用 Inline Class ( 内联类 ) 或者 Collapse Hierarchy ( 折叠继承体系 ) 来解决
- 为未来设计的函数参数,使用 Remove Parameter ( 移除参数 )
- 函数名称啰嗦,使用 Rename Method ( 函数改名 )
3.14 (P84)Temporary Field ( 令人迷惑的暂时字段 )
对象中某个字段仅为特定情况而设。使用 Extract Class ( 提炼类 ) 将这个字段提取出来
3.15 (P84)Message Chains ( 过度耦合的消息链 )
消息链 : 用户通过一个对象获取另一个对象,再通过获取的对象请求另一个对象,如此操作就是消息链。采取这种方式意味着客户代码将与查找过程中的导航结构紧密耦合,可以使用 Hide Delegate ( 隐藏“委托关系” ) 进行重构。但是谨慎处理!
3.16 (P85)Middle Man ( 中间人 )
过度委托形成中间人:Remove Middle Man ( 移除中间人 )
如果中间人还有其他行为,Replace Delegation with Inherited ( 以继承取代委托 )
3.17 (P85)Inappropriate Intimacy ( 狎昵关系 )
- 两个类相互依赖过多,花费大量时间去获取对方的 private 成员内容,使用 Move Field ( 移动字段 ) 和 Move Method ( 移动方法 ) 减少耦合性,或用 Change Bidirectional Association to Unidirectional ( 将双向关联改为单向关联 )
- 如果两个类无法移动相同数据和函数,可以使用 Extract Class ( 提炼类 ) , 让他们使用新类进行交互。
3.18 (P85)Alternative Classes with Different Interfaces ( 异曲同工的类 )
两个函数做了相同的事情却有不同的函数名称
3.19 (P86)Incomplete Library Class ( 不完美的库类 )
库函数功能不足,需要增加一些自定义的功能 :
- 需要加入少量操作,使用 Introduce Foreign Method ( 引入外加函数 )
- 需要加入大量操作,使用 Introduce Local Extension ( 引入本地扩展 )
3.20 (P86)Data Class ( 幼稚的数据类 )
幼稚的数据类 : 只有数据没有行为的类,其他类需要对该类的数据进行取值设值操作
- 使用 Encapsulate Field ( 封装字段 ) 和 Encapsulate Collection ( 封装集合 ) 对字段进行合理地封装
- 对于不该被其他类修改的字段:Remove Setting Method ( 移除设值函数 )
3.21 (P87)Refused Bequest ( 被拒绝的遗赠 )
如果子类不愿意接受超类的所有定义,应该使用 Replace inherited with Delegation ( 以委托取代继承 ) 来处理子类
3.22 Comments ( 过多的注释 ) 87
使用 Extract Method ( 提炼方法 ) 来解决注释过多问题,注释更多应该说明的是“怎么做”,而不是“做什么”,例如 : 对一个排序函数说明其采用二分法排序,而不是说明它是个排序函数,因为这个说明在函数名称中已经具备。
第 4 章 构筑测试体系
4.1 自测试代码的价值 89
- 确保所有测试都完全自动化,让它们检查自己的测试结果;
- 一套测试就是一个强大的 bug 探测器,能够大大缩减查找 bug 所需要花费的时间。
- 因为代码刚刚写完,测试出现问题后,心里很清楚自己修改或者添加了哪些东西,可能会在哪里出现了问题。
4.2 JUnit 测试框架 91
- 频繁地运行测试;
- 每次编译前都进行一次测试;
- 每天至少执行一次所有的测试。
4.3 添加更多测试 97
- 编写一个测试并运行起来,好过将所有的测试编好了一起运行。
- 测试特别需要注意可能出错的边界条件;
- 对于可能出错的地方,还需要检查是否抛出了预期的异常;
- 测试不能解决所有 bug,但是可以大大减少 bug 的数量。
第 12 章 大型重构 359
大型重构是程序开发必将遇到的,只是不知道在什么时间,用什么样的方式经历。例如 : 随着时间的推移,河道必定会被水草和垃圾所堵塞,你可以固定时间清淤,也可以放任自流直到崩溃。崩溃后依然会面临总结经验教训,再次重构系统。
大型重构很难给出具体的操作案例,因为每个大型案例相对于自身来说都是惟一的,是无法复制和重现的。可以复制与重现的都是这些大型重构中蕴含的具体的细节,因此这章主要讲的是思想和理念上的内容。
四个大型重构 :
- Tease Apart Inheritance ( 362 ) 用于处理混乱的继承体系
- 某个继承体系同时承担两项责任
- 建立两个继承体系,其中一个通过委托调用另一个
- Convert Procedural Design to Objects ( 368 ) 如何重构过时的编码技术遗留下来的程序
- 传统过程化风格的代码
- 将数据记录变成对象,将大块的行为分成小块,再将它们移入到相关对象中
- Separate Domain from Presentation ( 370 ) 将业务逻辑与用户界面分隔开来
- 用户界面类中包含了业务逻辑
- 将业务逻辑剥离到业务类中,参考 : MVC 模式
- Extract Hierarchy ( 375 ) 将复杂的类转化为一群简单的子类,从而简化系统。
- 某个类做了太多工作
- 某个类的部分工作是由大量的条件表达式完成的
- 建立继承体系,使用子类表示每一种特殊情况
第 13 章 重构,复用与现实 379
作为一个博士写的内容,仍然具有学术性较强的风格,可以当作历史资料了解一下重构的发展过程,也可以对重构的思想有更多理论上的认识。
安全重构 ( 391 )
安全重构的四条篱笆 :
- 相信你自己的编码能力;
- 相信你的编译器能捕捉你遗漏的错误;
- 相信你的测试套件能捕捉你和编译器都遗漏的错误;
- 相信代码复审能捕捉你、编译器和测试套件都遗漏的错误。
注 : 没有 100%安全的重构,但是可以通过以上的条件满足你对安全性的最低要求。
重构工具
- Eclipse ( 或其他 IDE ) 自带的重构工具 : Refactor;
- Java ( 或其他编译器 ) 自带的分析工具 : lint;
- JUnit 等自动化的测试工具。
第 14 章 重构工具 401
相对于 10 多年前写的内容,现在许多 IDE 都已经提供了对大部分重构功能的支持。但是了解重构的基本理念,对于正确地使用重构工具会有很大的帮助。因为成功的重构不依赖于工具,而决定于人,当人做出了正确的决定,合理地使用重构工具辅助自己,才能保证重构的完成