重构(Refactoring)技巧
本文简要整理重构方法的读书笔记及个人在做
Code Review
过程中,对程序代码常用的一些重构策略。通过适当的重构代码,的确可以显著提高代码的质量,令人赏心悦目。毫无疑问,这些重构策略均来自于
Martin Fowler
的
《重构-改善既有代码的设计》,只是如何在实际项目中灵活运用而已。(注:本文重构策略的名称及其大部分内容来自《重构-改善既有代码的设计》一书,
Martin Fowler
著,侯捷等译)。
先看看重构的定义吧:
(
1
)
Refactoring means rewriting existing source code with the intent of improving its design rather than changing its external behavior. The focus of refactoring is on the structure of the source code, changing the design to make the code easier to understand, maintain, and modify.
-
来自
Borland Together
提供的文档,觉得这个定义很清晰明了。
(
2
)重构是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,已改进程序的内部结构。-来自
Martin Fowler
的定义。
因此,掌握必要的重构技巧逐步成为对程序员基本的要求,重要的是在掌握这些技巧后,也有助于类库初期设计的质量,避免或减少代码的坏味道
(bad smell)
。
一、代码坏味道(Bad Smell in Codes)及其重构策略
1.尽量消除重复的代码,将它们合而为一
根据重复的代码出现在不同的地方,分别采取不同的重构的策略:
在同一个
Class
的不同地方:通过采用重构工具提供的
Extract Method
功能提炼出重复的代码,
然后在这些地方调用上述提炼出方法。
在不同
Subclasses
中:通过
Extract Method
提炼出重复的代码,然后通过
Pull Up Method
将该方法移动到上级的
Super class
内。
在没有关系的
Classes
中:通过对其中一个使用
Extract Class
将重复的代码提炼到一个新类中,然后在另一个
Class
中调用生成的新类,消除重复的代码。
2.拆解过长的函数
过长的函数在我们的日常代码中经常可见,在
C#
中常通过
#region #endregion
区隔为不同的功能区域。
重构策略:通过
Extract Method
将过长的函数按照功能的不同进行适当拆解为小的函数,并且给这些小函数一个好名字。通过名字来了解函数提供的功能,提高代码的理解性。
3.拆解过大的类
过大的类也经常见到,特别是类中含有大量的成员变量。
重构策略:通过
Extract Class
将一些相关成员变量移植到新的
Class
中,如
Employee
类,一般会包含有联系方式的相关属性(电话,
Mobile
,地址,
Zip
等等),则可以将这些移植到新的
EmployeeContact
类中。
4.过长的参数列
过长的参数列的主要问题是难以理解,并且难以维护。如果要增加新的参数或者删除某一参数,易造成参数前后不一致。
重构策略:如果可以通过向已存在的对象查询获取参数,则可通过
Replace Parameter with Method
,移除参数列,通过在函数内部向上述已存在的对象查询来获取参数。
如果参数列中若干参数是已存在对象的属性,则可通过
Preserve Whole Object
将这些参赛替换为一个完整对象,这样不仅提高代码的可读性,同时已易于代码今后的维护。
另外,还可以将若干不相关的参数,使用
Introduce Parameter Object
来创建一个新的参数类。不过,我个人觉得如果这些情况过多的话,会产生很多莫名其妙的参数类了,反而降低代码的可读性。
个人觉得前面
4
种坏味道比较显而易见,也比较容易处理。
现象:当某个
Class
因为外部条件的变化或者客户提出新的功能要求等时,每次修改要求我们更新
Class
中不同的方法。不过这种情况只有在事后才能觉察到,因为修改都是在事后发生的么(废话)。
重构策略:将每次因同一条件变化,而需要同时修改的若干方法通过
Extract Class
将它们提炼到一个新
Class
中。实现目标是:每次变化需要修改的方法都在单一的
Class
中,并且这个新的
Class
内所有的方法都应该与这个变化相关。
6
.
Shotgun Surgery
(霰弹式修改)
现象:当外部条件发生变化时,每次需要修改多个
Class
来适应这些变化,影响到很多地方。就像霰弹一样,发散到多个地方。
重构策略:使用
Move Method
和
Move Field
将
Class
中需要修改的方法及成员变量移植到同一个
Class
中。如果没有合适的
Class
,则创建一个新
Class
。实现目标是,将需要修改的地方集中到一个
Class
中进行处理。
比较
Divergent Change
(发散式变化)和
Shotgun Surgery
(霰弹式修改):
前者指一个
Class
受到多种外部变化的影响。而后者指一种变化需要影响到多个
Class
需要修改。都是需要修理的对象。
7
.
Feature Envy
(依恋情结)
现象:
Class
中某些方法“身在曹营心在汉”,没有安心使用
Class
中的成员变量,而需要大量访问另外
Class
中的成员变量。这样就违反了对象技术的基本定义:将数据和操作行为(方法)包装在一起。
重构策略:使用
Move Method
将这些方法移动到对应的
Class
中,以化解其“相思之苦”,让其牵手。
8
.
Data Clumps
(数据泥团)
现象:指一些相同数据项目(
Data Items
),如
Class
成员变量和方法中参数列表等,在多个
Class
中多次出现,并且这些数据项目有其内在的联系。
重构策略:通过使用
Introduce Parameter Object
(创建新的参数对象取代这些参数)或
Preserve Whole Object
(使用已存在的对象取代这些参数),实现使用对象代替
Class
成员变量和方法中参数列表,清除数据泥团,使代码简洁,也提高维护性和易读性。
9
.
Primitive Obsession
(基本型偏执狂)
现象:在
Class
中看到大量的基本型数据项目(
Data Item
),如
Employee
类中有大量的数据成员,
Employee#, FirstName, MiddleName, LastName, Address, State, City, Street, Zip, OfficePhone, CellPhone, Email……
等等。
重构策略:使用
Extract Class
(提炼新类)或
Preserve Whole Object
(使用已存在的对象取代这些参数),实现使用对象代替基本型数据项目(
Data Item
)。如上述
Employee
类中就可分别提炼出
EmployeeName
和
EmployeeContact
两个新类。
10
.
Switch Statements
(
Switch
语句)
现象:同样的
Switch
语句出现在不同的方法或不同的
Class
中,这样当需要增加新的
CASE
分支或者修改
CASE
分支内语句时,就必须找到所有的地方,然后进行修改。这样,就比较麻烦了。
重构策略:
(1)
首先采用
Extract Method
将
Switch
语句提炼到一个独立的函数。
(2)
然后以
Move Method
搬移到需要多态性(
Polymorphism
)的
Superclass
里面或者是构建一个新的
Superclass
。
(3)
进一步使用
Replace Type Code with Subclasses
或者
Replace Type Code with State/Strategy
。这步就比较麻烦些,不过记住如下基本规则:这里一般有
3
个
Class
分别为
Source Class
、
Superclass
和
Subclass
。
Source Class
:
l
使用
Self Encapsulate Field
,将
Type Code
成员变量封装起来,也就是建立对应的
Setter/Getter
函数。
l
在
Source Class
中增加一个
Superclass
类型的成员变量,用来存放
Subclass
实例对象。
l
在
Source Class
中的
Getter
函数,通过调用
Superclass
的
Abstract Query
函数来完成。
l
在
Source Class
中的
Setter
函数,通过调用
Superclass
中的
Static
工厂化方法来获取合适的
Subclass
实例对象。
Superclass
:
新建的一个
Class
(注:就是上面通过
Move Method
搬移生成的
Superclass
),根据
Type Code
的用途命名该
Class
,作为
Superclass
。
l
在
Superclass
中建立一个
Abstract Query
函数,用来获取
Subclass
的
Type Code
。
l
在
Superclass
中创建
Static
工厂化方法生产对应的
Subclass
对象,这里会存在一个
Switch
语句(不要再动脑筋来重构这个
Switch
语句了,这个
Switch
语句不会在多处重复存在,并且这里用于决定创建何种
Subclass
对象,这是完全可以接受的)。
Subclass
:
l
根据每一个
Switch/Type
分支,建立对应的
Subclass
,并且
Subclass
的命名可以参考
Switch/Type
分支的命名。
l
在每一个
Subclass
中重载
Superclass
的
Abstract Query
函数,返回特定的
Type Code
。
(4)
现在
Superclass
仍然存在
Switch
分支,是时候轮到
Replace Conditional with Polymorphism
上场了。具体而言,就是在每一个
Subclass
中创建重载方法(注:该方法是
Superclass
中含有
Switch
语句的方法),并将
Superclass
中
Switch
语句对应的
Case
分支剪切过来。最后将
Superclass
中该方法初象化
Abstract
,并清除
Switch
语句及其所有的
Case
分支。
这样就完成了整个重构过程,这个比较麻烦。
注:并不是一看到
Switch
语句及
CASE
分支,就马上
/
偏执狂采用上述重构策略进行重构,画蛇添足或吃亏不讨好(个人观点)。一般而言,只有看到多处出现相同的
Switch
语句时,才应该考虑进行重构。
11
.
Parallel Inheritance Hierarchies
(平行继承体系)
现象:为某个
class
增加一个
subclass
时,也必须为另一个
class
相应增加一个
subclass
。重构策略:
在一个
class
继承体系的对象中引用(
refer to
)另一个
class
继承体系的对象,然后运用
Move Method
和
Move Field
将被引用
class
中的一些方法和成员变量迁移宿主
class
中,消除被引用
class
的继承体系(注:这种平行继承体系好象比较少见也)。
12
.
Lazy Class
(冗赘类)
现象:某一些
class
由于种种原因,现在已经不再承担足够责任,有些多余了。如同国有企业冗余人员一样,需要下岗了。
重构策略:通过
Collapse Hierarchy
,将这些冗余的
class
合并到
superclass
或
subclass
中,或者通过
Inline Class
(与
Extract Class
相反),将这些冗余
class
中的所有
Method/Field
迁移到其他相关的
class
中。
13
.
Speculative Generality
(夸夸其谈未来性)
现象:系统中出现一些无用的
abstract class
,或者非必要的
delegation
(委托),或者多余的参数等等。
重构策略:分别使用
Collapse Hierarchy
合并
abstract class
,使用
Inline Class
移除非必要的
delegation
,使用
Remove Parameter
删除多余的参数。
14
.
Temporary Field
(令人迷惑的暂时值域)
现象:
class
中存在一些
Field
,这些
Field
只在某种非常特定的情况下需要。
重构策略:通过
Extract Class
将这些孤独的
Field
及其相关的
Method
移植的一些新的
Class
中。提炼出来的新
Class
可能没有任何抽象意义,只是提供
Method
的调用,这些新
Class
一般称为
Method Object
。
15
.
Message Chains
(过度耦合的消息链)
现象:向一个对象请求另一个对象,然后再向后者请求另一个对象,……,这就是
Message Chain
,意味着
Message Chain
中任何改变,将导致
Client
端不得不修改。
重构策略:通过
Hide Delegate
(隐藏委托关系)消除
Message Chain
,具体做法是在
Message Chain
的任何地方通过
Extract Method
建立一个简单委托(
Delegation
)函数,来减少耦合(
Coupling
)。
16
.
Middle Man
(中间转手人)
现象:过度运用
delegation
,某个
/
某些
Class
接口有一半的函数都委托给其他
class
,这样就是过度
delegation
。
重构策略:运用
Remove Middle Man
,移除简单的委托动作(也就是移除委托函数),让
client
直接调用
delegate
受托对象。和上面的
Hide Delegate
(隐藏委托关系)刚好相反的过程。
由于系统在不断的变化和调整,因此
[
合适的隐藏程度
]
这个尺度也在相应的变化,
Hide Delegate
和
Remove Middle Man
重构策略可以系统适应这种变化。
另外,可保留一部分委托关系(
delegation
),同时也让
Client
也直接使用
delegate
受托对象。
17
.
Inappropriate Intimacy
(狎昵关系)
现象:两个
Class
过分亲密,彼此总是希望了解对方的
private
成分。
重构策略:可以采用
Move Method
和
Move Field
来帮助他们划清界限,减少他们之间亲密行为。或者运用
Change Bidirectional Association to Unidirectional
,将双向关联改为单向,降低
Class
之间过多的依存性(
inter-dependencies
)。或者通过
Extract Class
将两个
Class
之间的共同点移植到一个新的
Class
中。
18
.
Alternative Classes with Different Interfaces
(异曲同工的类)
现象:两个函数做相同的事情,却有不同的
signature
。
重构策略:使用
Rename Method
,根据他们的用途来重命名。另外,可以适当运用
Move Method
迁移某些行为,使
Classes
的接口保持一致。
19
.
Incomplete Library Class
(不完美的程序库类)
现象:
Library Class
(类库)设计不是很完美,我们需要添加额外的方法。
重构策略:如果可以修改
Library Class
的
Source Code
,直接修改最好。如果无法直接修改
Library Class
,并且只想修改
Library Class
内的一两个函数,可以采用
Introduce Foreign Method
策略:在
Client Class
中建立一个函数,以外加函数的方式来实现一项新功能(一般而言,以
server class
实例作为该函数的第一个参数)。
如果需要建立大量的额外函数,可应该采用
Introduce Local Extension
:建立一个新
class
,使它包含额外函数,并且这个
class
或者继承或者
wrap
(包装)
source class
。
20
.
Data Class
(纯稚的数据类)
现象:
Data Class
指:一些
Class
拥有
Fields
,以及用来访问
Fields
的
getter/setter
函数,但是没有其他的功能函数。(感觉这些
Data Class
如同
Entity Class
或
Parameter Class
,用来传递参数,我认为这种情况下没有必要重构。)
重构策略:找出其他
class
中访问
Data Class
中的
getter/setter
的函数,尝试以
Move Method
将这些函数移植到
Data Class
中,实现将数据和操作行为(方法)包装在一起,也让
Data Class
承担一定的责任(方法)。
21
.
Refused Bequest
(被拒绝的遗赠)
现象:
Subclass
不想或不需要继承
superclass
的部分函数和
Field
。
重构策略:为
subclass
新建一个兄弟(
sibling class
),再运用
Push Down Method
和
Push Down Field
将
superclass
中的相应函数和
Field
下推到兄弟
class
,这样
superclass
就只包含
subclass
共享的东西了。其实,也就是将
superclass
中一些与特定的函数和
Field
放到特定的
subclass
中,
superclass
中仅包含
subclass
共享的函数和
Field
。
如果不想修改
superclass
,还可以运用
Replace Inheritance with Delegation
来达到目的。也就是以委托取代继承,在
subclass
中新建一个
Field
来保存
superclass
对象,去除
subclass
对
superclass
的继承关系,委托或调用
superclass
的方法来完成目的。
22
.
Comments
(过多的注释)
现象:(晕倒,这个也要重构,
Remove
掉所有的
Comments
吗?不是。)当代码中出现一段长长的注释,一般是由于代码比较糟糕,需要进行重构,除去代码的坏味道。
重构策略:通过上面提及的各种重构策略,将代码的坏味道去除,使注释变成多余。
如果需要注释
/
解释一段代码做了什么,则可以试试
Extract Method
,提取出一个独立的函数,让函数名称解释该函数的用途
/
功能。另外,如果觉得需要注释来说明系统的某些假设条件,
也可尝试使用
Introduce Assertion
(引入断言),来明确标明这些假设。
当你感觉需要撰写注释时,请先尝试重构,试着让所有的注释都变得多余。
5
.
Divergent Change
(发散式变化)