首先推荐下重构-改善既有代码的设计这本书,中文版翻译还可以,最近读了两遍,将其中的重点提取为读书笔记,关于其中每一项的详细解释和示例请参阅原书。

什么是重构(refactoring) Link to heading

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

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

重构的目的是使软件更容易被理解和修改。你可以在软件内部做很多修改,但必须对软件可观察的外部行为只造成很小变化,或甚至不造成变化。

为什么重构 Link to heading

重构改进软件设计

如果没有重构,程序的设计会逐渐腐败变质。重构很像是在整理代码,其改进设计的一个重要方向就是消除重复代码。

重构使软件更容易理解

除了计算机,我们编写的代码,更多的是给将来的程序员,甚至是我们自己看的。在重构上花一点时间,可以让代码更好地表达自己的意图。重构还可以帮助我们理解不熟悉的代码。

重构帮助找到 bug

对代码进行重构,可以加深对代码和程序结构的理解,有助于找到 bug。

重构提高编程速度

重构可以帮助改进软件设计,而良好的设计是快速开发的根本。

何时重构 Link to heading

重构本来就不是一件应该特别拨出时间做的事情,重构应该随时随地进行。你不应该为重构而重构,你之所以重构,是因为你想做别的什么事,而重构可以帮助你把那些事做好。

事不过三

第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是可以去做;第三次再做类似的事,你就应该重构。

添加新功能时重构

一个动机是重构可以帮助理解需要修改的代码;另一个动机是代码当前的设计无法帮助我轻松添加所需要的功能。

修复错误时重构

收到 bug 报告,就是需要重构的信号,因为代码没有清晰到可以一眼看出 bug 所在。

代码评审(code review)时重构

代码评审对于编写清晰代码很重要,也会让更多人有机会提出更多的建议。

对于今天的工作,我了解得很充分;对于明天的工作,我了解得不够充分。但如果我纯粹只是为今天工作,明天我将完全无法工作。

代码的坏味道 Link to heading

重复代码(Duplicated Code)

如果你在一个以上的地点看到相同的程序结构,那么可以肯定:设法将它们合而为一,程序会变得更好。

过长函数(Long Method)

我们遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。我们可以对一组甚至短短一行代码做这件事。哪怕替换后的函数调用动作比函数自身还长,只要函数名称能够解释其用途,我们也该毫不犹豫地那么做。关键不在于函数的长度,而在于函数“做什么”和“如何做”之间的语义距离。

如何确定该提炼哪一段代码呢? 一个很好的技巧是:寻找注释。它们通常能指出代码用途和实现手法之间的语义距离。如果代码前方有一行注释,就是在提醒你:可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。就算只有一行代码,如果它需要以注释来说明,那也值得将它提炼到独立函数去。

过大的类(Long Class)

如果想利用单个类做太多事情,其内往往就会出现太多实例变量。一旦如此,重复代码也就接踵而至了。

过长参数列(Long Parameter List)

太多参数会造成前后不一致、不易使用,而且一旦你需要更多数据,就不得不修改它。如果将对象传递给函数,大多数修改都将没有必要,因为你很可能只需(在函数内)增加一两条请求,就能得到更多数据。

发散式变化(divergent change)

某个类经常因为不同的原因在不同的方向上发生变化。

霰弹式修改(shortgun surgery)

如果每遇到某种变化,你都必须在许多不同的类内做出许多小修改。

依恋情结(feature envy)

函数对某个类的兴趣高过对自己所处类的兴趣,这种孺慕之情最通常的焦点便是数据。无数次经验里,我们看到某个函数为了计算某个值,从另一个对象那儿调用几乎半打的取值函数。

数据泥团(Data Clumps)

两个类中相同的字段,许多函数签名中相同的参数,这些总是绑在一起出现的数据应该有自己的对象。

基本类型偏执(primitive obsession)

在小任务上运用小对象,比如:结合数值和币种的 money 类、由一个起始值和一个结束值组成的 range 类、电话号码或邮政编码(ZIP)等的特殊字符串。

switch语句

大多数时候,一看到 switch 语句,你就应该考虑以多态来替换它。

冗赘类(Lazy Class)

对于没有必要存在的类,让它消失。

夸夸其谈未来型(speculative generality)

为了未来的可能性处理一些非必要的情况,往往造成系统难以理解和维护。

中间人(Middle Man)

如果某个类接口有一半的函数都委托给其它类,就属于过度委托。

不适当的亲密(Inappropriate Intimacy)

两个类关系过于亲密,花费太多时间去探究彼此的 private 成分,此时建议要么拆算,要么将共同点提取到一个新类。

过多的注释(Comments)

一段代码有长长的注释,这些注释之所以存在往往是因为代码很糟糕。当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余。

主要的重构列表 Link to heading

重新组织函数 Link to heading

提炼函数(Extract Method)

如果有一个过长的函数,或者一段需要注释才能理解的代码,将其中一段代码提取到一个独立函数中,并让函数名称解释该函数的用途。

简短而命名良好的函数更容易被复用,而且高层函数读起来就像一系列注释。

内联函数(Inline Method)

如果一个函数的名称和函数体一样清晰易懂,则去掉函数调用,在调用点直接使用函数体。

另一种场景是,有一群组织不合理的函数,可以先将它们内联到一个大函数中,然后再提炼出组织合理的小函数。

内联临时变量(Inline Tmep)

如果一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其他重构手法。将所有对该变量的引用替换为对应的赋值表达式。

以查询取代临时变量(Replace Temp With Query)

如果一个临时变量保存了一个表达式的运算结果,将这个表达式提取到一个函数中,将对这个临时变量的所有引用替换为对新函数的调用。该新函数也可以被其它函数调用,即提高了复用性。

引入解释型变量(Introduce Explaining Variable)

如果有一个复杂的表达式,将该复杂表达式(或一部分)赋值给一个临时变量,用变量的名称来解释表达式的意图。(用提炼函数(Extract Method)方法也可以,看哪种更合适)

分解临时变量(Split Temporary Variable)

如果一个临时变量被多次赋值,但它既不是循环变量,也不是用于收集计算结果,说明它承担了多个责任,有多个含义,则应该在每次赋值的时候使用单独的临时变量,用良好的命名表达意图。

移除对参数的赋值(Remove Assignments to Parameters)

如果需要在函数内对参数赋值,请使用一个临时变量取代参数。

Replace Method with Method Object(以函数对象取代函数)

你有一个大型函数,其中对局部变量的使用使你无法采用 提取函数(Extract Method)。 将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的字段。然后你可以在同一个对象中将这个大型函数分解为多个小型函数。

替换算法(Substitute Algorithm)

用一个更好的算法直接替换原有的算法。

在对象间搬移特性 Link to heading

搬移函数(Move Method)

如果一个函数与另一个类的交流比所在类更多,应该考虑将该函数搬移到另一个类。如果一个类有太多行为,或一个类与另一个类有太多合作而形成高度耦合,就应该考虑是否可以通过搬移函数进行重构。

搬移字段(Move Field)

如果一个类中的字段,被另一个类更多地用到,应该考虑将该字段搬移到另一个类中。

提炼类(Extract Class)

如果某个类做了两个或多个事情,应该将相应的数据和函数提炼到新的类中。如果类很大,做的事情很多,很多数据和函数总是一起出现或一起变化,就可以考虑提炼新类了。

内联类(Inline Class)

如果某个类没有承担什么责任,不再有单独存在的理由,将这个类的所有特性内联到另一个类中,将原类移除。

隐藏委托关系(Hide Deleage)

通过封装,对外部客户隐藏内部的委托细节,避免内部的委托发生变化波及客户。

如果某个客户先通过服务对象的字段得到另一个对象,然后调用后者的函数,那么客户就必须知晓这一层委托关系。万一委托关系发生变化,客户也得相应变化。你可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,从而去除这种依赖。这么一来,即便将来发生委托关系上的变化,变化也将被限制在服务对象中,不会波及客户。

移除中间人(Remove Middle Man)

如果服务类做了太多的简单委托,移除服务类,让客户直接调用委托类。

该重构手法与*隐藏委托关系(Hide Deleage)*刚好相反,所以很难说什么程度的隐藏和封装是合适的,这是随着系统的变化而变化的,根据具体的场景使用合适的方法即可。

引入外部函数(Introduce Foreign Method)

如果客户类需要的少数几个功能,服务类不能提供,而且不能修改服务类源码,则可以在客户类创建函数提供所需的功能。

引入本地扩展(Introduce Local Extension)

在*引入外部函数(Introduce Foreign Method)*的基础上,如果需要在客户类建立大量的外部函数,则应该考虑将这些函数组织到新的类中,该新类应该是源类的子类,即本地扩展。

重新组织数据 Link to heading

Self Encapsulate Field(自封装字段)

对于类的字段,可以直接访问,也可以通过取值函数/设值函数(get/set)去访问。在一个类中,可以直接访问,如果为了在子类中改变获取数据的方式(如延迟获取)等,则可以通过取值函数/设置函数访问。

Replace Data Value with Object(以对象取代数据值)

如果一个数据项,需要与其它的数据与行为放在一起才有意义,将数据项变成对象。

Replace Array with Object(以对象取代数组)

有一个数组,其中每个元素代表都是不同的东西,建议以对象代替数组,将数组中的每个元素作为对象的字段。

Duplicate Observed Data(复制“被监视数据”)

一个分层良好的系统,应该将处理用户界面和处理业务逻辑的代码分开。之所以这样做,原因有以下几点:(1)你可能需要使用不同的用户界面来表现相同的业务逻辑,如果同时承担两种责任,用户界面会变得过分复杂;(2)与 GUI 隔离之后,领域对象的维护和演化都会更容易,你甚至可以让不同的开发者负责不同部分的开发。

以字面常量取代魔法数(Replace Magic Number with Symbolic Constant)

如果有一个字面数值,带有特殊含义,将其替换为有意义的常量,通过命名表达其含义。

封装字段(Encapsulate Field)

如果有public的字段,将其改为private,并提供相应的访问函数。

Encapsulate Collection(封装集合)

如果一个函数返回一个集合,建议返回该集合的一个只读副本。而且,不要提供对集合的设值(set)函数,应该提供给集合添加/删除元素的函数。

以类取代类型码(Replace Type Code With Class)

如果类中有一个数值类型码,但并不影响类的行为,以一个新的类替换类型码。

以字段取代子类(Replace Subclass with Fields)

如果各个子类的唯一差别是返回常量值的函数上,建议在父类中添加表示该常量值的字段,并通过函数返回,然后移除所有的子类。

简化条件表达式 Link to heading

分解条件表达式(Decompose Conditional)

如果有一个复杂的条件表达式(if-else),将每部分都提炼成单独的函数。

合并条件表达式(Consolidate Conditional Expression)

如果多个条件表达式返回同样的结果,建议试用&或||将它们合并,并将条件判断提取为独立的函数。但是如果这些条件表达式的检查彼此独立,不应该放到一起去检查,则不要试用本项重构。

合并重复的条件片段(Consolidate Duplicate Conditional Fragments)

如果在条件表达式的每个分支上都有相同的代码,则应该将重复代码移到条件表达式之外。

移除控制标记(Remove Control Flag)

控制标记:在一系列布尔表达式中,可能会根据不同的条件给布尔变量赋予不同的值。建议试用break或return语句替换控制标记,提前返回或退出。

以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)

卫语句:单独检查条件表达式的某个分支,如果条件满足,则直接返回。如果条件表达式中,有些分支是特殊情况,建议试用卫语句提前返回。

以多态取代条件表达式(Replace Conditional with Polymorphism)

如果有个条件表达式(如switch语句)根据对象类型的不同执行不同的行为,建议使用多态替换:为每一个不同类型建立一个子类,将分支中的内容放到子类的覆写方法中。

引入Null对象(Introduce Null Object)

如果总是需要检查对象是否为null,可以考虑引入null对象。null对象是正常对象的一个子类,覆写的方法使用空实现,一般是单例,不可变。

引入断言(Introduce Assertion)

如果某一段代码需要对程序状态做出某种假设,以断言明确表明这种假设。断言应该总是为真,如果它失败,表明程序员犯了错误,应该抛出异常。断言的主要作用:一是帮助程序的读者更好地理解代码所做的假设;二是可以在距离bug最近的地方捕获之。不要滥用断言,断言只用于检查一定必须为真的条件,而不是用于检查你认为应该为真的条件。实际上,最后生产环境的代码应该将断言全部都删掉。

简化函数调用 Link to heading

函数改名(Rename Method)

将复杂的处理过程分解成小函数,可以增加程序的可读性,其中的关键是给函数命名。函数名应该准确表达它的用途,可以先考虑如何添加注释,然后将注释转换成函数名称。

添加参数(Add Parameter)

如果函数需要额外的信息,可以考虑给函数添加新的参数。添加新参数之前考虑:现有参数是否无法满足需要?是否可以通过其它函数调用获得需要的数据?

移除参数(Remove Parameter)

如果函数不再需要某个参数,将其移除。

将查询函数和修改函数分离(Separate Query from Modifier)

如果某个函数既返回对象状态,又修改对象状态,建议分离成查询和修改两个独立的函数。任何有返回值的函数,都不应该有看得到的副作用。

令函数携带参数(Parameterize Method)

如果两个函数做着类似的工作,但因少数几个值导致行为略有不同,可以考虑合并为一个函数,通过参数处理变化的部分,这样可以去除重复的代码,提高灵活性。

以明确函数取代参数(Replace Parameter with Explicit Methods)

如果一个函数,根据参数的值不同采取不同的行为,建议针对每一个参数值,建立独立的函数,调用方可以直接调用对应的函数,就可以避免条件表达式。注意与令函数携带参数(Parameterize Method)区分开来。

保持对象完整(Preserve Whole Object)

如果将一个对象的若干数据作为参数传给一个函数,可以考虑直接将该对象作为参数传递。需要注意:1. 如果传递对象会导致依赖关系恶化,则不要使用本项重构。2. 如果调用函数使用了对象的很多项数据,需要考虑该函数是否应该被移到对象中去。

以函数取代参数(Replace Parameter with Methods)

如果函数可以通过其它途径(如调用其它的函数)获得参数值,就应该去掉参数值,缩短参数列的长度。

引入参数对象(Introduce Parameter Object)

如果特定的一组参数总是同时出现在不同的函数参数列表,建议使用一个对象将这些数据组织到一起,可以缩短参数列的长度,提高了代码的一致性。

移除设值函数(Remove Setting Method)

如果类中的某个字段在对象创建后不应该改变,去掉该字段的设值函数。

隐藏函数(Hide Method)

如果某个函数没有被外部类使用到,将该函数设置为private

以工厂函数取代构造函数(Replace Constructor with Factory Method)

如果在创建对象时还需要执行一些额外的操作,建议将构造函数替换为工厂函数。

封装向下转型(Encapsulate Downcast)

如果函数的返回值需要调用者进行向下转型(downcast),建议在该函数内执行向下转型,返回调用者需要的类型。

以异常取代错误码(Replace Error Code with Exception)

如果某个函数返回特定的错误码表示某种异常情况,建议直接抛出异常。

以测试取代异常(Replace Exception with Test)

面对调用者可以预先检查的条件,调用者应该先检查该条件,不要通过捕获异常去处理可以预见的逻辑。不要滥用异常,异常应该只用于异常的、罕见的行为。

处理概括关系 Link to heading

字段上移(Pull Up Field)

两个子类有相同的字段,将该字段移到超类中去。

函数上移(Pull Up Method)

有些函数,在各个子类中产生完全相同的结果,将该函数移到超类。

构造函数本体上移(Pull Up Constructor Body)

如果各个子类中都有一些构造函数,它们的函数体几乎完全一致,应该在超类中新建一个构造函数,并在各个子类的构造函数中调用它。

函数下移(Push Down Method)

如果超类中的某个函数只是被部分子类用到,将这个函数移到需要它的子类中去。

字段下移(Push Down Field)

如果超类中的某个字段只是被部分子类用到,将这个字段移到需要它的子类中去。

提炼子类(Extract Subclass)

如果类的某些特性只被某些(不是全部)实例用到,新建一个子类,将特定的属性移到子类中去。

提炼超类(Extract Superclass)

如果两个类有相似特性,建立一个超类,将相同的特性移到超类。

折叠继承体系(Collapse Hierarchy)

如果子类和超类并无太大区别,将它们合并。

塑造模板函数(Form Template Method)

如果有一些子类,其中的某个函数以相同顺序执行大致相近的操作,但是各操作不完全相同。将这些操作分别放进独立函数中,并保持它们都有相同的签名,然后将原函数上移至超类,子类重写实现不同的逻辑。

以委托取代继承(Replace Inheritance with Delegation)

如果子类只使用了超类接口中的一部分,或者子类从超类继承了一大堆并不需要的数据,建议将继承改为委托。

以继承取代委托(Replace Delegation with Inheritance)

如果某个类使用了委托类中的所有函数,需要编写所有简单的委托函数,建议将委托改为继承。