一、引入
1. 修改案例
- 详见第一章
(1)大函数拆成小函数;
(2)更改变量名;
(3)转移到所用数据的所属对象内,并去掉旧函数;
(4)去除临时变量 运用多态取代条件逻辑,最好不要在另一个对象的数学基础使用 switch 语句,如果不得已使用也应该在对象自己的数据上使用;
(5)每次新作修改及时测试;
2. 重构理论
-
2.1 是什么
动词定义:在不改变代码外在行为的前提下,对代码作出修改,以改进程序的内部结构的过程;
名词定义:对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本;
2.2 为什么
改进软件设计 使软件更容易理解 帮助找 bug 提高编程速度;
2.3 重构时机
何时重构
(1)三次法则:事不过三,三则重构;
(2)添加功能;
(3)修补错误;
(4)复审代码;
(1)容易阅读;
(2)所有逻辑都只在唯一地点指定;
(3)新的改动不会危及现有行为;
(5)尽可能简单表达条件逻辑
2.5 间接层和重构
- 允许逻辑共享;
- 分开解释意图和实现;
- 隔离变化,封装条件逻辑;
“间接层”所能带来的全部利益:解释能力、共享能力、选择能力,其都是由小函数支持的;
3. 重构的基本技巧与难题
重构的基本技巧:小步前进,频繁测试;
- 设计模式与重构的关系:模式是目标,重构是途径;
- 重构的难题——数据库
- 程序与数据库结构的高耦合;
- 数据迁移;
- 修改接口:让旧接口调用新接口;
- 尽量不要发布接口;
- 难以通过重构手法完成的设计改动:在一个项目中,很难(但还是有可能)将不考虑安全性需求时构造起来的系统重构为具备良好安全性系统;
-
4. 重构与测试
每个类都应有对应的测试函数,并以 ta 来测试自己这个类,要确保测试类完全实现自动化,即让 ta 检查自己的测试结果;
- 编写测试代码的时机:
- 需要添加新功能的时候,先写测试代码,好处是可以将注意力放在接口而非实现,也是个阶段性的 end 标识;
- Junit 框架设计用来进行单元测试,功能测试往往以其他工具辅助进行;
- 编写边测试,不要期待一开始就考虑到所有情况,能写出完美的代码;
- 考虑可能出错的边界条件,并把测试火力集中在那儿;
- 当事情被认为应该会出错时,别忘了检查是否抛出预期的异常;
测试收益在测试数量达到一定规模时开始呈递减趋势,要测试的是那些复杂和易出错的地方,不要因为测试不能检查出所有 bug 就不写测试,毕竟测试可以检查出大多数 bug;
二、代码的坏味道
详见第三章
- 重复代码
- 过长函数
- 应积极地分解函数,遵循一条规则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名;
- 过大的类
- 采用 extract Class 和 extract subClass;
- 过长参数列
- 发散式变化
- 某个类常因为不同的原因在不同的方向上发生变化;
- shotgun Surgery:一个变化引发多个类相应修改;
- 依恋情结:判断哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数据摆在一起;
- 数据泥团:减少字段和参数的个数:
- 基本类型偏执(Primitive Obsession)
- 有一组应该总是放在一起的字段,可运用 extract Class;
- 如果在参数列中看到基本型数据,可尝试 Introduce Parameter Object;
- 如果发现自己正从数组中挑选数据,可运用 replace Array with Object;
- Switch Statement
- 仅适用:只在单一函数中有些选择事例,且不想改动它们;
- 一般情况下,面向对象编程中应用多态来替换 switch;
- 平行继承体系
- 为某个类增加一个子类,也必须为另一个类相应地增加一个子类;
- 发现某个继承体系的类名称前缀和另一个继承体系的类名称前缀完全相同
- 消除这种重复性的一般策略:让一个继承体系的实例引用另一个继承体系的实例,然后再使用 move method 和 move field
- Lazy Class : 对于几乎没有用的组件,使用 inline Class;
- Speculative generality:去除或简化一切用不到或不值得的;
- middle man
- 不可过度使用委托,如果代理函数只有少数几个,可运用 InlineMethod 把它们放进调用端,如果其还有其它行为,可运用 Replace Delegation with Inheritance 把它变成实责对象的子类;
- Inappropriate intimacy
- 对于紧密联系的两个类,可运用 extract class 把两者共同点提炼到一个新的公开类,也可使用 hide delegate 让另一个类来传递它们之间的联系;
- Incomplete Library class
- 若只想修改类库中的一两个函数,可以使用 introduce foreign method,若想要添加一大堆额外行为,需使用 introduce local extension;
- Data class 只有字段+setter&getter函数:保证恰当封装;
- refused bequest
- 若子类复用了超类的行为,却不愿支持超类的接口,切勿胡乱修改继承体系,而应 replace inheritance with delegation;
- over comments
尽量用重构让注释变多余,注释一般用于标识待实现行为,对于想注释说明的代码,尝试 extract method,对于想要注释说明的行为,尝试 rename method,对于想要注释说明某些系统的需求规格,尝试 introduce assertion;
三、重构列表
-
1. 重新组织函数
1.1 处理过长代码 Long method
extract method
- motivation:函数过长或需引入注释,细粒度函数可增加复用概率且覆写容易;
- mechanics
- 以函数用途对新函数命名;
- 将提炼代码从源函数复制到目标函数;
- 检查提炼代码中是否有作用域仅局限于源函数或只使用在提炼代码中;
- 将提炼代码中使用的源函数的局部变量通过参数传给目标函数;
- 若提炼代码对局部变量有修改,则将其作为返回值;
- 处理完所有局部变量后进行编译;
- 在源函数中将提炼代码变为对目标函数的调用;
- 若参数众多,则:
- 使用 replace temp with query;
- replace method with method object,即抽象出一个新的类;
1.2 inline method 内联函数
- motivation
- 一个函数的本体与名称同样清楚易懂;
- 将目标函数的所有调用对象的函数内容都内联到函数对象中,将整个大函数作为整体移动;
- mechanics
- 函数不具有多态性;
- 将函数中的所有被调用点都替换为函数本体;
- 编译测试;
1.3 inline temp 内联临时变量
- 有一个临时变量,只被一个简单表达式赋值一次,却妨碍了其他重构手法;
- motivation
- 作为 replace temp with query 的一部分;
- 某个临时变量被赋予某个函数调用的返回值;
- mechanics
- 检查给临时变量赋值的语句,确保等号右边的表达式无副作用;
- 将临时变量声明为 final 并编译(检查临时变量是否只被赋值一次);
- 将临时变量的所有引用点都替换为“为临时变量赋值”的表达式;
- 每做修改都进行测试,删除多余的声明和赋值语句,再次编译、测试;
1.4 replace temp with query
- 程序以一个临时变量保存某一表达式的运算结果,将这个表达式提炼到一个函数中,将临时变量的所有引用点改为对新函数的调用,方便被其他函数使用;
- motivation
- 运用 extract method 前的必要步骤;
- 同一个类的所有方法都可获得此信息;
- mechanics
- 找出只被赋值一次的临时变量,若某个临时变量被赋值超过一次,考虑使用 split temporary variable,将其分割为多个变量;
- 将该临时变量声明为 final;
- 将等号右侧为临时变量赋值的语句提炼为一个独立函数,并确保其无副作用,否则使用 separate query from modifier;
- 编译、测试;
- 对该临时变量使用 inline temp;
1.5 introduce explaining variable 引入解释性变量
- 将一个复杂表达式(或一部分)的结果放入一个临时变量,以变量名称来解释表达式的用途;
- motivation
- 在条件逻辑中解释条件语句的意义;
- 在较长算法中用重构的临时变量解释每一步运算的意义;
- 针对复杂表达式优先考虑使用 extract method,当局部变量使用 extract method 难以进行时或需要花费巨大工作量时,才考虑使用 introduce explaining variable;
- mechanics
- 将复杂表达式的一部分动作赋值给一个 final 临时变量;
- 将所有使用该表达式的地方替换为此临时变量;
1.6 remove middle man
- motivation:某个类做了过多的简单委托动作;
- mechanics:让受托类直接被客户调用;
1.7 introduce foreign method
- motivation:需要为提供服务的类增加一个函数,但不能对这个类作出修改;
- mechanics:在客户类中建立一个函数,并以第一参数的形式传入一个服务类实例;
1.8 introduce local extension
- motivation:需要为服务类增加一些函数,但不能对其修改;
mechanics:建立一个包含这些额外函数的扩展类,使其成为原服务类的子类或包装类;
2. 在对象之间搬移特性
2.1 move method
程序中,有个函数与其所处类之外的类调度关系更密切;
- motivation
- 一个类有太多行为,或与另外一个类有太多合作而造成高度耦合;
- mechanics
- 在该函数最常引用的类中,建立一个有着类似行为的新函数,将原函数作为委托或者移除;
2.2 move field
- motivation:程序中某个字段被其所在类之外的某个类频繁使用;
- mechanics
- 在目标类中建立与原字段相同的字段,并建立相应的 setter & getter 函数;
- 删除原字段,并将源对象中对原字段的引用替换为对目标对象行为的调用;
2.3 extract class
- motivation
- 类包含大量的字段和函数,变得复杂;
- mechanics
- 分解责任,建立新类,并正确关联新旧类之间的关系;
2.4 inline class
- motivation:某个类没有做太多事情;
- mechanics:将这个搬移至另一个类;
2.5 hide delegation
- motivation:客户通过委托类来调用另一个对象;
- mechanics:在服务类上建立客户所需的所有函数,用于隐藏委托关系;
2.6 split temporary variable
- 程序中有某个临时变量被赋值超过一次,但其既不是循环变量,也不用于收集计算结果,针对每次赋值,创造一个独立、对应的临时变量;
- motivation
- 循环变量、结果收集变量、用于保存一段冗长代码运算结果的临时变量;
- mechanics
- 在每个新的赋值处,重新声明并修改变量名称,以 final 修饰,并修改对此变量的所有引用点,编译 & 测试;
2.7 remove assignment to parameters
- 代码对参数赋值时;
- motivation
- 混淆按值传递和按引用传递;
- mechanics
- 用一个临时变量替代待处理的参数,修改对此参数的所有引用点,编译 & 测试;
- 对参数加上 final,强制其遵循“不对参数赋值”;
2.8 replace method with method object
- 在大型代码中,对局部变量的重构无法使用 extract method;
- motivation:小型代码的优雅,局部变量泛滥,增加分解难度;
- mechanics
- 将此函数放入一个对象,使局部变量成为对象内的字段,可以在这个对象中将大型函数分解成多个小型函数;
2.9 substitute algorithm
-
3. 重新组织数据
3.1 self excapsulated field 自封装字段
motivation:直接访问一个字段,但与字段之间的耦合关系逐渐变得笨拙;
- mechanics:为这个字段建立 setter & getter 函数,并且只以这些函数来访问字段;
3.2 replace data value with object
- motivation:有一个数据项,需要与其他数据或行为一起使用才有意义;
- mechanics:将数据项变成对象;
3.3 change value to reference
- motivation:从一个类中衍生出许多彼此相等的实例,希望将它们替换为一个对象;
- mechanics:将这个值对象变成引用对象,使用 replace constructer with factory method;
3.4 change reference to value
- motivation:值对象可变,需保证对某一对象的改变会自动更新其他“代表相同事物”的对象;
- mechanics:将引用对象变成值对象;
3.5 replace array with object
- motivation:有一个数组,其中每个元素代表不同的事物;
- mechanics:以对象代替数组,数组元素用字段表示;
3.6 duplicate observed data
3.7 change unidirectional association to bidirectional
- motivation:两个类都需要使用对方特性,但其间只有一条单向连接;
- mechanics:添加一个反向指针,并使修改函数能够同时更新两条连接;
- 若两者都是引用对象,且其间的关联是“一对多”关系,那么由“拥有单一引用”的那一方承担“控制者”角色;
- 若某个对象是组成另一对象的部件,那么由后者负责控制关联关系;
- 若两者都是引用对象,且其间的关联是“多对多”关系,可任选一个来控制关联关系;
3.8 change bidirectional association to unidirectional
- motivation:两个类之间有双向关联,但其中一个类之后不再需要另一个类的特性;
- mechanics:去除不必要的关联;
3.9 replace magic number with symbolic constant
- motivation:有一个字面数值,带有特别含义;
- mechanics:创造一个常量,以含义为其命名,并将字面数值为常量赋值;
3.10 encapsulate field
- motivation:类中存在一个 public 字段;
- mechanics:将此字段声明为 private,并提供相应的访问函数;
3.11 encapsulate collection
- motivation:有个函数返回一个集合;
- mechanics:让这个函数返回该集合的一个只读副本,并在这个类中提供添加/移除集合;
3.12 replace record with data class
- motivation
- 可能面对一个遗留程序,可能需要通过一个传统 API 来与记录结构交流,或是处理从数据库读出的记录;
- mechanics
- 新建一个类,表示这个记录,对于记录中的每一项数据,在新建类中建立对应的 private 字段,并提供相应的 setter & getter 函数;
3.13 replace type code with class
- motivation:类之中有一个数值类型码,但其并不影响类的行为;
- mechanics:以一个新的类替换该数值类型码;
3.14 replace type code with subclasses
- motivation
- 有一个不可变的类型码,ta 会影响类的行为,可借助多态来处理变化行为;
- 宿主类中出现“只与具备特定类型码之对象相关”的特性;
- mechanics
- 以类型码的宿主类为基类,针对每种类型码建立相应的子类,如果类型码被传递给构造函数,需要将构造函数换成工厂函数;
3.15 replace type code with state/strategy
- motivation:有一个类型码,会影响类的行为,但无法通过集成手法消除;
- mechanics
- 新建一个类,根据类型码的用途为 ta 命名,记为状态对象,为这个新类添加子类,每个子类对应一种类型码;
- 在超类中建立一个抽象的查询函数,用于返回类型码,在每个子类中覆写该函数,返回确切的类型码;
- 在源类中建立一个字段来保存新建的状态对象,调整源类中负责查询类型码的函数,将查询动作转发给状态对象;
- 编译 & 测试;
3.16 replace subclass with fields
- motivation:不同子类中的同一个访问函数返回不同的常量数据,可以在超类中将访问函数声明为抽象函数,并在不同子类中让它返回不同的值;
mechanics
motivation:复杂条件逻辑降低代码的可读性;
- mechanics:将 if、then、else 段落提炼出来,各自构成一个独立函数;
4.2 consolidate conditional expression
- motivation:有一串条件检查,检查条件各不相同,最终行为却一致;
- mechanics:确定这些条件语句没有副作用,使用适当的逻辑操作符,将一系列相关条件表达式合并为一个,并对合并后的条件表达式实施 extract method;
4.3 consolidate duplicate conditional fragments
- motivation:一组条件表达式的所有分支都执行了相同的某段代码;
- mechanics:将这段重复代码搬移至条件表达式之外;
4.4 remove control flag
- motivation:在一系列布尔表达式中,某个变量带有“控制标记”的作用;
- mechanics
- 找出可跳出这段逻辑的控制标记值所对应的赋值语句,适当选用 break 或 continue;
- 对于变量既是控制标记也是运算结果的情况,可把计算该变量的代码提炼到一个独立函数中,然后以 return 语句取代控制标记;
4.5 replace nested conditional with guard clauses
- motivation:若某个条件极其罕见,应单独检查该条件,并在条件为真时立刻从函数中返回,这种单独检查被称为“卫语句”(guard clauses);
- mechanics:对于每个检查放进一个卫语句,卫语句要不就从函数中返回,要不就抛出一个异常,特殊情况下可试着将条件反转;
4.6 replace conditional with polymorphism
- motivation:有个条件表达式会根据对象类型的不同而选择不同的行为;
- mechanics:多态,将这个条件表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数;
4.7 introduce null object
- motivation:需要再三检查某对象是否为 null;
- mechanics:为源类建立一个子类,使其行为就像是源类的 null 版本,在源类和 null 子类中都加上 isNull() 函数,前者的 isnull 应返回 false,后者的 isnull 应返回 true;
4.8 introduce assertion
- motivation:某一段代码需要对程序状态做出某种假设;
mechanics:使用断言明确标明假设,断言是一个条件表达式,应该总为真,若其失败,表示程序员犯了错误,断言绝不能被系统的其他部分使用,程序最终成品应将断言删除,断言的价值在于帮助程序员理解代码正确运行的必要条件;
5. 简化函数调用
5.1 rename method
motivation:函数的名称未能揭示函数的用途
- mechanics:修改函数名称;
5.2 add parameter
- motivation:某个函数需要从调用端得到更多信息;
- mechanics:为此函数添加一个对象参数,让该对象带进函数所需信息;
5.3 remove parameter
- motivation:函数本体不再需要某个参数;
- mechanics:去除不必要的参数;
5.4 separater query from modifier
- motivation:某个函数既返回对象状态值,又修改对象状态;
- mechanics:建立两个不同的函数,其中一个负责查询,另一个负责修改;
5.5 parameterize method
- motivation:若干函数做了类似的工作,但在函数本体中却包含了不同的值;
- mechanics:建立单一函数,以参数表达那些不同的值;
5.5 replace parameter with explicit methods
- motivation:某个函数完全依据参数值而采取不同行为;
- mechanics:针对该参数的每一个可能值,建立一个独立函数;
5.6 preserve whole object
- motivation:从某个对象中取出若干值,将它们作为某一次函数调用时的参数;
- mechanics:对目标函数新添一个参数项,用以代表原数据所在的完整对象,确定可被包含在新添对象中的参数;
5.7 replace parameter with methods
- motivation:对象调用某个函数,并将所得结果作为参数,传递给另一个函数,而接受该参数的函数本身也能够调用前一个函数,即函数可通过其他途径获得参数值;
- mechanics:必要时可将参数的计算过程提炼到一个独立函数中,将函数本体类引用该参数的地方改为调用新建函数;
5.8 introduce parameter object
- motivation:某些参数总是很自然地同时出现;
- mechanics:以一个对象取代这些参数;
5.9 remove setting method
- motivation:类中的某个字段应在对象创建时被设值,然后就不再改变;
- mechanics
- 修改构造函数,使其直接访问设值函数所针对的那个变量,若某个子类通过设值函数给超类的某个 private 字段设了值,那就不能采用此修改方式,应尝试在超类中提供一个 protected 函数(最好是构造函数)来给这些字段设值;
- 去除该字段所有设值函数,将其针对的字段设为 final;
5.10 hide method
- motivation:有一个函数从未被其他任何类用到;
- mechanics:将这个函数修改为 private;
5.11 replace constructor with factory method
- motivation:在派生子类的过程中以工厂函数取代类型码;
- mechanics:新建一个工厂函数,让其调用现有的构造函数,将调用构造函数的代码改为调用工厂函数;
5.12 encapsulate downcast
- motivation:某个函数返回的对象,需要由函数调用者执行向下转型;
- mechanics:将向下转型动作移到函数中,通常出现在返回一个集合或迭代器的函数中;
5.13 replace error code with exception
- motivation:某个函数返回一个特定的代码,用以表示某种错误情况;
- mechanics:改用异常,注意区分受控异常与非受控异常;
5.14 replace exception with test
- motivation:针对一个调用者可以预先检查的条件,却抛出异常,“异常”只应用于异常的、罕见的行为,而不应成为条件检查的替代品;
mechanics:修改调用者,使其在调用函数之前先做检查;
6. 处理概况关系
6.1 pull up field
motivation:两个子类拥有相同的字段;
- mechanics:将该字段移至超类;
6.2 pull up method
- motivation:有些函数,在各个子类中产生完全相同的结果;
- mechanics:将该函数移至超类;
6.3 pull up constructor body
- motivation:在各个子类中拥有一些构造函数,它们的本体几乎完全一致;
- mechanics:在超类中新建一个构造函数,并在子类构造函数中调用它;
6.4 push down method
- motivation:超类中的某个函数只与部分(而非全部)子类有关;
- mechanics:将这个函数移到相关子类中;
6.5 push down field
- motivation:超类中的某个字段只被部分(而非全部)子类用到;
- mechanics:将此字段移到所需子类中;
6.6 extract subclass
- motivation:类中的某些特性只被某些(而非全部)实例用到;
- mechanics:新建一个子类,将上面所说的那一部分特性移到子类中;
6.7 extract superclass
- motivation:两个类有相似特性;
- mechanics:为这两个类建立一个超类,将相同特性移至超类;
6.8 extract interface
- motivation:若干客户使用类接口中的同一子集,或者两个类的接口有部分相同;
- mechanics:将相同的子集提炼到一个独立接口中;
6.9 collapse hierarchy
- motivation:超类和子类之间无太大区别;
- mechanics:将它们合为一体;
6.10 form template method
- motivation:有些子类,其中相应的某些函数以相同顺序执行类似的操作,但各个操作上的细节有所不同;
- mechanics:将这些操作分别放进独立函数中,并保持它们都有相同的签名,使得原函数变得相同,然后将原函数移至超类;
6.11 replace inheritance with delegation
- motivation:某个子类只使用超类接口中的一部分,或是根本不需要继承而来的数据;
- mechanics:在子类中新建一个字段用以保存超类:调整子类函数,令它改而委托超类,然后去掉两者之间的继承关系;
6.12 replace delegation with inheritance
- motivation:在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数;
- 如果并未使用受托类的所有函数或受托对象被多个对象所共享且受托对象可变,那么就不应该使用 replace delegation with inheritance;
- 如果只是委托函数众多,可以选择 remove middle man 让客户端自己调用受托函数,也可以使用 extract superclass 将两个类接口相同的部分提炼到超类中,也可以采用类似手法使用 extract interface;
mechanics:让委托类继承受托类,将受托字段设为该字段所处对象本身;
7. 大型重构
大型重构的重要性
- 使小型重构突显价值的质量,如可预测的结果、可观察的过程、立竿见影的效果等并不存在于大型重构,若无大型重构,很可能面临投入大把时间学习重构却在实际工作中无法获得切实利益的风险;
- 四个大型重构
7.1 tease apart inheritance
- motivation:某个继承体系同时承担两项责任;
- mechanics:建立两个继承体系,并通过委托关系让其中一个可以调用另一个;
7.2 convert procedural design to object
- 将过程化设计转化为对象设计,即将数据记录变成对象,将大块的行为分成小块,并将行为移入相关对象之中;
7.3 separate domain from presentation
- motivation:某些 GUI 类之中包含了领域逻辑;
- mechanics:将领域逻辑分离出来,为它们建立独立的领域类;
7.4 extract hierarchy
- motivation:某个类承担太多工作,其中一部分工作是以大量条件表达式完成的;
mechanics:建立继承体系,以一个子类表示一种特殊情况;
四、重构工具
refactoring Browser
- 自动检查被圈选的代码段落是否可以提炼;
- 计算新函数所需的参数,并要求为新函数取一个名称;
- 把圈选代码从源函数中提炼出来,并在源函数中加上对新函数的调用;
- 重构工具的技术标准
- 程序数据库:具备贯穿整个程序搜索各种程序元素的能力;
- 解析树:对函数的任何修改都必须能够处理多层函数结构;
- 准确性:由工具实现的重构,必须合理保持程序原有行为;
重构工具的实用标准
详见第十三章(重构,复用与现实)
重构的本质是对面向对象设计的遵循,一些重构手法无不体现着抽象、封装、继承、多态的特性,而且也利用中间代理、工厂等设计模式,本着对高内聚与低耦合的追求,试图从代码本身提炼表达,编码之美,大道至简;
- 后期的重构是对早期设计的一种弥补;