本文分类:
软件理论与软件工程

本文是《重构——改善既有代码的设计》一书的读书笔记。

重构概述

重构的概念(What)

英文:Refactoring

名词:对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低修改成本。

动词:使用一系列重构方法,在不改变软件可观察行为的前提下,调整其结构。

为什么要重构(Why)

何时重构(When)

  1. 重构时机

     - 随时随地
     - 三次法则:第三次做类似的事情,就应该重构了
     - 添加功能时
     - 修复bug时
     - 复审代码,即Code Review时
    
  2. 不该重构的情况

     - 代码是在太混乱了,设计完全错误
     - 如果项目已近最后期限,应该避免重构
     - 重构还不如重新编码
    

如何重构(How)

  1. 读懂代码(包括测试代码)
  2. 进行重构
  3. 运行所有的单元测试

重构与设计

重构与设计是互补的,程序应该先设计,但在开始编码后,设计上的不足可以用重构来弥补。

设计应该是适度的设计,而不必过度设计。如果能很容易的通过重构来适应需求的变化,那么就不必过度的设计,当需求改变时再重构代码。

重构要点

  1. 如果你发现自己需要为程序添加一个功能,而代码的结构使你无法很方便地达到目的,那就先重构那个程序,使功能的添加比较容易进行,然后再添加那个功能。

  2. 重构前,先检查自己是否有一个可以依靠的测试机制。这些测试必须有自我检验的能力。

  3. 重构技术就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。

  4. 任何一个傻瓜都能写出计算机可以理解的代码。唯有写出让人能够容易理解的代码才是真正优秀的程序员。

  5. 事不过三,三则重构。

  6. 不要过早发布接口。谨慎修改你代码的各种规范,使重构更顺畅。

  7. 当你感觉需要撰写注释时,先尝试重构,试着让注释显得多余。

  8. 确保所有测试都完全自动化,让它们检查自己的测试结果。

  9. 一套测试就是一个强大的bug检测器,能够大大缩减查找bug所需要的时间。

  10. 频繁地进行测试。每次编译请把测试也考虑进去——每天至少执行每个测试一次。

  11. 每当你收到bug报告,请先写一个单元测试来暴露这这个bug。

  12. 编写未臻完善的测试并实际运行,好过对完美测试的无尽等待。

  13. 考虑可能出错的边界条件,把测试火力集中在那儿。

  14. 当事情被大家认为应该会出错时,别忘了检查是否抛出了预期的异常。

  15. 不要因为测试无法捕获所有bug不就写测试,因为测试的确可以捕捉到到大多数bug。

代码的坏味道

  1. Duplicated Code:重复代码

    指不同的地方出现相同的程序结构。

    重构方法:Extract Method、Extract Class、Pull Up Method、Form Template Method

  2. Long Method:过长函数

    重构方法:Extract Method、Replace Temp with Query、Replace Method with Method Object、Decompose Conditional

  3. Large Class:过大的类

    重构方法:Extract Class、Extract Subclass、Extract Interface、Replace Data Value with Object

  4. Long Parameter List:过长参数列表

    重构方法:Replace Parameter with Method、Introduce Parameter Object、Preserve Whole Object

  5. Divergent Change:发散式变化

    一个类经常由于不同的原因向不同的方向变化,实际上是违法了单一职责原则。

    重构方法:Extract Class

  6. Shotgun Surgery:霰弹式修改

    一种变化会导致好几个类的修改。

    重构方法:Move Method、Move Field、Inline Class

  7. Feature Envy:依恋情结

    类中的方法对另一个类的访问超过了对所在类的访问。

    重构方法:Move Method、Move Field、Extract Method

  8. Data Clumps:数据泥团

    指喜欢经常成群出现的多个数据项,如两个类中相同的字段,或者许多方法中相同的参数签名。

    重构方法:Extract Class、Introduce Parameter Object、Preserve Whole Object

  9. Primitive Obsession:基本类型偏执

    热衷于使用int、long、String等基本类型而不是有意义的小对象。

    重构方法:Replace Parameter with Method、Extract Class、Introduce Parameter Object、Replace Array with Object、Replace Type Code with Class、Replace Type Code with Subclass、Replace Type Code with State/Strategy

  10. Switch Statement:Switch语句

    相同的Switch语句散落在各处,需要添加一个case时需要修改多个地方。

    重构方法:Replace Conditional With Polymorphic、Replace Type Code with Subclass、Replace Type Code with State/Strategy、Replace Parameter with Explicit Methods、Introduce Null Object

  11. Parallel Inheritance Hierarchies:平行继承体系

    如果一个类增加一个子类,另一个类也必须相应的增加一个子类。

    重构方法:Move Method、Move Field

  12. Lazy Class:冗余类

    无用的类

    重构方法:Inline Class、Collapse Hierarchy

  13. Speculative Generality:夸夸其谈未来性能

    指无用的抽象类,无用的预留参数等当前没有用到的部件。这个往往是过度设计的结果。

    重构方法:Collapse Hierarchy、Inline Class、Remove Parameter、Rename Method

  14. Temporary Field:令人迷惑的临时字段

    指仅在特定环境下使用的实例变量,比如为了方便某个方法调用而在对象中添加的字段,该字段仅在这个方法中有用。

    重构方法:Extract Class、Introduce Null Object

  15. Message Chains:过度耦合的消息链

    指客户向一个对象索取另一个对象,然后在向后者索求另一个对象,然后在索求另一个对象——客户与查找过程的紧密耦合。

    重构方法:Hide Delegate

  16. Middle Man:中间人

    过度委托,一个类中的大部分方法都委托给其他类。

    重构方法:Remove Middle Man、Inline Method、Replace Delegation with Inheritance

  17. Inappropriate Intimacy:狎昵关系

    两个类大量探究彼此的private部分。

    重构方法:Move Method、Move Field、Change Bindirectional Association to Unidirectional、Replace Inheritance with Delegation、Hide Delegate

  18. Alternative Classes with Different Interface:异曲同工的类

    类名不同但功能相似。

    重构方法:Rename Method、Move Method

  19. Incomplete Library Class:不完善的类库

    指现有的功能不能满足要求。

    重构方法:Introduce Foreign Method、Introduce Local Extension

  20. Data Class:纯稚的数据类

    指只有公共成员变量,或只有字段和get/set方法的类。

    重构方法:Move Method、Encapsulate Field、Encapsulate Collection

  21. Refused Bequest:被拒绝的遗赠

    指子类只使用了父类的部分方法。

    重构方法:Replace Inheritance with Delegation

  22. Comments:过多的注释

    过多的垃圾注释,或者代码太复杂,必须使用注释说明。

    重构方法:Extract Method、Introduce Assertion

重新组织函数

  1. Extract Method:抽取方法

    将一段独立的代码抽取到一个新的方法,并以它“做什么”来命名。

    • 无局部变量:直接抽取;
    • 局部变量在抽取的方法中只读:以参数传递;
    • 局部变量在抽取的方法中被赋值:
      • 如果变量只在抽取的代码中使用:将变量的声明也抽取到方法中
      • 抽取的代码之外也使用了这个变量:
        • 抽取的方法之后没有使用:在抽取的方法中直接修改
        • 抽取的方法之后有使用:返回值返回
          • 如果需要返回的代码超过一个,则暂不抽取
  2. Inline Method:内联方法

    如果一个方法的实现很简单,并且实现本身和方法的名称一样通俗易懂,则直接在方法的调用点用方法的实现代替方法调用。如果方法被子类覆写,或者涉及递归等复杂的情况,则暂不重构。

  3. Inline Temp:内联临时变量

    如果一个临时变量只被一个简单的表达式赋值,而且这个临时变量妨碍了其他重构手法,则将对这个临时变量的引用替换为这个赋值表达式。使用该方法要确保赋值表达式没有副作用,而且保证变量后面没有被赋值(可以通过将变量声明为final测试)。

  4. Replace Temp With Query:查询取代临时变量

    如果一个临时变量保存某一个(组)表达式的计算结果,则将表达式提炼为一个方法,这个新方法也可以被其他方法调用。

  5. Introduce Explaining Variable:引入解释性变量

    将复杂表达式(或其中一部分的)结果保存到一个临时变量,并以它的用途命名该临时变量,提高可读性。

  6. Split Temporary Variable:分解临时变量

    如果一个临时变量被赋值超过一次,而且它不是循环变量,也不用于收集计算结果,则针对每次赋值创造一个新的临时变量,提高可读性,避免混淆。

  7. Remove Assignments to Parameters:移除对参数的赋值

    不要对方法传递进来的参数赋值,尤其是混用值传递和引用传递的时候,会降低代码清晰度,应该建一个临时变量,并将参数传递给它,如果确实需要修改传递进来的值,请使用返回值。

  8. Replace Method with Method Object:以方法对象取代方法

    如果代码使用了很多临时变量,无法进行方法抽取,则这个方法变成一个新的类,那么方法的临时变量就会变成新类的实例域,然后就可以在该类中将这个大方法分解成多个小方法,而旧的方法调用者改成新建一个类对象,并调用相应的方法。

  9. Substitute Algorithm:替换算法

    如果两个算法做从相同的事情,则保留清晰的那个算法。

在对象之间迁移特性

  1. Move Method:迁移方法

    如果一个类中某个方法跟另一个类的交互更多,则这个方法可能放错了地方,考虑迁移到那个类。

  2. Move Field:迁移字段

    如果一个类中的某个字段更多的被另一个类使用,则这个字段可能放错了地方,考虑迁移到那个类。

  3. Extract Class:抽取类

    某个类做了两个类应该做的事情,比如某些字段经常一起变化,某些字段和某些方法总是一起出现,则考虑将它们提取出来,形成一个新的类。

  4. Inline Class:内联类

    如果一个类没有做太多的事情,则将考虑将其特性迁移到另一个类,并将其删除。

  5. Hide Delegate:隐藏委托

    如果一个类A经常需要通过其委托类B来调用另一个类C,则在委托类B中建立委托方法,类A通过调用B的委托方法访问类C。

  6. Remove Middle Man:移除中间人

    如果一个类做了太多简单的委托操作,则删除这个类,让客户端直接调用受托类。

  7. Introduce Foreign Method:引入外加函数

    当需要为类新加一个方法,但无法修改该类时,可以新建一个方法,并将该类的对象作为参数传递进去,在这个方法中完成新的功能。

  8. Introduce Local Extension:引入本地扩展

    当需要为类新加一个方法,但无法修改该类时,可以让一个新类继承这个类,或者成为这个类的包装类,并在这个新类中实现新的功能。

简化方法调用

  1. Rename Method:重命名方法

    如果方法名称未能正确表明方法的用途,则重命名方法

  2. Add Parameter:添加参数

    如果方法需要从调用端得到更多的信息,则添加参数

  3. Remove Parameter:删除参数

    如果方法不再需要某个参数,则删除它

  4. Separate Query From Modify:分离查询和修改的方法

    如果一个方法中既查询了一个对象,也修改了一个对象,则建立两个方法,一个负责查询,一个负责修改。

  5. Parameterize Method:让方法携带参数

    如果几个方法做了类似的操作,只是数据不同,则将这几个方法合并为一个方法,并用参数表达那些不同的值。

  6. Replace Parameter with Explicit Method:用明确的方法替换参数

    如果有一个方法,完全根据不同的参数值采用了不同的行为,则将这个方法拆分,为参数的每一个可能的值建立一个方法。

  7. Preserve Whole Object:保持对象完整

    如果有个方法的参数全部来自于一个对象,则改为使用这个对象作为参数。

  8. Replace Parameter with Method:已放到取代参数

    对象调用某个方法,并将其作为另一个方法的参数,如果另一个方法也能调用这个方法,则去除参数,直接让其调用这个方法。

  9. Introduce Parameter Object:引入参数对象

    如果某些参数总是一起出现,则使用一个类对象来代替这些参数,可以减少参数数量。

  10. Remove Setting Method:删除设值方法

    如果类中的某字段应该在创建时设置,此后不再变化,则删除set方法。

  11. Hide Method:隐藏方法

    如果一个方法从来没有被其他方法使用,则改为private。

  12. Replace Constructor with Factory Method:使用工厂方法取代构造器

    如果创建对象不是仅仅做简单的对象构建,如根据不同的参数进行不同的构建,或者需要控制对象的实例个数,则使用工厂方法取代构造器

  13. Encapsulate Downcast:封装向下转型

    如果某个方法返回的对象需要调用者执行向下转型,则将转型动作移动到方法中。

  14. Replace Error Code with Exception:使用异常代替错误码

    对于返回特定的值(如-1)表示错误的方法,考虑改为抛出异常,调用者不一定会检查返回值。

  15. Replace Exception with Test:以测试取代异常

    对于一个可以预先检查的条件,不要抛出异常,而应该用if先对其进行检查。

简化条件表达式

  1. Decompose Conditional:分解条件表达式

    如果有一个复杂的if-then-else语句,则将if、then、else语句提取为单独的方法。

  2. Consolidate Conditional Express:合并条件表达式

    如果有一系列的条件执行相同的逻辑,则将这些条件提炼为一个返回Boolean的独立方法,然后在一个条件测试语句中调用这个方法。

  3. Consolidate Duplicate Conditional Fragment:合并重复的条件片段

    如果在条件表达式的每个分支上都有一段相同的代码,则将这段代码提取到条件表达式之外

  4. Remove Control Flag:移除控制标记

    在一系列Boolean表达式中,如果某个变量带有控制标记的作用,则移除这个变量,使用break或者return语句代替。

  5. Replace Nested Conditional with Guard Clause:以卫语句取代嵌套条件表达式

    条件表达式通常有两种表现形式,第一种形式是:所有分支都属于正常行为,第二种形式则是条件表达式提供的答案中只有一种是正常行为,其他都是不正常见的情况。如果两条分支都是正常行为,就应该使用if else。如果某个分支极其罕见,就应该单独检查该条件,并在该条件为真时从函数返回,这样的检查就是传说中的“卫语句”。

  6. Replace Conditional with Polymorphic:以多态取代条件表达式

    如果表达式根据不同对象类型选择不同的行为,则将这个表达式的每个分支放到一个子类内的覆写方法中,然后将原始的方法声明为抽象方法。

  7. Introduce Null Object:引入Null对象

    需要再三检查某物是否为null值,则考虑将null值替换为null对象(无效对象)。

  8. Introduce Assertion:引入断言

    如果某一段代码需要对程序状态做出某种假设,则以断言明确表现这种假设。

处理泛化关系

  1. Pull Up Field:字段上移

    如果子类有相同的字段,则迁移到父类

  2. Pull Up Method:方法上移

    如果子类有功能相同的方法,则迁移到父类

  3. Pull Up Constructor Body:构造方法体上移

    将子类构造方法体中相同的部分提取到父类的构造方法,子类使用super调用这个构造方法。

  4. Push Down Field:字段下移

    将父类中只与部分子类相关的字段迁移到这些子类。

  5. Push Down Method:方法下移

    将父类中只与部分子类相关的方法迁移到这些子类。

  6. Extract Subclass:提取子类

    将类中只被某些实例用到的特性迁移到一个新的子类中。

  7. Extract Superclass:提炼超类

    如果两个类有相似的特性,则新建一个类作为这两个类的父类,并将那些相似的特性移动到这个新建的父类。

  8. Extract Interface:提取接口

    如果若干客户使用类接口中的同一子集,或者两个类有相同的部分,则将相同的子集提取到一个接口中。

  9. Collapse Hierarchy:合并继承体系

    如果子类和超类没有太大的差别,则合并到一个继承层次中。

  10. Form Template Method:塑造模板方法

    类似模板方法模式,如果子类中以相同的顺序执行某些类似的方法,则将操作顺序提取到一个方法,并放到父类,父类调用这些不同的操作,子类通过覆写这些方法实现差异。

  11. Replace Inheritance with Delegation:以委托取代继承

    就是使用组合代替继承。如果子类只使用超类接口的一部分,或者根本不需要继承而来的数据,则删除子类对父类的继承,而是在子类中新建一个超类对象。

  12. Replace Delegate with Inheritance:以继承取代委托

    就是用继承代替组合。如果经常需要为两个组合类编写极其简单的委托方法,也就是说一个类的行为基本上都委托给了另一个类,则直接让这个类继承另一个类更简便。

重新组织数据

  1. Self Encapsulate Field:自封装字段

    为字段增加get/set方法,好处是可以增加校验,或者延迟初始化,而且子类通过覆写方法改变行为,但本类中,尤其是构造函数中是直接访问字段,还是使用get/set方法,是值得讨论的。

  2. Replace Data Value with Object:以对象取代数据值

    如果一个数据必须和其他数据和方法一起使用才有意义,则将该数据项改为类对象。

  3. Change Value to Reference:将值改为对象引用

  4. Change Reference to Value:将引用对象改为值对象

    可以将对象分成两类:reference object(引用对象)和value object(值对象)。值对象有一个非常重要的特征:它们应该是不可变的。要在引用对象和值对象之间做选择有时并不容易,如果希望给一个对象加入一些可修改数据,并确保对任何一个对象的修改都能影响到所引用此对象,则需要使用引用对象。而在分布式和并发系统中,不可变的值对象则更有用,因为无需考虑它们的同步问题。

  5. Replace Array with Object:以对象取代数组

    如果一个数组的元素各自代表不同的东西,则以对象替换数组,对于数组中的每个元素,以一个字段来表示。

  6. Duplicate Observed data:复制被监视数据

    指的是基于MVC模式重构,抽取原来被内嵌在用户界面中的业务逻辑处理,使用户界面和处理业务逻辑的代码分开,然后使用观察者模式实现数据和用户界面的同步。

  7. Change Unidirection Association to Bidirectional:将单向关联改为双向关联

    两个类都需要使用对方特性,但其间只有一条单向连接。添加一个反向指针,并使修改函数能够同时更新2条连接。

  8. Change Bidirectional Association to Unidirection:将双向关联改为单向关联

    两个类之间有双向关联,但其中一个类如今不再需要另一个类的特性,则去除不必要的关联。

  9. Replace Magic Number with Symbolic Constant:字面常量取代魔法数

    如果一个字面数值有特别含义,则创建一个常量,根据其意义命名,并将上述的字面数值替换为这个常量。

  10. Encapsulate Field:封装字段

    将类中存在的public字段改为为private,并且提供相应的访问函数。

  11. Encapsulate Collection:封装集合

    如果一个方法返回一个集合,则让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合元素的函数。

  12. Replace Record with Data Class:以数据类取代记录

    你需要面对传统编程环境中的记录结构,比如一个遗留程序,或者是需要通过一个传统API来与记录结构交流,或是处理从数据库读出的记录,则为该记录创建一个类似外部记录的类,以便日后将某些字段和函数搬移到这个类中。

  13. Replace Type Code with Class:以类来取代类型码

  14. Replace Type Code with Subclass:以子类来取代类型码

  15. Replace Type Code with State/Strategy:以状态/策略模式取代类型码

    如果类中有一个数值类型码,但它并不影响类的行为,则以一个新的类替换该数值类型码(感觉用枚举类也不错)。如果一个类型会影响行为,则可以借助多态,以子类取代类型码来处理变化的行为。但如果类型的值在对象的生命周期内会发生变化,或者由于其他原因使得无法用子类,则可以使用状态模式和策略模式来重构。

  16. Replace Subclass with Fields:以字段取代子类

    如果各个子类的唯一差别只在“返回常量数据”的方法上,则在父类中新增一个字段,修改这些方法,使它们返回这个字段并删除子类,这个新增字段可以在对象构造时赋值。

大型重构

  1. Tease Apart Inheritance:梳理并分解继承体系

    如果某个继承体系同时承担了两项责任,则建立两个继承体系,并通过委托关系让一个可以调用另一个。

  2. Convert Procedural Design to Objects:将过程化设计转化为对象设计

    将数据记录转化为对象,将大块的行为分解为小块,并移到相关的对象中。

  3. Separate Domain from Presentation:分离领域和展示

    实际指按照MVC模式重构程序

  4. Extract Hierarchy:提取继承层次

    如果某个类做了大量的工作,并且一部分工作是以大量的条件表达式完成的,则建立继承体系,以一个子类表示一种特殊情况。

本文来自 [时光记 - 王智超的个人空间](www.hiwzc.com),转载请注明出处。