1 代码的坏味道
重复代码(Duplicated Code)
同一个类的两个函数含有相同的表达式,两个互为兄弟的子类内含有相同的表达式,两个毫不相关的类中出现重复代码。过长函数(Long Function)
把需要用注释说明的内容写进独立的函数中,并以其用途命名。过大的类(Large Class)
单个类做太多事情,出现太多实例变量及代码。过长参数列(Long Parameter List)
尽量通过对象传递参数。发散式变化(Divergent Change)
一个类受多种变化的影响。霰弹式修改(Shotgun Surgery)
一种变化引发多个类相应修改,需要将变化提炼出来。数据泥团(Data Clumps)
相同的若干项数据出现在不同地方,这些绑在一起出现的数据应提炼到独立的对象。纯数据类(Data Class)
类仅拥有字段以及用于访问这些字段的函数,可考虑搬迁访问字段函数周围的代码。当纯数据类不可修改且仅用于传递信息例外。基本类型偏执(Private Obsession)
如果有一组总是被放在一起的基本类型数据,可以尝试将这组数据放到一个单独类中变成结构类型数据。依恋情结(Feature Envy)
充血模式函数对某个类的兴趣高过自己所处类的兴趣。被拒绝的遗赠(Refused Bequest)
子类不想继承超类所有的函数和数据,可考虑提炼继承体系。异曲同工的类(Alternative Classes with Different Interfaces)
不同名字的类或函数做相同的事。令人迷惑的暂时字段(Temporary Field)
类中某个字段只为某些特殊情况而设置。冗赘的元素(Lazy Element)
消除不值得存在的元素。夸夸其谈的未来性(Speculative Generality)
预留无用的抽象类,无用的抽象参数。中间人(Middle Man)
类中拥有过多的中间层委托。内幕交易(Insider Trading)
一个模块过于关注另一个模块的字段。过多的注释(Comments)
当需要撰写注释时,先尝试重构,试着让所有注释都变得多余。如果不知道该做什么的,才是注释的良好运用时机。
2 重构手法
重新组织函数与数据,简化条件表达式与函数调用,在对象之间搬移特性,处理概括关系。
2.1 重新组织函数
- 提炼函数(Extract Method):提炼代码并通过函数名称解释该函数的用途。
- 内联函数(Inline Method):函数的内容清晰易懂,在函数调用点插入函数本体并移除该函数。
- 内联变量(Inline Variable):临时变量只被赋值一次,替换临时变量为赋值的表达式本身。
- 引入解释性变量(Introduce Explaining Variable):将复杂表达式的结果放进一个或多个临时变量,以此变量名称来解释表达式用途。
- 以查询取代临时变量(Replace Temp with Query):临时变量保存表达式运算结果,可将表达式提炼到新函数中,替换临时变量为对新函数的调用。
- 分解临时变量(Split Temporary Variable):临时变量不应被多次赋值,可针对每次赋值创建独立的临时变量。
- 移除对参数的赋值(Remove Assignments Parameters):当对参数进行赋值时,可引入临时变量替代。
- 以函数对象取代函数(Replace Method with Method Object):将大型函数放进独立对象中,再分解为多个小型函数。
- 替换算法(Substitute Algorithm):提炼函数算法,使其更加清晰。
2.2 重新组织数据
- 封装字段(Encapsulate Field):类中只以字段取值或设值函数来访问字段而非直接访问字段,类中public字段声明为private并提供相应的访问函数。
- 移除设值函数(Remove Setting Method):当字段在创建后不希望被修改,应移除设值函数。
- 封装集合(Encapsulate Collection):当函数返回一个集合时,让函数返回该集合的只读副本,并在类中提供添加、移除集合元素的函数。
- 以对象取代数据值(Replace Data Value with Object):当数据项需要与其他数据和行为一起使用才有意义时,将数据项变成对象。
- 以对象取代数组(Replace Array with Object):当数组中的元素各自代表不同东西,用拥有明确含义的字段对象替换该数组。
- 将单向关联改为双向关联(Change Unidirectional Association to Bidirectional):两个类都需要使用对方特性,但其间只有一条单向链接时,添加反向指针,并使修改函数能够同时更新两条链接。
- 将双向关联改为单向关联(Change Bidirectional Association to Unidirectional):两个类之间有双向关联,但其中一个类不再需要另一个类的特性时,去除不必要的关联。
- 以字面常量取代魔法数(Replace Magic Number with Symbolic Constant)。
- 以类取代类型码(Replace Type Code with Class):使用类或枚举替换数值类型码。
- 以子类取代类型码(Replace Type Code with Subclass):对影响类行为的不可变类型码,使用子类取代。
- 以State/Strategy取代类型码(Replace Type Code with State/Strategy):对影响类行为的不可变类型码,当无法通过继承消除,引入状态对象与继承取代类型码。
- 以字段取代子类(Replace Subclass with Fields):当子类间的差别是返回常量数据的函数,应将常量上移到超类并消除子类。
- 将引用对象改为值对象(Change Reference to Value):当多个类引用相同对象,且该对象很小、字段不可变且不易管理,将它变成值对象。
2.3 简化条件表达式
- 分解条件表达式(Decompose Conditional):将复杂的条件表达式提炼到独立函数中。
- 合并条件表达式(Consolidate Conditional Expression):多个条件得到相同结果,使用与非逻辑将多个条件进行合并。
- 合并重复的条件片段(Consolidate Duplicate Conditional Fragments):每个条件分支上有相同的代码,将其搬移到条件表达式之外。
- 移除控制标记(Remove Control Flag):以break或return取代控制标记。
- 以卫语句取代嵌套条件表达式(Replace nested Conditional with Guard Clauses):拆分或反转条件,引入适当卫语句(return)以减少条件的嵌套层数。
- 以多态取代条件表达式(Replace Conditional with Polymorphism):当条件表达式根据对象类型选择不同的行为,将表达式的每个分支放进子类的覆写函数中,再将原始函数声明为抽象函数。
- 引入断言(Introduce Assertion):代码对某个条件做出假设,以断言明确表达这种假设,注意不可滥用。
2.4 简化函数调用
- 修改函数声明(Change Function Declaration):修改未能表达函数用途的名称,去除没有被使用的参数。
- 引入参数对象(Introduce Parameter Object):当多个参数总是很自然地同时出现,以一个对象取代这些参数。
- 保持对象完整(Preserve Whole Object):当调用者从某个对象中取出若干值作为函数调用的参数,可改为传递整个对象。
- 以查询取代参数(Replace Parameter with Query):若函数可通过其他途径获得参数值,就不应该通过参数取得该值。
- 以参数取代查询(Replace Query with Parameter):若函数通过其他途径获得参数值成本较高,可考虑通过参数取得该值。
- 令函数携带参数(Parameterize Method):若干函数本体包含了不同的值却做了类似的工作,可提取出公共函数,以参数表达不同的值。
- 隐藏函数(Hide Method):通过修改可见范围,尽可能降低所有函数可见度。
- 将查询函数和修改函数分离(Separate Query from Modifier):函数既返回对象状态值,又修改对象状态,将查询跟修改拆分出来。
- 以明确函数取代参数(Replace Parameter with Explicit Methods):函数根据参数值采取不同行为,可根据参数值拆分到独立函数。
- 以工厂函数取代构造函数(Replace Constructor with Factory Method):存在复杂构建或根据类型来创建不同对象,可将构建函数替换为工厂函数。
- 封装向下转型(Encapsulate Downcast):函数调用者需要对函数返回对象执行向下转型,将向下转型动作移到函数中。
- 以异常取代错误码(Replace Error Code with Exception):通过业务异常替代特定返回码,当外部需要提前检查时抛非受控异常,否则函数声明受控异常并由调用者处理。
- 以测试取代异常(Replace Exception with Test):当调用的函数可能因预检查而抛出异常,应在调用函数之前先做检查。
2.5 在对象之间搬移特性
- 搬移函数(Move Method):函数与某个类进行密切交流,可将函数搬移到该类中。
- 搬移字段(Move Field):字段与某个类进行密切交流,可将字段搬移到该类中。
- 提炼类(Extract Class):类做了多个类做的事,可将相关的字段和函数拆分到新类。
- 将类内联化(Inline Class):类没有做太多事情,可将类的所有特性搬移到另一个类并删除原类。
- 隐藏委托关系(Hide Delegate):外部通过类里的委托关系来调用另一个对象,可在类建立外部所需的所有函数,并隐藏委托关系。
- 移除中间人(Remove Middle Man):类做了过多的简单委托动作,让外部直接调用受托类。
- 引入外加函数(Introduce Foreign Method):当需要为无法修改的服务类增加函数,可在客户类中建立函数,并以第一参数形式传入服务类实例。
- 引入本地扩展(Introduce Local Extension):当需要为无法修改的服务类提供额外函数,通过新类扩展或包装服务类,使它包含这些额外函数。
2.6 处理概括关系
- 字段上移(Pull Up Field):两个子类拥有相同的字段,将该字段移至超类。
- 函数上移(Pull Up Method):函数在各个子类中产生完全相同的结果,将该函数移至超类。若待上移函数调用了只出现于子类的函数,可在超类中为被调用函数声明抽象函数。
- 构造函数本体上移(Pull Up Constructor Body):多个子类拥有完全一致的构造函数,可将其上移到超类,并在子类构造函数中调用它。
- 函数下移(Push Down Method):超类中的某个函数只与部分子类有关,将这个函数移到相关的子类。
- 字段下移(Push Down Field):超类中的某个字段只被部分子类用到,将这个字段移到相关的子类。
- 提炼超类(Extract Superclass):多个类有相似的特性,为这些类建立一个超类,将相同特性提炼到超类。
- 提炼接口(Extract Interface):若干客户使用类接口中的同一子集,或多个类的接口有部分相同,将相同的子集提炼到独立接口。
- 折叠继承体系(Collapse Hierarchy):超类和子类之间无太大差别时,将它们合为一体。
- 塑造模板函数(Form TemPlate Method):子类中某些函数以相同顺序执行类似的操作,但各操作的细节上有所不同,可将操作分别提炼到相同签名的独立函数,并将组装顺序的函数上移到超类。
- 以委托取代继承(Replace Inheritance with Delegation):子类仅使用超类的部分内容或不需要继承而来的数据,调整子类函数令它委托超类,去掉两者之间的继承关系。
参考文献
《重构-改善既有代码的设计》