重构-改善既有代码的设计 读书笔记

496 阅读13分钟

第二章 重构的原则

重构: 是在不改变软件可观察行为的前提下,对代码的结构进行调整。提高其可理解性,降低其修改成本。

整个重构的过程需要测试,而不需要花时间去调试。

两顶帽子

Kent Beck ““两顶帽子””的比喻

  • 添加新功能
  • 重构

添加新功能是 不修改既有代码

重构时不增加新功能

为什么重构

  • 重构改进软件的设计

由于短期目的的代码修改,可能由于没有完全理解架构的整体设计而破坏代码结构。重构来消除重复代码,让未来的改动更加容易。

  • 重构使软件更容易理解

重构可以让代码更易读。让代码更好地表达自己的意图。

  • 重构帮助找到bug

对代码重构通常需要开发这对代码有深入的了解,往往更容易发现代码的bug。

  • 重构提高编程速度

何时开始重构

  • 预备性重构:在添加新功能之前重构。
  • 帮助理解的重构:修改代码时,遇到难以理解的代码时。
  • 捡垃圾式重构:重构时,遇到与当前重构的代码有关联的”垃圾“代码时。
  • 有计划的重构和见见机行事的重构:添加功能或修复Bug时顺便重构。
  • 代码评审时重构:

什么时候不应该重构 -重构比重写还困难时

重构的挑战

  • 会延缓新功能的开发
  • 分支:多分支的场景,分支隔离的越久,集成的难度越大。建议每天至少一次合并会”主线“。
  • 测试:自测试的代码使得重构成为可能。
  • 遗留代码:遗留的系统多半没有测试,很难安全的重构。作者不建议一鼓作气的重构完复杂而混乱的遗留代码,而是一点点重构。

重构与性能

  • 其他任何情况下“编写快速软件”的秘密就是:先写出可调优的软件,然 后调优它以求获得足够的速度

3种编写快速软件的方法:

  • 时间预算法:用于性能要求极高的实时系统,对组件的资源做严格的分配。
  • 持续关注发:开发人员在开发是设法保持系统高性能。容易让程序变的难以维护,继而减缓开发速度。而且可能伴随对硬件和运行时的错误理解,效果并不理想。
  • 不对性能过分的关注,直到进入性能优化阶段,再遵循特定的流程来优化程序。通常 90%的优化工作都是白费劲。

性能优化的流程

  1. 使用度量工具,监控哪些代码消耗大量的空间和时间。
  2. 对性能热点做集中的重构。
  3. 编译→ 测试 →执行步骤1,如果没有性能提升则撤销修改。知道达到客户满意。

第三章 代码的坏味道

神秘命名(Mysterious Name)

命名无法直观的表达意图,命名是编程最难的两件事之一(命名,缓存失效),好的命名通常可以节省时间。

重复代码(Duplicated Code)

重复的代码让理解和修改代码变得困难

过长函数(Long Function)

函数越长,就越难理解,越难修改。

过长参数列表(Long Parameter List)

长参数令人迷惑,调用困难

全局数据(Global Data)

全局数据的问题在于,从代码库的任何一个角落都可以修改它,而且没有任何机 制可以探测出到底哪段代码做出了修改,又难以定位bug。

重构手法:

  • 使用函数封装全局变量,让修改看的见。
  • 最好将这个函数(及其 封装的数据)搬移到一个类或模块中,只允许模块内的代码使用它,从而尽量控 制其作用域

可变数据(Mutable Data)

对数据的修改经常导致出乎意料的结果和难以发现的bug。

发散式变化(Divergent Change)

一个变化导致多处代码修改,让需求变更之后的修改变的困难。

霰弹式修改(Shotgun Surgery)

遇到变化,需要在代码种进行多处细小的改动,修改时很容易漏掉。

依恋情结(Feature Envy)

一个函数与另外一个模块中的函数或数据交互的格外频繁。

数据泥团(Data Clumps)

类中的某些字段总是成组的出现,这些字段应该有属于自己的类。不要建大而全类。

基本类型偏执(Primitive Obsession)

用基本类型来代替,金额,坐标,重量这些数据。

  • 使用对象包装这些基本类型。

重复的switch (Repeated Switches)

在不同的地方反复使用同样的switch 逻辑,的问题在于:每当你想增加一个选择分支时,必须找到所有 的switch,并逐一更新。

循环语句(Loops)

管道操作可以帮助我们更快地看清被处理的元素以及处理 它们的动作

冗赘的元素(Lazy Element)

过度的封装,将简单到方法名称几乎和实现一样的方法提炼成方法,或将一个简单的方法独立成类。

  • 内联函数 (115)或是内联类(186)

夸夸其谈通用性(Speculative Generality)

设计和增加了一些自认为未来可能用的上的功能,企图以各式各样的钩子和特殊情况来处理一些 非必要的事情,让代码变的复杂。

临时字段(Temporary Field)

其内部某个字段仅为某种特定情况而设。这样的代 码让人不易理解。

  • 使用提炼类将这些为某些特定情况而增加的字段独立出去,同时将相关的函数也搬移出去。

过长的消息链(Message Chains)

消息链指链式调用,例如用户向一个对象请求另一个对象,然后再向后者请求另一个对 象,然后再请求另一个对象。

问题:客户端代码将与查找过程 中的导航结构紧密耦合。一旦对象间的关系发生任何变化,客户端就不得不做出 相应修改。

中间人(Middle Man)

某个类的接口有一半的函数都委 托给其他类就是过度运用委托。

内幕交易(Insider Trading)

对于模块之前不可避免的数据交换都应该放在明面上。

过大的类(Large Class)

大类往往出现过多的字段,重复的代码就不可避免。

类内如果有太多代码,也是代码重复、混乱并最终 走向死亡的源头

  • 通常方法拥有统一的前缀或后缀,就说明他们有机会被提炼到统一的组件中。

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

纯数据类(Data Class)

一个类仅有一些字段和读写这些字段的方法,除此之外没有其他东西。

  • 应该将调用这个函数的取值/设值 搬移到这个函数中。
  • 随意的提供setter方法和给字段标记public 无异,修改行为貌似可控,实际不是。

被拒绝的遗赠(Refused Bequest)

子类不应该继承他们不需要的数据和方法。

  • 使用组合而不是继承

注释(Comments)

如果需要注释来说明一块代码了做什么,或者是需要解释其行为,说明需要用提炼函数或者改名函数声明来重构。

如果你不知道该做什么,这才是注释的良好运用时机。除了用来记述将来的 打算之外,注释还可以用来标记你并无十足把握的区域。你可以在注释里写下自 己“为什么做某某事”。这类信息可以帮助将来的修改者,尤其是那些健忘的家伙。

名称目的
提炼函数(Extract Method)解决重复代码的问题,让代码更加容易理解,小函数更易读
内联函数(Inline Function)取消函数之间不必要的间接的调用
内联变量(Inline Variable)去除一些不必要的临时变量
改变函数声明(Change Function Declaration)让函数名称更易读,简化函数列表
封装变量(Encapsulate Variable)降低重构过程的难度,提高对数据修改和使用行为的可观测性
变量改名(Rename Variable)增加可读性
引入参数对象(Introduce Parameter Object)明确数据项之间的联系。
函数组合成类(Combine Functions into Class)封装一组函数到类中,为一组函数提供共用的环境,简化函数的调用。
函数组合成变换(Combine Functions into Transform)解决中间态(派生) 数据计算逻辑散乱不好维护的问题
封装记录(Encapsulate Record)将记录封装成对象,不过度暴露细节,使数据的访问和更新更加可控
封装集合(Encapsulate Collection)让人更清楚看到数据被修改的时间和方式。
以对象取代基本类型(Replace Primitive with Object)"可以方便对这些数据,增加一些特殊的行为,例如对一个电话号码 进行 脱敏显示 "
以查询取代临时变量(Replace Temp with Query)简化只被赋值一次的变量 。
提炼类(Extract Class)解决大类难以维护的问题,职责不单一的问题。
内联类(Inline Class)如果一个类不再承担足够责任,不再有 单独存在的理由,将这样的类内联到其他类中
隐藏委托关系(Hide Delegate)提升委托类的稳定性,隐藏受托类的细节。
移除中间人(Remove Middle Man)封装对受托对象的访问,解决委托类过大,过于繁琐的问题。
替换算法(Substitute Algorithm)替换掉不容易修改的算法。
搬移函数(Move Function)保持函数放在适合的上下文(类)中
搬移字段(Move Field)保持字段在合适的上下文中
搬移语句到函数(Move Statements into Function)将在某个函数调用之前一定要做的前置操作搬移到函数中,消除重读的代码
搬移语句到调用者(Move Statements to Callers)将函数中影响函数通用性的语句搬移到调用方,定义出合适的边界,提高函数通用性
以函数调用取代内联代码(Replace Inline Code with Function Call)将重复的内联代码分装成函数,增强语句的可读性,消除重复代码
移动语句(Slide Statements)让有关联的代码组队出现,提升代码的可读性
拆分循环(Split Loop)让每个循环只做一件事,提升可读性。
以管道取代循环(Replace Loop with Pipeline)使用管道能够加清晰的描述行为,提升可读性,例如在Java中使用 stream 流操作替换循环。
移除死代码(Remove Dead Code)立即移除无用的代码,保持代码简洁。
拆分变量(Split Variable)对于表达不同含义的变量应该独立声明,而不是复用之前的变量。
字段改名(Rename Field)提高可读性,便于理解
以查询取代派生变量(Replace Derived Variable with Query)每次使用都重新计算派生变量,防止源数据和派生变量变更不一致的问题。
将引用对象改为值对象(Change Reference to Value)值对象通常不可变,也更容易理解。
将值对象改为引用对象(Change Value to Reference)
分解条件表达式(Decompose Conditional)封装复杂的条件判断和各个分支中的操作。更容易理解和便于修改。
合并条件表达式(Consolidate Conditional Expression)将条件不同单行为相同的分支的条件合并,是判断的意图更加清晰
以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)提前结束判断分支,避免过多的嵌套。
以多态取代条件表达式(Replace Conditional with Polymorphism)降低复杂判断逻辑的理解难度,提高可扩展性
引入特例(Introduce Special Case)将对某个数据结构特殊值判断的逻辑收拢到一处。
引入断言(Introduce Assertion)在能确保一定出现某种行为的位置使用断言替换if,方便追踪和定位bug
将查询函数和修改函数分离(Separate Query from Modifier)查询函数应该只做查询操作,将查询中额外的操作分离出来,减少看的见的副作用
函数参数化(Parameterize Function)将逻辑相似,只有参数字面量不同的函数 合并为一个函数,消除重复。
移除标记参数(Remove Flag Argument)移除标记那一部分函数逻辑被执行的参数,保持函数职责单一,提高代码可读性
保持对象完整(Preserve Whole Object)尽可能传递完整的对象,如果需要对象中的几个属性,应该抽取单独的类,防止“数据泥团”
以查询取代参数(Replace Parameter with Query)调用函数传入值,和通过一个函数获取这个值一样容易,那可以使用查询取代值。
以参数取代查询(Replace Query with Parameter)
移除设值函数(Remove Setting Method)使用构造器或者能清晰表达意图的函数来修改属性值,增加可读性。
以工厂函数取代构造函数(Replace Constructor with Factory Function)解决构造函数无法根据环境或参数信息返回子类实例或代理对象的问题。可以将创建对象实现的更加灵活。
以命令取代函数(Replace Function with Command)
以函数取代命令(Replace Command with Function)
函数上移(Pull Up Method)将子类中都存在的方法上移到超类中,避免重复。
字段上移(Pull Up Field)将子类中都存在的字段上移到超类中,同时将关联的方法也上移,避免重复。
构造函数本体上移(Pull Up Constructor Body)将子类中共同的行为的构造器上移到超类中。
函数下移(Push Down Method)将只出现在某个或某几个子类中的函数从超类中移走。
字段下移(Push Down Field)将只出现某个或某几个子类中的属性从超类中移走,放到真正关心它的子类中去,保证超类中的属性,所有的子类都需要。
以子类取代类型码(Replace Type Code with Subclasses)使用子类,替代类型码,可以利用多态更加灵活的定义每种类型的行为
移除子类(Remove Subclass)当子类所支持的变化搬移至别处或者是去除,则去除子类替换为超类中的一个字段。
提炼超类(Extract Superclass)当几个类都有相同的行为时,应该提炼超类,将相同数据使用字段上移,相同的行为使用函数上移
折叠继承体系(Collapse Hierarchy)当一个类和他的超类没有多大差别时,应该将超类和子类合并。合并到哪一方取决于那个名字放在未来更有意义。
以委托取代子类(Replace Subclass with Delegate)继承之间关系紧密,修改超类可能会破坏子类,集成关系不够灵活,使用组合更加灵活
以委托取代超类(Replace Superclass with Delegate)继承之间关系紧密,修改超类可能会破坏子类,集成关系不够灵活,使用组合更加灵活