第14章 重构工具401 《重构》的读书笔记
学习基础:
熟悉《设计模式》的基本概念,熟悉基本的Java语法,熟悉Eclipse和JUnit的使用,有相对较好的英语基础。
学习过程:
- 先看第1章,手工输入实例程序,了解重构的方法和过程。重点是理解重构的思路,最好的理解方式就是通过实践的方式理解。
- 再看第2~4章,内容为选择性阅读,没兴趣或者看不懂的都可以跳过,因为后面还可以回头再读。
- 接着第5~12章,最好按顺序把代码一个个输入,再按照作者的步骤重构操作一次,并结合自己以往工作中的实践来理解。
学习目的:
使自己编写的代码更容易被人读懂。
学习感悟:
- 代码的重构应该是一步步完成的,每次重构的部分不要超过自己的理解能力的5%。虽然这样操作略显繁琐,但是可以减轻头脑重构过程中的记忆强度,减少代码出错的机会。
- 代码的重构一定要配合JUnit(TDD,测试驱动开发)完成,再加上Git(版本管理)和Eclipse(IDE的重构工具)那就事半功倍了。
学习代码:
Refactored-MartinFowler
总览
- 第1章(必读),从实例程序出发,了解重构的方法和过程。
- 第2章,讨论重构的一般性原则、定义和进行重构的原因。
- 第3章,介绍如何判断问题代码,以及如何用重构改善它们。
- 第4章,在代码中构建java的测试环境
- 第5~12章,具体面对的问题和重构的方法。
- 第13章,Bill Opdyke在商业开发中应用重构
- 第14章,自动化重构工具(今天看来,已经不是太大问题,Eclipse的Refactor已经非常好用)
- 第15章,重构的哲学思想
第1章 重构,第一个案例
1.1 (P1)起点
因为代码的结构无法满足添加新功能的需要,因此先进行重构,使代码方便添加新功能,然后再添加新功能。
1.2 (P7)重构的第一步
首先确认自己拥有一套可靠的测试机制,因为重构有可能引入问题,通过测试保证重构没有改变程序功能。
第2章 重构原则
2.1 (P53)何谓重构
重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高理解性和降低修改成本。
重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
定义的扩展:
- 重构让软件更容易理解和修改
- 重构不会改变软件的可观察行为,即使改变也只能是微小的影响,软件功能一如既往。
重构的目标:只改变程序内部结构,不添加新的功能
不要一心二用:
- 添加新功能的时候,不重构已有代码。
- 重构代码时,不增加新功能。
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(以委托取代继承)来处理子类
使用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都已经提供了对大部分重构功能的支持。但是了解重构的基本理念,对于正确地使用重构工具会有很大的帮助。因为成功的重构不依赖于工具,而决定于人,当人做出了正确的决定,合理地使用重构工具辅助自己,才能保证重构的完成。