chapter 1 重构,第一个案例
1.1 什么时候需要重构
需要为程序添加一个特性,但代码结构无法使自己方便的那么做,就先重构那个程序,使得特性添加比较容易进行,然后在添加特性。
1.2 重构的第一步
为即将修改的代码建立一组可靠的测试环境,即有一套可靠的测试机制;
1.3 分解并重组statement()
- 重构准则1:提炼一个函数时,必须知道可能出什么错。
- 首先找出
局部变量
和参数
,任何不会被修改的变量都可以当成参数传入新的函数。如果只有一个变量会被修改,就可以将其当成返回值。 - 重构必须以微小的步伐修改程序,如果犯下错误,就很容易发现他。
- 临时变量去掉,减少参数传递的过程,当然这可能会出现性能下降的问题。
1.4 利用多态取代与价格相关的条件逻辑
- 使用
switch
语句时,应该在自己的对象的数据身上使用,而不是在别的对象的数据身上使用。
chapter 2 重构原则
2.1 何谓重构
- 重构(名词):对软件内部结构的一种调整,目的是在不改变[软件之可察行为]前提下,提高其可理解性,降低修改成本。
- 重构(动词):使用一系列重构准则(手法),在不改变[软件之可观察行为]前提下,调整其结构。
- 使用重构技术开发软件时,只要做两件事情
- 添加新功能:此时不应该修改既有代码,只管添加新功能。通过测试(并让测试正常运行)可以衡量自己的工作进度。
- 重构:此时不能再添加新功能,只管改进程序结构。此时不应该添加任何测试(除非发现先前遗漏的任何东西),只在绝对必要(用以处理接口变化)时才修改测试。
2.2 为何重构(重构的好处和目的)
- 改进软件设计,去掉重复代码;
- 使程序易于理解;
- 帮助找bug(理解了程序的行为,自然更容易发现程序中的bug)
- 提高编程速度。
2.3 何时重构
重构应该随时随地进行,重构是为了做别的事情,而重构可以帮助自己把那件事情做的更好。
- 添加功能时一并重构
- 修补错误时一并重构
- 复审代码时一并重构
2.4 怎么对经理说
不告诉经理关于重构的任何事情。
2.5 重构的难题
重构可能引入的问题,以及局限
-
数据库
面向对象的数据库设计在数据迁移的时候会出现隐患
-
修改接口
- 修改接口时,需要特别谨慎,因为接口被修改了,任何事情都有可能发生。
即使找到也不能修改
的接口才会成为问题。 - 解决办法:让旧接口继续工作,让旧接口调用新接口。让旧函数调用新函数,千万不要复制实现代码,防止
重复代码
。
- 修改接口时,需要特别谨慎,因为接口被修改了,任何事情都有可能发生。
难以通过重构手法完成的设计改动
何时不该重构
- 重构的前提:重构之前,代码在大部分情况下可以可以
正常运作
,否则就应该重写
。 - 重写的前提:现有代码根本无法正常运转。
- 将大块头软件
重构为封装良好的小型组件
,然后就可以逐一对组件做出重构
或者重建
的决定。 - 如果项目已经接近最后期限,就没有必要重构。
2.6 重构与设计
重构与设计互补。有个重构,在设计的时候可以为了减少所谓的灵活性而增加无畏的复杂。使得软件设计更加简洁。
2.7 重构与性能
重构是为了更好的性能优化。
2.8 重构的起源
chapter 3 代码的坏味道
重构时机的选择
3.1 重复的代码
- 同一个类中两个函数有相同的表达式
- 抽取方法,将重复代码提取到一个方法里面。
- 两个互为兄弟的子类中含有相同的表达式
- 代码完全相同
- 抽取方法,提取到父类中
- 代码只是相似,并非完成相同
- 将相似部分和差异部分割开,单独构成一个函数,这里可以使用
模板方法
设计模式。
- 将相似部分和差异部分割开,单独构成一个函数,这里可以使用
- 有些函数用不同的算法做相同的事
- 选择较清晰的方法,将其他函数的算法替换掉。
- 代码完全相同
- 两个互不相关类中出现了重复代码
- 对其中一个类抽取新的类,将重复代码提炼到一个独立的class中,然后在另一个类中使用这个新的class。
3.2 过长函数
小函数的价值:间接层所能带来的全部好处都是小型函数支持的。
间接层的好处:解释能力、共享能力、选择能力
- 每当感觉需要用注释来说明的时候,我们就需要把说明的东西写进一个独立的函数,用函数的用途来命名而非实现方法。
- 条件和循环也是提炼的信号。
3.3 过大的类
- 一个class中做了太多的事情,此时就会出现太多实例变量,这时就需要抽取子类或者父类,将过多的实例变量分解到不同的类中。
- 抽取新的类、父类、子类的技巧:先确定客户端如何使用它们然后用抽取接口的方式为每一种使用方式提炼出一个接口。
3.4 过长参数列
- 少使用全局变量,多使用局部变量。
- 通过传递对象来缩短参数列表,如果某些数据缺乏合理的对象归属,就为他们制造一个参数对象。
3.5 发散式变化
- 即一个class受多种变化的影响;
- 针对某一外界变化的所有修改都应该只发生在单一的类中,而这个新的class内中的所有内容都应该反应外界变化。
即面向对象六大准则之一:每个类引起其变化的原因只有一个。
3.6 霰弹式修改
即一个变化引起多个class的修改。
3.7 依恋情节
对象技术的要点:将数据和加诸其上的操作行为包装在一起。
- 依恋情节:函数对某个class的兴趣高于自己所在的host class。即该函数的参数来源于其他类的成员变量。
- 此时,将这个函数移动到另一个地方。
- 当一个函数会用上多个class特性时
- 判断哪个class拥有最多【被此函数使用】的数据,然后将这个函数和那些数据摆放在一起。此时先将这个函数分解为数个较小的函数并放置在不同地点。问题即解决。
3.8 数据泥团
两个class内的相同值域(field)或许多函数签名式中的相同参数。
这些总是绑在一起出现的数据应该放进属于他们自己的对象中。
3.9 基本型别偏执
可以将基本类型的数据封装成对象。
3.10 switch惊悚现身
少用switch-case
语句,用多态来代替。
3.11 平行继承体系
- 当自己为一个类增加一个子类的时候,就必须为另一个class增加一个子类;
- 某个继承体系的class名称前缀和另一个继承体系的class名称前缀完全一样。
3.12 冗余类
如果一个class所得不值其身价,则这个类就应该消失,或者用内部类来代替。
3.13 夸夸其谈未来性
过多考虑未来的特性,使得代码冗余且难于理解,此时就需要将其去掉。
- 如果某个
abstract class
没有太大作用,就去掉这个层级。 - 没有必要的委托,可以用内部类来代替。
- 函数的某些参数未被使用,就应该去掉这些参数。
- 函数的名称带有多余的抽象意味,就应该对方法重命名。
3.14 令人迷惑的暂时值域
- 某个instance变量仅为某种特定情势而设。
- 抽取一个新的类,然后把和这个变量相关的代码都放到这个类中。
- 或者使用
引入空对象
,在变量不合法的情况下创建一个新对象,从而避免写条件式代码
。
3.15 过度耦合的消息链(调用链)
- 在一个对象里面调用另一个对象,然后再索引另一个对象...
- 先观察调用链最终是用来做什么的,然后采用
抽取方法
、移动方法
、新增方法
来解决过度耦合的调用问题。
- 先观察调用链最终是用来做什么的,然后采用
3.16 中间转手人
封装往往伴随委托
,就有可能产生过度委托
的情况;
3.17 狎昵关系
- 两个类过度亲密
- 采用
移动方法
、移动成员变量
、抽取类
来帮助他们划清界限。 -
继承
往往造成过度亲密,因为子类对父类的了解总是超过父类的主观愿望,此时可以让子类离开继承体系。
- 采用
3.18 异曲同工的类
两个函数做同一件事,函数声明却不一样,此时可以采用移动方法
、抽取父类
来解决。
3.19 不完美的程序类库
需要在类库的基础上在进行封装。
3.20 纯稚的数据类
数据类:他们拥有一些成员变量,以及操作这些成员变量的函数,除此之外,没有其他用处。
3.21 被拒绝的遗赠
子类应该继承父类的数据,访问父类的函数,但如果不想或者不需要继承,就意味着继承体系设计错误。这时需要为这个子类建立一个兄弟siblingclass
,在把所有用不到的函数下推给那个兄弟,如此父类就只持有所有的子类共享东西。
3.22 过多的注释
当感觉要写注释时,先尝试重构,试着让所有的注释都变得多余。
如果不知道要做什么,这才是写注释的时机。可以把自己将来要做的事情写下来,以及自己无十足把握的地方。
chapter 4 构建测试体系
4.1 自我测试代码的价值
- 程序员时间消耗:
- 编写代码;
- 决定下一步做什么;
- 设计;
- 调试解bug;(时间花费最多的地方)
正是因为没有好的测试框架,使得调试花的时间最多,因此有一个好的测试框架特别重要。
-
每个class中都应该有一个测试函数。
要频繁的进行测试,确保所有测试都完成自动化,让它们检查自己的测试结果,通过测试来确定bug出现的阶段和位置。
4.2 JUnit 测试框架
可以一次运行多个测试,使用TestSuite
来完成,实际开发中,单元测试就够了。
单元测试和功能测试
- 单元测试
- JUnit是用来做单元测试的。我们所说的测试就是单元测试。
- 单元测试的思想:每个test class只对单一的package运行,可以测试其他packages的接口,除此之外假设其他package一切正常。
- 重构时使用的就是
单元测试
。
- 功能测试
- 功能测试保证软件能够正常运行。当功能测试测出问题后,还是需要用
单元测试
来定位bug;
- 功能测试保证软件能够正常运行。当功能测试测出问题后,还是需要用
4.3 添加更多测试
- 测试的目的:找出现在或者未来可能出现的错误,测试是一种风险驱动行为。
- 做法:
- 观察class该做的所有事情,然后针对任何一项功能的的任何一种可能失败的情况进行测试。
- 考虑可能出错的边界条件,把测试火力集中在那里。
编写不完美的测试并实际运行,好多对完美测试的无尽等待。
即使只做一点点测试,也会让自己从中受益。
测试不可能捕获所有bug,但却可以捕获大部分bug,这会节省不少时间。
chapter 5 重构名录
5.1 重构的记录格式
5.2 寻找引用点
即函数、对象、变量被调用的地方,通过开发工具、编译器来完成即可。
5.3 这些重构准则有多成熟
重构的基本技巧:小步前进,频繁测试
chapter 6 重新组织你的函数
6.1 Extract Method(抽取函数)
函数过长,一个函数里面做了太多事情,导致逻辑特别复杂。
- 动机:
- 函数粒度越小,复用的可能性就越大;
- 函数粒度越小,高层函数读起来就像一系列注释;
- 函数粒度越小,函数复写也会更容易;
- 函数长度不是问题的关键,关键在于函数名称和函数本体之间的语义距离,提炼工作是要加强代码的清晰度。
- 作法:
- 创建一个新的函数,根据这个函数的作用(意图、功能、目标)来命名(以做什么而不是
怎么做
来命名);- 即使想提炼的代码非常简单,只要新函数的名称可以更好的展示代码意图,就应该提炼他,如果不能想出一个更有意义的名字,就不动。
- 将提炼的代码转移到新函数中;
- 处理所有的局部变量;
- 创建一个新的函数,根据这个函数的作用(意图、功能、目标)来命名(以做什么而不是
- 范例:
6.2 Inline Method (内联方法)###
在不影响代码的逻辑清晰度和理解的情况下,多个函数能合并成一个函数时就应该合并,把不必要的间接层去掉。
6.3 Inline Temp(内联临时变量)
一个临时变量,只被一个简单的表达式赋值过一次,同时影响了其他重构手法。
- 动机:
- 影响了其他重构手法;
- 只使用了一次,不需要临时变量来保存;
- 作法:去掉这个临时变量,直接用表达式来代替这个临时变量。
- 将这个变量申明为
final
,检查是否真的只调用了一次。 - 找到所有的引用点,将变量替换成表达式。
- 将这个变量申明为
6.4 replace Temp with Query(以查询取代临时变量)###
用一个临时变量(temp)保存一个表达式的运算结果。
- 动机:
- 临时变量只能在函数内可见,因此不仅会使函数变长,也无法被其他函数调用,复用性不够。
-
replace Temp with Query
这种重构手法是为Extract Method
服务的,局部变量难以被提炼;
- 作法:将这个表达式提炼到独立函数中。将调用这个临时变量的所有地方换成对新函数的调用。
- 找出只被赋值一次的临时变量,如果临时变量被赋值多次,就先先把他拆分成多个临时变量;
- 将
临时变量
申明为final
, - 把对这个临时变量赋值的计算语句提取到一个独立的函数中。
6.5 Introduce Explaining Variable(引入解释性变量)
如果有一个复杂表达式,将该复杂表达式(或其中一部分)的结果放进临时变量,以此变量名来解释表达式的用途。
- 动机
- 表达式有可能非常复杂而难以阅读,此时临时变量可以将表达式分解成比较容易管理的方式
- 在
if
的条件中,引入临时变量可以很好的解释条件表达式 - 在较长的算法中,可以用临时变量来解释每一步运算的意义。
- 说明:实际上这种重构手法并不经常用,通常使用抽取方法(Extract Method)来替代这种手法(因为临时变量局限性大,复用性低,但是函数复用性高),只有
抽取方法
难以进行时,才用引入解释性变量
的重构手法。 - 作法:
- 将待分解的复杂表达式的一部分运算结果赋值给一个被
final
修饰的新变量; - 然后用新变量替换这个表达式;
- 将待分解的复杂表达式的一部分运算结果赋值给一个被
6.6 Split Temporary Varible (分解临时变量)
某个临时变量被赋值超过一次,他既不是循环变量
,也不是一个集用临时变量
- 动机:
-
循环变量
和集用临时变量
(如累加、字符串拼接、IO流、集合操作等)可以多次赋值,这两种情况允许多次赋值,其他情况都不允许多次赋值; - 同一个临时变量被赋值超过一次,就意味着它们在函数中承担了一个以上的责任,这样会使代码阅读者糊涂。这时就需要重构。
-
- 作法:
- 针对每次赋值,创造一个独立的、对应的临时变量,通过
final
修饰新的临时变量来检查只被赋值一次; - 编译测试;
- 针对每次赋值,创造一个独立的、对应的临时变量,通过
6.7 Remove Assignments to Parameters (移除对参数的赋值)
- 场景:代码中对一个参数进行了赋值操作
- 动机:降低代码清晰度
- 作法:
- 建立一个临时变量,把待处理的参数赋值给他;
- 以
对参数的赋值
为界,将其后所有对此参数的引用点全部替换成对临时变量的引用
; - 修改赋值语句,使其改为对新建临时变量赋值。
6.8 Replace Method with Method Object 以函数对象取代函数
- 情景:一个函数对局部变量的使用使得无法使用
抽取方法
的重构手法 - 动机:
- 局部变量的存在会增加函数分解难度。当局部变量过多时,
以查询取代临时变量
的重构手法就无法拆分函数了,这是就需要使用函数对象
这中重构手法了。 - 小型函数会大大提高代码的可维护性和可读性。程序中能使用小型函数就不适用大型函数。
- 局部变量的存在会增加函数分解难度。当局部变量过多时,
- 做法:
- 将这个函数放进一个单独的类中,如此一来局部变量变成了类内成员字段,通过新类的
构造方法
将旧函数所有的值传递过来,包括旧函数所在的对象。 - 在新的对象里面,使用
抽取方法
将大型函数分解。
- 将这个函数放进一个单独的类中,如此一来局部变量变成了类内成员字段,通过新类的
6.9 substitute Algotithm (替换算法)
将函数本体替换成另一个算法。
- 做法
- 先将原函数分解,便于替换;
- 新算法必须先通过编译,否则坚决不替换;
- 在调试过程中,以旧算法为参考标准;
chapter 7 在对象之间搬移特性
7.1 Move Method (搬移函数)
- 场景:有个函数与其所驻的class之外的另一个class进行更多的交流,调用或者被后者调用。
- 动机:两个类耦合度太高,通过搬移函数使系统中的类更加简单。
- 做法:在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数,或是将旧函数完全移除。
- 注意事项:
- 在迁移函数时,函数所使用的特性(字段和函数),可以考虑他们一起迁移,可以将使用这些特性的所有函数一起迁移。
- 迁移函数后,可以为新的函数重新命名。
- 如果函数既用到了所在类的对象,又用到了其他类的对象,在迁移函数之前,先要将方法拆分,然后在迁移。
- 旧函数是否要删除要看具体情况。
- 当需要使用源类的特性时:
- 将这个特性也移动到目标类中;
- 建立或使用一个从目标类到源类的引用关系;
- 将
源对象
当做参数传递给目标参数; - 如果所需是个变量,将其当做参数传递给目标参数。
7.2 Move Field(搬移字段)
同迁移函数
7.3 Extract Class (抽取类)
- 情景:某个类做了应该有两个类做的事情
- 动机:
- 做法:建立一个新的类,将相关的字段和函数从旧的类搬移到新类中。
- 注意事项:
- 尽量不要建立
从新类通往旧类
的连接; - 先移动
底层函数
(被调用的次数多与调用其他函数的次数),在移动高层函数; - 决定是否公开新类。如果公开,就要决定让他成为引用对象,还是不可变的值对象(数据安全问题);
- 尽量不要建立
7.4 Inline class (将类内联化)
- 情景:某个类没有做太多的事情
- 动机:如果一个类不再承担足够责任、不再有独立存在的理由
- 做法:将这个类的所有特性搬移到另一个类中,然后移除原类。
7.5 Hide delegate(隐藏“委托关系”)
- 场景: 客户通过一个委托类来调用另一个对象
- 动机:
-
封装
意味着每个对象都应该尽可能少的了解系统的其他部分。当发生变化时,需要了解这一变化的对象就会比较少,这会使变化比较容易进行。 - 如果
A对象
通过B对象
得到C对象
,然后调用C对象
的函数,那么客户就必须知道这一层委托关系
,这时可以在B对象
中创建一个委托函数
,使得A对象
和B对象
建立关系,B对象
和C对象
建立关系,但是A对象
和C对象
永远不建立关系。这样就可以屏蔽变化。
-
- 做法:在服务类上建立客户所需的所有函数,用以隐藏委托关系。
- 缺点:每当客户需要使用受托类的新特性时,就必须在服务端添加一个
委托函数
,随着受托类
的特性(功能)
越来越多,这个过程会很痛苦。
7.6 Remove Middle Man(移除中间人)
- 场景:某个类做了过多简单的委托动作。
- 动机:当
委托函数
越来越多时,变得难以维护,这时就需要去掉委托函数,直接调用另一个对象。是隐藏“委托关系”
的逆过程。 - 做法:让客户直接调用受托类
7.7 Introduce Foreign Method(引入外加函数)
对源码根据自己的需要进行在封装,自己就新封装一个函数,传入已有的对象。
7.8 Introduce Local Extention(引入本地扩展)
- 场景:需要为服务类提供一些额外函数,但是无法修改这个类
- 动机:源码里面的函数不满足自己的条件,自己必须扩展,而且扩展的函数多余
2个
(少于2个
,使用引入外加函数
的重构手法),为了满足函数和数据应该被统一封装
的原则,就需要新建一个类,来专门管理这些方法和数据。
转型构造函数:
接受原对象作为参数
的构造函数
1.采用子类化方案:转型构造函数应该调用适当的超类构造函数
2.采用包装类方案:转型构造函数应该将他得到的传入参数以实例变量的形式保存起来,用作接受委托的原对象。
- 做法:新建一个类,使它包含这些额外函数,让这个扩展品成为源类的
子类
或者包装类
。- 对于
子类
和包装类
的选择:优先选择子类
,因为这样工作量比较少。 -
包装类
的使用时机:子类会存在对象创建的问题,使得创建多个对象,会造成数据不同步的问题,使用包装类
就不存在这个问题。使用包装类
时,对本地扩展的修改会波及原对象,反之亦然。
- 对于
chapter 8 重新组织数据
8.1 Self Encapsulate Field(自封装字段)
即常说的把字段值的修饰符改为private
修饰,提供访问他们的方法
- 好处:子类可以复写这些取值/设值方法,为这些值提供新的数据约束。
8.2 Replace Data value with Object (以对象取代数值项)###
- 场景:有一个数据项,需要和其他数据和行为一起使用才有意义。
- 动机:随着业务的增多,需要多个
数据项
来表征一个事物,这时就需要构建一个新的对象来管理代码,将相关数据和业务操作封装到一个类中,防止重复代码
。 - 做法:将数据项变成对象。
8.3 Change Value to Reference(将值对象改为引用对象)
- 场景:一个类有许多彼此相等的实例,希望将他们替换为同一个对象,比如需要控制类的实例,如一个客户对应一个对象,这时需要使用
工厂方法
。
8.4 Change Reference to Value(将引用对象改为值对象)
- 场景:某个引用对象很小且不可变,而且不易管理
- 动机:
引用对象
必须被某种方式控制,会造成内存区域之间错中复杂的关系,而不可变的值对象就不用考虑同步问题。 - 注意事项:把一个
引用对象
改成值对象
,关键在于:检查它是否不可变。如果不是,就不能使用本项重构,因为可变的值对象会造成烦人的别名问题。
8.5 Replace Array with Object(以对象取代数组)
- 场景:数组中的元素各自代表不同的东西。
- 动机:一个数组容纳了多种不同对象,没有见名知意。
- 做法:以对象替换数组,对于数组中的每个元素,以一个字段来表示。
8.6 Duplicate Observed Data(复制“被监视数据”)
- 场景:有一些领域数据置身于GUI控件中,而领域函数需要访问这些数据。
- 动机:
- 做法:将该数据复制到一个领域对象中,建立一个
Observer模式
,用以同步领域对象和GUI对象内的重复数据。
8.7 change UniDirectional Association to Bidirectional (将单向关联改为双向关联)
- 场景:两个类需要使用对方的特性,但是其间只有一条单向连接。
- 动机:
- 做法:添加一个反向指针,并使修改双方关系的函数能同时更新两条连接。
8.8 Change Bidirectional Assocition to Unidirectional (将双向关联改为单向关联)
8.9 Replace Magic Number with Symbolic Constant (以字面常量取代魔法数)
魔法数字:拥有特殊含义,却又不能明确表现这种意义的数字。
- 场景:一个字面数值,带有特别含义
- 动机:
- 常量不会造成任何性能开销;
- 如果需要在多个地方引用同一个逻辑数,一旦这个数字发生改变,修改起来就很麻烦。
- 做法:创造一个常量,根据其意义为他命名,并将上述字面数值替换为这个常量
8.10 Encapsulate Field(自封装字段)
- 场景:类中存在一个
public
修饰的字段 - 做法:将它申明为
private
,并提供相应的访问函数。
8.11 Encapsulate Collection(封装集合) 【donnot understand】###
- 场景:有个函数返回一个集合。
- 动机:
- 取值函数不应该返回集合自身,因为这会让用户得以修改集合内容而集合拥有者却一无所知。
- 这也会对用户暴露过多对象内部数据结构的信息。
- 当一个取值函数确实需要返回多个值,一定要避免用户直接操作对象内所保存的集合,而且一定要隐藏对象内与用户无关的数据结构。
- 注意事项:不应该为这个集合提供
设值函数
; - 做法:让这个函数返回该集合的一个只读副本,并在这个类中提供
添加/移除
集合元素的函数。
8.12 replace record with Data class(以数据类取代记录)###
8.13 replace Type code with class(以类取代类型码)###
- 场景:类中有一个数值类型码,但它并
不影响类的行为
(实际上就是一系列自定义常量) - 动机:
- 类型码本质上是个数值,编译器无法进行类型检查,这会成为潜在bug。
- 做法:以一个新的类替换该数值类型码
8.14 replace Type code with subclass (以子类取代类型码)
- 场景:一个不可变的类型码,它会影响类的行为。
- 动机:
- 类型码会影响宿主类的行为,那么最好用多态来处理变化行为。
- 宿主类中出现了“只与具备特定类型码之对象相关”的特性。
- 做法:以子类取代这个类型码
- 使用工厂方法来替换构造函数。
- 为类型码的每一个数值新建一个相应的子类。
8.15 replace type code with state/strategy(以state/strategy取代类型码)
- 场景:有一个类型码,会影响类的行为,但你无法通过继承手法消除它。
8.16 Replace Subclass with Fields(以字段取代子类)
- 场景:各个子类的唯一差别只在“返回常量数据”的函数身上
- 动机:
- 做法:修改这些函数,使他们返回超类中的某个新增字段,然后销毁子类
chapter 9 简化条件表达式
9.1 Decopose Conditional (分解条件表达式)
- 场景:有一个复杂的条件(if-then-else)语句
- 动机:代码阅读性差,维护困难
- 做法:从
if
,then
,else
三个段落中分别提取出独立函数
9.2 Consolidate Conditional Expression(合并条件表达式)
- 场景:有一系列条件测试,都得到相同结果
- 动机:
- 合并后的条件代码表示只有一次检查,只不过多个并列条件需要检查而已。而原先的代码表示这里有一些各自独立的条件测试,他们只是恰好同时发生而已。
- 这项重构为
Extract Method
做好准备。将检查条件提炼成一个独立函数,使得代码逻辑更为清晰。
- 做法:将这些测试条件合并为一个条件表达式,并将这个表达式提炼到一个独立的函数中。
- 注意:并不是所有的条件表达都需要合并,如果你认为这些检查的确彼此独立,不应该视为一次检查,那就不需要使用本次重构。
9.3 Consolidate Duplicate Conditional Fragments(合并重复的条件片段)
- 场景:在条件表达式的每个分支上有着相同的一段代码。
- 动机:清晰那些东西随条件变化而变化,哪些东西保持不变。
- 做法:将这段重复代码搬迁到条件表达式之外。
9.4 Remove Control Flag(移除控制标记)
- 场景:在一系列布尔表达式中,某个变量带有“控制标记(control flag)”的作用
- 动机:这种控制标记带来的麻烦超过了它所带来的便利,大大降低了代码的条件表达式可读性。
- 做法:以
break
,return
,continue
语句取代控制标记。- 将这段逻辑提炼到一个独立的函数中,然后使用
return
替换标记值,也可以用return
返回标记值。
- 将这段逻辑提炼到一个独立的函数中,然后使用
9.5 Replace Nested Conditonal with Guard Clauses(以卫语句取代嵌套条件表达式)
- 场景:函数中的条件逻辑嵌套过多使人难以看清正常的执行路径。
- 动机:如果
if-else-if
中的分支表达并不是同等概率出现的,那么会影响我们对代码的理解。 - 做法:使用卫语句表现所有特殊情况。(对于不经常出现的情况单独进行检查)
9.6 Replace Conditional with Polymorphism (用多态取代条件表达式)###
- 场景:一个条件表达式,它根据对象类型的不同而选择不同的行为。
- 动机:
- 做法:将这个条件表达式的每一个分支放进一个子类内的复写函数中,然后将原始函数申明为抽象。
9.7 Introduce Null Object(引入null对象)
- 场景:你需要再三检查某对象是否为null
- 动机:
- 做法:将null值替换为null对象。
9.8 Introduce Assertion(引入断言)
- 场景:某一段代码需要对程序状态做出某种假设。
- 做法:以断言明确表现这种假设。
chapter 10 简化函数调用
10.1 Rename Method(函数改名)
- 场景:函数名称未能揭示函数的用途
- 动机:函数的名称应该准确的表达它的用途。
- 做法:首先考虑应该给这个函数加上一句怎样的注释,然后想办法将注释变成函数名称。
10.2 Add parameter(添加参数)
- 场景:为此函数添加一个对象参数,让该对象带进函数所需信息
- 动机:
- 做法:
10.3 Remove Parameter(移除参数)
- 场景:函数本体不再需要某个参数
- 动机:
- 做法:将该参数移除
10.4 Separate Query from Modefier(将查询函数和修改函数分离)
- 场景:某个函数既返回对象状态值,又修改对象状态。
- 动机:任何一个函数都应该没有副作用,单一职责原则。
- 做法:建立两个不同的函数,其中一个负责查询,另一个负责修改。
10.5 Parameterize Method (令函数携带参数)
- 场景:若干个函数做了类似的工作,但是在函数本体里却包含了不同的值。
- 动机:提高代码复用性,灵活性,方便扩展和维护。
- 做法:建立单一函数,以参数表达那些不同的值。
10.6 Replace Parameter with Explicit Methods(以明确函数取代参数)【此项重构和上一条相反】
- 场景:一个函数完全取决于参数值而采取不同的行为,即函数内部根据不同的条件表达式做出不同的行为。
- 动机:可以避免条件表达式,防止不合法的参数,同时使得代码更加清晰。
- 做法:针对该函数的每一个可能值,建立一个独立的函数。
10.7 Preserve Whole Object(保持对象完整)
- 场景:从某个对象中取出若干值,将他们作为某一次参数调用的参数。
- 动机:
- 防止调用函数需要新的数据项,就必须查找并修改对此函数的所有调用的情况发生;
- 代码可读性变高,防止过长的参数列表。
- 减少重复代码。
- 缺点:会使依赖结构恶化。
- 做法:传递整个对象。
10.8 Replace Parameter with Method(以函数取代参数)
- 场景:对象调用某个函数,并将所得结果作为参数传递给另一个函数,而接受该参数的函数本身也能够调用前一个函数。
- 动机:
- 缩短参数列表;
- 做法:让参数接受者去除该项参数,并直接调用前一个参数。
10.9 Introduce Parameter Object(引入对象参数)
- 场景:某些参数总是同时出现
- 动机:
- 缩短参数列表,防止数据泥团(data clumps);
- 降低代码的理解和维护难度。
- 做法:以一个对象取代这些参数。
10.10 Remove Setting Method(移除设值函数)
- 场景:类中某个字段应该在对象创建时被设值,然后就不再改变。
- 动机:
- 如果为某个字段提供设置函数,就暗示这个字段可以被改变。
- 如果你不希望在对象创建之后此字段还有机会改变,就不要提供设置函数,并将其声明为
final
;
- 做法:去掉该字段的所有设值函数。
10.11 Hide Method (隐藏函数)
- 场景:一个函数从没有被其他任何类用到。
- 动机:
- 做法:将这个函数修改为
private
修饰;
10.12 replace Constructor with Factory Method(以工厂方法取代构造函数)
- 场景:在创建对象时不仅仅是做简单的建构动作。
- 动机:
- 在派生子类的时候以工厂函数取代类型码。
- 完成构造函数完成不了的功能,实现不同的创建行为。
- 做法:将构造函数替换为工厂函数。
10.13 Encapsulate Downcast(封装向下转型)
- 场景:某个函数返回的对象,需要由函数调用者执行向下转型。
- 动机:
- 转型一般是由编译器来完成的,在强类型语言中,代替了编译器。
- 应该为函数调用者提供准确的函数类型。
- 做法:将向下转型的动作移到函数中。
10.14 Replace Error Code with Exception (以异常取代错误码)
- 场景:某个函数返回一个特定的代码,用以表示某种错误情况。
- 动机:
- 程序中发现错误的地方,并不一定知道如何处理,而且还要告诉调用者。
- 做法:改用异常。
10.15 Replace Exception with Test(以测试(条件表达式)取代异常)
- 场景:面对一个调用者可以预先检查的条件,抛出了一个异常。
- 动机:"异常"只应该被用于罕见的、异常的行为,也即那些意料之外的错误行为,而不应该成为条件表达式的替代品。
- 做法:修改调用者,使它在调用函数之前先做检查。
chapter 11 处理概括关系(继承关系)
11.1 Pull Up Field(字段上移)
- 场景:两个子类拥有相同的字段(判断字段是否重复,主要是观察函数如何使用他们)
- 动机:
- 去除重复数据申明;
- 去除重复的行为;
- 做法:将该字段移至超类
11.2 Pull Up Method (函数上移)
- 场景:有些函数,在各个子类中产生完全相同的结果
- 动机:
- 避免行为重复,重复代码
- 提高代码可维护性
- 做法:将该函数移动至超类
11.3 Pull Up Constrcutor Body (构造函数本体上移)
- 场景:各个子类中拥有一个构造函数,并且本体几乎完全一致。
- 动机:
- 做法:在超类中新建一个构造函数,并在子类构造函数中调用她。
11.4 Push Down Method (函数下移)
- 场景:超类中的某个函数只与部分子类有关
- 动机:
- 做法:将这些函数移动到相关的子类中去。
11.5 Push Down Field (字段下移)
- 场景:超类中某个字段只被部分子类用到
- 动机:
- 做法:将这个字段移到需要它的那些子类中去。
11.6 Extract Subclass(提炼子类)
- 场景:类中某些特性只被某些实例用到
- 动机:某些行为只被一部分实例用到,其他实例不需要他们,这些行为一般是通过类型码来区分的。
- 做法:新建一个子类,将上面所说的那一部分特性移到子类中去。
11.7 提炼超类
- 场景:两个类有相似的特性
- 动机:去掉重复代码,提高系统可维护性
- 做法:为这两个类建立起一个超类,将相同特性迁移至超类
11.8 Extract Interface(提炼接口)
- 场景:若干客户使用类接口中的同一子集,或者两个类的接口有部分相同
- 动机:将各个类的责任划分清晰,看清系统的责任划分
- 做法:将相同的子集提炼到一个独立的接口中
11.9 Collapse Hierarchy(折叠继承关系)
- 场景:超类和子类之间无太大区别
- 动机:防止继承体系过于复杂。
- 做法:将他们合为一体
11.10 Form Template Method (塑造模板函数)
- 场景:一些子类,其中相应的某些函数以相同顺序执行类似的操作,但各个操作细节的细节上有所不同。
- 动机:继承可以避免大量重复代码;当两个函数以相同顺序执行大致相近操作,而各操作各不完全相同,此时可以将执行操作的顺序移动至父类,并借助多态保证各操作得以保持差异性。
- 做法:将这些操作分别放进独立的函数中,并保持他们都有相同的签名,于是原函数就变得相同了。然后将原函数上移至超类。
11.11 Replace Inheritance with Delegation(以委托取代继承)
- 场景:某个子类只使用超类接口的一部分,或根本不需要继承而来的数据
- 动机:
- 超类中的许多操作并不真正适合子类;或者,从超类中继承了子类不需要的数据;或者,超类中某些
protected
函数对子类并没有什么意义。 -
**委托**
可以清楚的表明你只需要受托类的一部分功能;
- 超类中的许多操作并不真正适合子类;或者,从超类中继承了子类不需要的数据;或者,超类中某些
- 做法:在子类中新建一个字段用来保存超类;调整子类函数,令它改而继承超类,然后去掉二者之间的继承关系。
11.12 Replace Delegation with Inheritance(以继承取代委托)##
- 场景:在两个类之间使用委托关系,并经常为整个接口编写许多极简单的委托函数;
- 动机:
- 做法:让委托类继承受托类
- 注意:
- 如果没有使用受托类的所有函数,就不应该使用该重构手法;
- 受托对象不止一个其他共享对象,而且受托对象是可变的,此时也不可以使用该重构手法。
chapter 12 大型重构
12.1 Tease Apart Inheritance(梳理并分解继承体系)
12.2 Convert Procedural Design to Objects(将过程设计)
12.3 Separate Domain from Presentation(将领域和表述/显示分离)
- 场景:某些GUI 类中包含了领域逻辑
- 动机:
- 做法:将领域逻辑分离出来,为他们建立独立的函数
12.4 Extract Hierarchy(提炼继承体系)
- 场景:某个类做了太多工作,其中一部分工作是以大量条件表达式完成的。
- 动机:
- 做法:建立继承体系,以一个子类表示一种特殊情况