《重构》的读书笔记

  • 总览
  • 第1章 重构,第一个案例
  • 第2章 重构原则
  • 第3章 代码的坏味道
  • 第4章 构筑测试体系
  • 第12章 大型重构359
  • 第13章 重构,复用与现实379
  • 第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(以委托取代继承)来处理子类

      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都已经提供了对大部分重构功能的支持。但是了解重构的基本理念,对于正确地使用重构工具会有很大的帮助。因为成功的重构不依赖于工具,而决定于人,当人做出了正确的决定,合理地使用重构工具辅助自己,才能保证重构的完成。

    • posted @ 2019-01-16 17:48 zYx.Tom 阅读(...) 评论(...) 编辑 收藏