第七章:封装
1. 封装记录(Encapsulate Record)
-
曾用名:以数据类取代记录(Replace Record withData Class)
-
时机:
- 记录型结构能直观地组织起存在关联的数据,但无法清晰地区分“记录中存储的数据”和“通过计算得到的数据”。
- 相比记录型结构,类对象可以隐藏结构的细节,用户不用关心细节。
- 记录型结构一条记录上持有什么字段不够直观。
- 做法:
- 对持有记录的变量使用封装变量,将其封装到一个函数中。
- 创建一个类,将记录包装起来,并将记录变量的值替换为该类的一个实例。然后在类上定义一个访问函数,用于返回原始的记录。修改封装变量的函数,令其使用这个访问函数。
- 测试。
- 新建一个函数,让它返回该类的对象,而非那条原始的记录。
- 对于该记录的每处使用点,将原先返回记录的函数调用替换为那个返回实例对象的函数调用。使用对象上的访问函数来获取数据的字段,如果该字段的访问函数还不存在,那就创建一个。每次更改之后运行测试。
- 关键字:
转化函数、取值函数、设值函数、替换调用者、替换设置者
2. 封装集合(EncapsulateCollection)
- 时机
- 只对集合变量的访问进行了封装,但依然让取值函数返回集合本身。这使得集合的成员变量可以直接被修改,而封装它的类则全然不知,无法介入。
- 以某种形式限制集合的访问权,只允许对集合进行读操作。
- 做法
- 如果集合的引用尚未被封装起来,先用封装变量封装它。
- 在类上添加用于“添加集合元素”和“移除集合元素”的函数。
- 执行静态检查。
- 查找集合的引用点。如果有调用者直接修改集合,令该处调用使用新的添加/移除元素的函数。每次修改后执行测试。
- 修改集合的取值函数,使其返回一份只读的数据,可以使用只读代理或数据副本。
- 测试。
- 关键字:
取值函数、返回集合本身、访问权、封装变量
3. 以对象取代基本类型(Replace Primitive with Object)
-
曾用名:以对象取代数据值(Replace Data Value with Object)
-
曾用名:以类取代类型码(Replace Type Code with Class)
-
时机
- 随着迭代一些简单数据项不再那么简单,它可能还要肩负一些其他的职责,如比较、值行为等。持续下去会导致相同的逻辑处理增多,代码重复。
- 做法
- 如果变量尚未被封装起来,先使用封装变量封装它。
- 为这个数据值创建一个简单的类。类的构造函数应该保存这个数据值,并为它提供一个取值函数。
- 执行静态检查。
- 修改第一步得到的设值函数,令其创建一个新类的对象并将其存入字段,如果有必要的话,同时修改字段的类型声明。
- 修改取值函数,令其调用新类的取值函数,并返回结果。
- 测试。
- 考虑对第一步得到的访问函数使用函数改名,以便更好反映其用途。
- 考虑应用将引用对象改为值对象或将值对象改为引用对象,明确指出新对象的角色是值对象还是引用对象。
- 关键字:
数据项、不再简单、类
4. 以查询取代临时变量(Replace Temp with Query)
- 时机
- 分解一个冗长的函数,将变量抽取到函数里能使函数的分解过程更简单。
- 将变量的计算逻辑放到函数中,也有助于在提炼得到的函数与原函数之间设立清晰的边界,这能帮我发现并避免难缠的依赖及副作用。
- 避免在多个函数中重复编写计算逻辑。
- 做法
- 检查变量在使用前是否已经完全计算完毕,检查计算它的那段代码是否每次都能得到一样的值。
- 如果变量目前不是只读的,但是可以改造成只读变量,那就先改造它。
- 测试。
- 将为变量赋值的代码段提炼成函数。
- 测试。
- 应用内联变量手法移除临时变量。
- 关键字:
冗长、提炼函数、删变量
5. 提炼类(Extract Class)
-
反向重构:内联类
-
时机
- 随着迭代类的责任不断增加,类变得过分复杂。(这个类不纯洁了)
- 做法
- 决定如何分解类所负的责任。
- 创建一个新的类,用以表现从旧类中分离出来的责任。
- 构造旧类时创建一个新类的实例,建立“从旧类访问新类”的连接关系。
- 对于你想搬移的每一个字段,运用搬移字段搬移之。每次更改后运行测试。
- 使用搬移函数将必要函数搬移到新类。先搬移较低层函数(也就是“被其他函数调用”多于“调用其他函数”者)。每次更改后运行测试。
- 检查两个类的接口,去掉不再需要的函数,必要时为函数重新取一个适合新环境的名字。
- 决定是否公开新的类。如果确实需要,考虑对新类应用将引用对象改为值对象(252)使其成为一个值对象。
- 关键字:
责任不断增加、不易理解、搬移字段、搬移函数、接口删除
6. 内联类(Inline Class)
-
反向重构:提炼类
-
时机
- 一个类不再承担足够责任,不再有单独存在的理由。
- 做法
- 对于待内联类(源类)中的所有 public 函数,在目标类上创建一个对应的函数,新创建的所有函数应该直接委托至源类。
- 修改源类 public 方法的所有引用点,令它们调用目标类对应的委托方法。每次更改后运行测试。
- 将源类中的函数与数据全部搬移到目标类,每次修改之后进行测试,直到源类变成空壳为止。
- 删除源类,为它举行一个简单的“丧礼”
- 关键字:
萎缩类、提炼类、方法搬家、抛弃旧类
7. 隐藏委托关系(Hide Delegate)
-
反向重构:移除中间人
-
时机
- 一个类需要隐藏其背后的类的方法或事件。
- 一个客户端调用类的方法时候,必须知道隐藏在后面的委托关系才能调用。
- 做法
- 对于每个委托关系中的函数,在服务对象端建立一个简单的委托函数。
- 调整客户端,令它只调用服务对象提供的函数。每次调整后运行测试。
- 如果将来不再有任何客户端需要取用 Delegate (受托类),便可移除服务对象中的相关访问函数。
- 测试。
- 关键字
委托函数、替换调用者、删除委托整个类
8. 移除中间人(Remove MiddleMan)
-
反向重构:隐藏委托关系
-
时机
- 服务类完全变成了一个中间人,此时应该让客户直接调用受托类。
- 做法
- 为受托对象创建一个取值函数。
- 对于每个委托函数,让其客户端转为连续的访问函数调用。每次替换后运行测试。
- 关键字
中间人、直接调用受托类、隐藏委托关系、删除中间人
9. 替换算法(Substitute Algorithm)
- 时机
- 旧算法已经不满足当前功能。
- 有更好的方式可以完成与旧算法相同的事情(通常是因为优化)。
- 做法
- 整理一下待替换的算法,保证它已经被抽取到一个独立的函数中。
- 先只为这个函数准备测试,以便固定它的行为。
- 准备好另一个(替换用)算法。
- 执行静态检查。
- 运行测试,比对新旧算法的运行结果。如果测试通过,那就大功告成;否则,在后续测试和调试过程中,以旧算法为比较参照标准。
第八章:搬移特性
1. 搬移函数(Move Function)
-
曾用名:搬移函数(Move Method)
-
时机
- 随着对代码的理解加深,知道那些软件要素如何组织最为恰当。要将这种理解反映到代码上,就得不断地搬移这些元素。
- 一个模块频繁引用其他上下文中的元素,而对自身上下文中的元素却关心甚少。
- 一个函数在发展过程中,现在它已经有了更通用的场景。
- 做法
- 检查函数在当前上下文里引用的所有程序元素(包括变量和函数),考虑是否需要将它们一并搬移。
- 检查待搬移函数是否具备多态性。
- 将函数复制一份到目标上下文中。调整函数,使它能适应新家。
- 执行静态检查。
- 设法从源上下文中正确引用目标函数。
- 修改源函数,使之成为一个纯委托函数。
- 测试。
- 考虑对源函数使用内联函数。
- 关键字
确定关系、确定继承、函数搬家、优化新函数
2. 搬移字段(Move Field)
- 时机
- 坏的数据结构本身掩藏了程序真实意图。
- 数据结构已经不适应于需求。
- 每当调用某个函数时,除了传入一个记录参数,还总是需要同时传入另一条记录的某个字段一起作为参数。
- 一同出现、一同作为函数参数传递的数据,最好是规整到同一条记录中,以体现它们之间的联系。
- 如果修改一条记录时,总是需要同时改动另一条记录,那么说明很可能有字段放错了位置。
- 如果我更新一个字段时,需要同时在多个结构中做出修改,那也是一个征兆,表明该字段需要被搬移到一个集中的地点,这样每次只需修改一处地方。
- 做法
- 确保源字段已经得到了良好封装。
- 测试。
- 在目标对象上创建一个字段(及对应的访问函数)。
- 执行静态检查。
- 确保源对象里能够正常引用目标对象。
- 调整源对象的访问函数,令其使用目标对象的字段。
- 测试。
- 移除源对象上的字段。
- 测试。
3. 搬移语句到函数(Move Statements into Function)
-
反向重构:搬移语句到调用者
-
时机
- 如果发现调用某个函数时,总有一些相同的代码也需要每次执行,那么我会考虑将此段代码合并到函数里头。
- 如果某些语句与一个函数放在一起更像一个整体,并且更有助于理解。
- 做法
- 如果重复的代码段离调用目标函数的地方还有些距离,则先用移动语句将这些语句挪动到紧邻目标函数的位置。
- 如果目标函数仅被唯一一个源函数调用,那么只需将源函数中的重复代码段剪切并粘贴到目标函数中即可,然后运行测试。本做法的后续步骤至此可以忽略。
- 如果函数不止一个调用点,那么先选择其中一个调用点应用提炼函数,将待搬移的语句与目标函数一起提炼成一个新函数。给新函数取个临时的名字,只要易于搜索即可。
- 调整函数的其他调用点,令它们调用新提炼的函数。每次调整之后运行测试。
- 完成所有引用点的替换后,应用内联函数将目标函数内联到新函数里,并移除原目标函数。
- 对新函数应用函数改名,将其改名为原目标函数的名字。
- 关键字
消除重复、代码靠近、移动语句、提炼函数、内联函数
4. 搬移语句到调用者(Move Statements to Callers)
-
反向重构:搬移语句到函数
-
时机
- 随着系统能力发生演进(通常只要是有用的系统,功能都会演进),原先设定的抽象边界发生偏移,出现多个不同的关注点。
- 以往在多个地方共用的行为,如今需要在某些调用点面前表现出不同的行为。
- 做法
- 最简单的情况下,原函数非常简单,其调用者也只有寥寥一两个,此时只需把要搬移的代码从函数里剪切出来并粘贴回调用端去即可,必要的时候做些调整。运行测试。如果测试通过,那就大功告成,本手法可以到此为止。
- 若调用点不止一两个,则需要先用提炼函数将你不想搬移的代码提炼成一个新函数,函数名可以临时起一个,只要后续容易搜索即可。
- 对原函数应用内联函数。
- 对提炼出来的函数应用改变函数声明(124),令其与原函数使用同一个名字。如果你能想到更好的名字,那就用更好的那个。
- 关键字
边界偏移、移动语句、提炼函数
5. 以函数调用取代内联代码(Replace Inline Code with Function Call)
- 时机
- 如果一些内联代码,它们做的事情仅仅是已有函数的重复,可以用一个函数调用取代内联代码。
- 做法
- 将内联代码替代为对一个既有函数的调用。
- 测试。
- 关键词
内联替换
6. 移动语句(Slide Statements)
-
曾用名:合并重复的代码片段(Consolidate Duplicate Conditional Fragments)
-
时机
- 如果有几行代码取用了同一个数据结构,那么最好是让它们在一起出现,而不是夹杂在取用其他数据结构的代码中间。
- 做法
- 确定待移动的代码片段应该被搬往何处。仔细检查待移动片段与目的地之间的语句,看看搬移后是否会影响这些代码正常工作。如果会,则放弃这项重构。
- 剪切源代码片段,粘贴到上一步选定的位置上。
- 测试。
- 关键词
移动语句、确定目标
7. 拆分循环(Split Loop)
- 时机
- 如果一次循环中做了两件不同的事,每当需要修改循环时,都得同时理解这两件事情。需要将循环拆分。
- 做法
- 复制一遍循环代码。
- 识别并移除循环中的重复代码,使每个循环只做一件事。
- 测试。
- 关键词
复制循环、行为拆分、函数提炼
8. 以管道取代循环(Replace Loop with Pipeline)
- 时机
- 一组虽然在做相同事情的循环,但是内部过多的处理逻辑,使其晦涩难懂。
- 做法
- 创建一个新变量,用以存放参与循环过程的集合。
- 从循环顶部开始,将循环里的每一块行为依次搬移出来,在上一步创建的集合变量上用一种管道运算替代之。每次修改后运行测试。
- 搬移完循环里的全部行为后,将循环整个删除。
- 关键字:
新变量、合适的管道、删除整个循环
9. 移除死代码(Remove Dead Code)
- 时机
- 有不再使用的代码。应该立马删除。
- 做法
- 如果死代码可以从外部直接引用,比如它是一个独立的函数时,先查找一下还有无调用点。
- 将死代码移除。
- 测试。
第九章:重新组织数据
1. 拆分变量(Split Variable)
-
曾用名:移除对参数的赋值(Remove Assignments to Parameters)
-
曾用名:分解临时变量(Split Temp)
-
时机
- 一个变量被应用到两种/多种的作用下。
- 如果变量承担多个责任,它就应该被替换(分解)为多个变量,每个变量只承担一个责任。
- 做法
- 在待分解变量的声明及其第一次被赋值处,修改其名称。
- 如果可能的话,将新的变量声明为不可修改。
- 以该变量的第二次赋值动作为界,修改此前对该变量的所有引用,让它们引用新变量。
- 测试。
- 重复上述过程。每次都在声明处对变量改名,并修改下次赋值之前的引用,直至到达最后一处赋值。
2. 字段改名(Rename Field)
- 时机
- 记录结构需要改名。
- 做法
- 如果记录的作用域较小,可以直接修改所有该字段的代码,然后测试。后面的步骤就都不需要了。
- 如果记录还未封装,请先使用封装记录。
- 在对象内部对私有字段改名,对应调整内部访问该字段的函数。
- 测试。
- 如果构造函数的参数用了旧的字段名,运用改变函数声明将其改名。
- 运用函数改名给访问函数改名。
3. 以查询取代派生变量(Replace Derived Variable with Query)
- 时机
- 有些变量其实可以很容易地计算出来。用查询取代派生变量。
- 做法
- 识别出所有对变量做更新的地方。如有必要,用拆分变量分割各个更新点。
- 新建一个函数,用于计算该变量的值。
- 用引入断言(302)断言该变量和计算函数始终给出同样的值。
- 测试。
- 修改读取该变量的代码,令其调用新建的函数。
- 测试。
- 用移除死代码去掉变量的声明和赋值。
4. 将引用对象改为值对象(Change Reference to Value)
-
反向重构:将值对象改为引用对象
-
时机
- 如果将内部对象视为引用对象,在更新其属性时,保留原对象不动,更新内部对象的属性;
- 如果将其视为值对象,替换整个内部对象,新换上的对象会有想要的属性值。
值对象:a.b=new b(1)
引用对象:a.b.c=1
- 做法
- 检查重构目标是否为不可变对象,或者是否可修改为不可变对象。
- 用移除设值函数逐一去掉所有设值函数。
- 提供一个基于值的相等性判断函数,在其中使用值对象的字段。
- 关键字:
不可变、替换设置引用值为设置值
5. 将值对象改为引用对象(Change Value to Reference)
-
反向重构:将引用对象改为值对象
-
时机
- 数据副本在多处使用,并且需要一处变化其他地方同步更新。
- 做法
- 为相关对象创建一个仓库(如果还没有这样一个仓库的话)。
- 确保构造函数有办法找到关联对象的正确实例。
- 修改宿主对象的构造函数,令其从仓库中获取关联对象。每次修改后执行测试。
第十章:简化条件逻辑
1. 分解条件表达式(Decompose Conditional)
- 时机
- 条件逻辑内,过长的函数,导致难以理解条件逻辑的场景。
- 单个条件逻辑处理的函数过大。
- 做法
- 对条件判断和每个条件分支分别运用提炼函手法。
2. 合并条件表达式(Consolidate Conditional Expression)
- 时机
- 一串条件检查:检查条件各不相同,最终行为却一致。应该使用“逻辑或”和“逻辑与”将它们合并为一个条件表达式。
- 做法
- 确定这些条件表达式都没有副作用。
- 使用适当的逻辑运算符,将两个相关条件表达式合并为一个。
- 测试。
- 重复前面的合并过程,直到所有相关的条件表达式都合并到一起。
- 可以考虑对合并后的条件表达式实施提炼函数。
3. 以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)
- 如果两条分支都是正常行为,就应该使用形如if…else… 的条件表达式;
- 如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。
- 做法
- 选中最外层需要被替换的条件逻辑,将其替换为卫语句。
- 测试。
- 有需要的话,重复上述步骤。
- 如果所有卫语句都引发同样的结果,可以使用合并条件表达式合并之。
4. 以多态取代条件表达式(Replace Conditional with Polymorphism)
- 时机
- 复杂的条件逻辑难以理解。
- 做法
- 如果现有的类尚不具备多态行为,就用工厂函数创建之,令工厂函数返回恰当的对象实例。
- 在调用方代码中使用工厂函数获得对象实例。
- 将带有条件逻辑的函数移到超类中。
- 任选一个子类,在其中建立一个函数,使之覆写超类中容纳条件表达式的那个函数。将与该子类相关的条件表达式分支复制到新函数中,并对它进行适当调整。
- 重复上述过程,处理其他条件分支。
- 在超类函数中保留默认情况的逻辑。或者,如果超类应该是抽象的,就把该函数声明为 abstract ,或在其中直接抛出异常,表明计算责任都在子类中。
5. 引入特例(Introduce SpecialCase)
-
曾用名:引入Null对象(Introduce Null Object)
-
时机
- 数据结构的调用者都在检查某个特殊值,并且这个值每次所做的处理也都相同。
- 多处以同样方式应对同一个特殊值
- 做法
从一个作为容器的数据结构(或者类)开始,其中包含一个属性,该属性就是我们要重构的目标。容器的客户端每次使用这个属性时,都需要将其与某个特例值做比对。我们希望把这个特例值替换为代表这种特例情况的类或数据结构。
- 给重构目标添加检查特例的属性,令其返回 false 。
- 创建一个特例对象,其中只有检查特例的属性,返回true。
- 对“与特例值做比对”的代码运用提炼函数,确保所有客户端都使用这个新函数,而不再直接做特例值的比对。
- 将新的特例对象引入代码中,可以从函数调用中返回,也可以在变换函数中生成。
- 修改特例比对函数的主体,在其中直接使用检查特例的属性。
- 测试。
- 使用函数组合成类或函数组合成变换,把通用的特例处理逻辑都搬移到新建的特例对象中。
- 对特例比对函数使用内联函数,将其内联到仍然需要的地方。
6. 引入断言(Introduce Assertion)
- 时机
- 断言是一个条件表达式,应该总是为真。如果它失败,表示程序员犯了错误。断言的失败不应该被系统任何地方捕捉。
- 做法
- 如果你发现代码假设某个条件始终为真,就加入一个断言明确说明这种情况。
因为断言应该不会对系统运行造成任何影响,所以“加
入断言”永远都应该是行为保持的。
第十一章:重构API
1. 将查询函数和修改函数分离(Separate Query from Modifier)
- 时机
- 任何有返回值的函数,都不应该有看得到的副作用——命令与查询分离。
- 做法
- 复制整个函数,将其作为一个查询来命名。
- 从新建的查询函数中去掉所有造成副作用的语句。
- 执行静态检查。
- 查找所有调用原函数的地方。如果调用处用到了该函数的返回值,就将其改为调用新建的查询函数,并在下面马上再调用一次原函数。每次修改之后都要测试。
- 从原函数中去掉返回值。
- 测试。
2. 函数参数化(ParameterizeFunction)
-
曾用名:令函数携带参数(Parameterize Method)
-
时机
- 如果我发现两个函数逻辑非常相似,只有一些字面量值不同,可以将其合并成一个函数,以参数的形式传入不同的 值,从而消除重复。
- 做法
- 从一组相似的函数中选择一个。
- 运用改变函数声明,把需要作为参数传入的字面量添加到参数列表中。
- 修改该函数所有的调用处,使其在调用时传入该字面量值。
- 测试。
- 修改函数体,令其使用新传入的参数。每使用一个新参数都要测试。
- 对于其他与之相似的函数,逐一将其调用处改为调用已经参数化的函数。每次修改后都要测试。
3. 移除标记参数(Remove Flag Argument)
-
曾用名:以明确函数取代参数(Replace Parameter with Explicit Methods)
-
时机
- 当标记参数让人难以理解到底有哪些函数可以调用、应该怎么调用。
- 做法
- 针对参数的每一种可能值,新建一个明确函数。
- 对于“用字面量值作为参数”的函数调用者,将其改为调用新建的明确函数。
4. 保持对象完整(Preserve Whole Object)
- 时机
- 如果代码从一个记录结构中导出几个值,然后又把这几个值一起传递给一个函数。
- 调用者将自己的若干数据作为参数,传递给被调用函数。
- 做法
- 新建一个空函数,给它以期望中的参数列表(即传入完整对象作为参数)。
- 在新函数体内调用旧函数,并把新的参数(即完整对象)映射到旧的参数列表(即来源于完整对象的各项数据)。
- 执行静态检查。
- 逐一修改旧函数的调用者,令其使用新函数,每次修改之后执行测试。
- 所有调用处都修改过来之后,使用内联函数(115)把旧函数内联到新函数体内。
- 给新函数改名,从重构开始时的容易搜索的临时名字,改为使用旧函数的名字,同时修改所有调用处。
5. 以查询取代参数(Replace Parameter with Query)
-
曾用名:以函数取代参数
-
反向重构:以参数取代查询
-
时机
- 如果调用函数时传入了一个值,而这个值由函数自己来获得也是同样容易,这就是重复。
- 做法
- 如果有必要,使用提炼函数将参数的计算过程提炼到一个独立的函数中。
- 将函数体内引用该参数的地方改为调用新建的函数。每次修改后执行测试。
- 全部替换完成后,使用改变函数声明将该参数去掉。
6. 以参数取代查询(Replace Query With Parameter)
-
反向重构:以查询取代参数
-
时机
- 引用一个全局变量,或者引用另一个我们想要移除的元素。需要将其替换为函数参数,从而将处理引用关系的责任转交给函数的调用者。
- 如果一个函数使用了另一个元素,而后者不具引用透明性,那么包含该元素的函数也就失去了引用透明性。只要把“不具引用透明性的元素”变成参数传入,函数就能重获引用透明性。
- 做法
- 对执行查询操作的代码使用提炼变量,将其从函数体中分离出来。
- 现在函数体代码已经不再执行查询操作(而是使用前一步提炼出的变量),对这部分代码使用提炼函数。
- 使用内联变量,消除刚才提炼出来的变量。
- 对原来的函数使用内联函数。
- 对新函数改名,改回原来函数的名字。
7. 移除设值函数(Remove Setting Method)
- 时机
- 如果不希望在对象创建之后此字段还有机会被改变,那就不要为它提供设值函数(同时将该字段声明为不可变)。
- 做法
- 如果构造函数尚无法得到想要设入字段的值,就使用改变函数声明将这个值以参数的形式传入构造函数。在构造函数中调用设值函数,对字段设值。
- 移除所有在构造函数之外对设值函数的调用,改为使用新的构造函数。每次修改之后都要测试。
- 使用内联函数消去设值函数。如果可能的话,把字段声明为不可变。
- 测试。
8. 以工厂函数取代构造函数(Replace Constructor with Factory Function)
-
曾用名:以工厂函数取代构造函数(Replace Constructor with Factory Method)
-
动机
- 与一般的函数相比,构造函数常有一些丑陋的局限性,在要求普通函数的场合就难以使用。
- 做法
- 新建一个工厂函数,让它调用现有的构造函数。
- 将调用构造函数的代码改为调用工厂函数。
- 每修改一处,就执行测试。
- 尽量缩小构造函数的可见范围。
9. 以命令取代函数(Replace Function with Command)
-
曾用名:以函数对象取代函数(Replace Method With Method Object)
-
反向重构:以函数取代命令
-
时机
- 需要命令对象提供的某种能力而普通的函数无法提供这种能力时。
- 做法
- 为想要包装的函数创建一个空的类,根据该函数的名字为其命名。
- 使用搬移函数把函数移到空的类里。
- 可以考虑给每个参数创建一个字段,并在构造函数中添加对应的参数。
10. 以函数取代命令(Replace Command with Function)
-
反向重构:以命令取代函数
-
时机
- 命令对象为处理复杂计算提供了强大的机制,如果这个函数不是太复杂,那么命令对象可能显得费而不惠。应该考虑将其变回普通的函数。
- 做法
- 运用提炼函数,把“创建并执行命令对象”的代码单独提炼到一个函数中。
- 对命令对象在执行阶段用到的函数,逐一使用内联函数。
- 使用改变函数声明,把构造函数的参数转移到执行函数。
- 对于所有的字段,在执行函数中找到引用它们的地方,并改为使用参数。每次修改后都要测试。
- 把“调用构造函数”和“调用执行函数”两步都内联到调用方(也就是最终要替换命令对象的那个函数)。
- 测试。
- 用移除死代码把命令类消去。
第十二章:处理继承关系
1. 函数上移(Pull Up Method)
-
反向重构:函数下移
-
时机
- 如果某个函数在各个子类中的函数体都相同。适合函数上移。
- 如果两个函数工作流程大体相似,但实现细节略有差异,那么可以考虑先借助塑造模板函数构造出相同的函数,然后再提升它们。
- 做法
- 检查待提升函数,确定它们是完全一致的。
- 检查函数体内引用的所有函数调用和字段都能从超类中调用到。
- 如果待提升函数的签名不同,使用改变函数声明将那些签名都修改为你想要在超类中使用的签名。
- 在超类中新建一个函数,将某一个待提升函数的代码复制到其中。
- 执行静态检查。
- 移除一个待提升的子类函数。
- 测试。
- 逐一移除待提升的子类函数,直到只剩下超类中的函数为止。
2. 字段上移(Pull Up Field)
-
反向重构:字段下移
-
时机
- 子类用于重复的字段。
- 做法
- 针对待提升之字段,检查它们的所有使用点,确认它们以同样的方式被使用。
- 如果这些字段的名称不同,先使用变量改名为它们取个相同的名字。
- 在超类中新建一个字段。
- 移除子类中的字段。
- 测试。
3. 构造函数本体上移(Pull Up Constructor Body)
- 时机
- 如果各个子类中的函数有共同行为,使用提炼函数将它们提炼到一个独立函数中,然后使用函数上移将这个函数提升至超类。
- 如果重构过程过于复杂,可以使用以工厂函数取代构造函数。
- 做法
- 如果超类还不存在构造函数,首先为其定义一个。确保让子类调用超类的构造函数。
- 使用移动语句将子类中构造函数中的公共语句移动到超类的构造函数调用语句之后。
- 逐一移除子类间的公共代码,将其提升至超类构造函数中。对于公共代码中引用到的变量,将其作为参数传递给超类的构造函数。
- 测试。
- 如果存在无法简单提升至超类的公共代码,先应用提炼
函数,再利用函数上移提升之。
4. 函数下移(Push Down Method)
-
反向重构:函数上移
-
时机
- 如果超类中的某个函数只与一个(或少数几个)子类有关,那么最好将其从超类中挪走,放到真正关心它的子类中去。
- 做法
- 将超类中的函数本体复制到每一个需要此函数的子类中。
- 删除超类中的函数。
- 测试。
- 将该函数从所有不需要它的那些子类中删除。
- 测试。
5. 字段下移(Push Down Field)
-
反向重构:字段上移
-
动机
- 如果某个字段只被一个子类(或者一小部分子类)用到,就将其搬移到需要该字段的子类中。
- 做法
- 在所有需要该字段的子类中声明该字段。
- 将该字段从超类中移除。
- 测试。
- 将该字段从所有不需要它的那些子类中删掉。
- 测试。
6. 以子类取代类型码(Replace Type Code with Subclasses)
-
包含旧重构:以State/Strategy取代类型码(Replace Type Code with State/Strategy)
-
包含旧重构:提炼子类(Extract Subclass)
-
反向重构:移除子类
-
时机
- 一个类中有一些有必要的多态性被隐藏。
- 根据某个状态码来返回不同的行为。
-做法
- 自封装类型码字段。
- 任选一个类型码取值,为其创建一个子类。覆写类型码类的取值函数,令其返回该类型码的字面量值。
- 创建一个选择器逻辑,把类型码参数映射到新的子类。
- 测试。
- 针对每个类型码取值,重复上述“创建子类、添加选择器逻辑”的过程。每次修改后执行测试。
- 去除类型码字段。
- 测试。
- 使用函数下移和以多态取代条件表达式处理原本访问了类型码的函数。全部处理完后,就可以移除类型码的访问函数。
7. 移除子类(Remove Subclass)
-
曾用名:以字段取代子类(Replace Subclass with Fields)
-
反向重构:以子类取代类型码
-
时机
- 随着软件的演化,子类所支持的变化可能会被搬移到别处,甚至完全去除,这时子类就失去了价值。
- 有时添加子类是为了应对未来的功能,结果构想中的功能压根没被构造出来,或者用了另一种方式构造,使该子类不再被需要了。
- 做法
- 使用以工厂函数取代构造函数,把子类的构造函数包装到超类的工厂函数中。
- 如果有任何代码检查子类的类型,先用提炼函数把类型检查逻辑包装起来,然后用搬移函数将其搬到超类。每次修改后执行测试。
- 新建一个字段,用于代表子类的类型。
- 将原本针对子类的类型做判断的函数改为使用新建的类型字段。
- 删除子类。
- 测试。
8. 提炼超类(Extract Superclass)
- 时机
- 如果两个类在做相似的事,可以利用基本的继承机制把它们的相似之处提炼到超类。
- 做法
- 为原本的类新建一个空白的超类。
- 测试。
- 使用构造函数本体上移、函数上移和字段上移手法,逐一将子类的共同元素上移到超类。
- 检查留在子类中的函数,看它们是否还有共同的成分。如果有,可以先用提炼函数将其提炼出来,再用函数上移搬到超类。
- 检查所有使用原本的类的客户端代码,考虑将其调整为使用超类的接口。
9. 折叠继承体系(Collapse Hierarchy)
- 时机
- 在重构类继承体系时,经常把函数和字段上下移动。随着继承体系的演化,有时会发现一个类与其超类已经没多大差别,不值得再作为独立的类存在。此时可以把超类和子类合并起来。
- 做法
- 确认选择想移除的类:是超类还是子类。
- 使用字段上移、字段下移、函数上移和函数下移,把所有元素都移到同一个类中。
- 调整即将被移除的那个类的所有引用点,令它们改而引用合并后留下的类。
- 移除我们的目标;此时它应该已经成为一个空类。
- 测试。
10. 以委托取代子类(Replace Subclass with Delegate)
- 时机
- 如果一个对象的行为有明显的类别之分并且每个子类根据需要覆写部分特性。适合使用委托。
- 做法
- 如果构造函数有多个调用者,首先用以工厂函数取代构造函数把构造函数包装起来。
- 创建一个空的委托类,这个类的构造函数应该接受所有子类特有的数据项,并且经常以参数的形式接受一个指回超类的引用。
- 在超类中添加一个字段,用于安放委托对象。
- 修改子类的创建逻辑,使其初始化上述委托字段,放入一个委托对象的实例。
- 选择一个子类中的函数,将其移入委托类。
- 使用搬移函数(198)手法搬移上述函数,不要删除源类中的委托代码。
- 如果被搬移的源函数还在子类之外被调用了,就把留在源类中的委托代码从子类移到超类,并在委托代码之前加上卫语句,检查委托对象存在。如果子类之外已经没有其他调用者,就用移除死代码去掉已经没人使用的委托代码。
- 测试。
- 重复上述过程,直到子类中所有函数都搬到委托类。
- 找到所有调用子类构造函数的地方,逐一将其改为使用超类的构造函数。
- 测试。
- 运用移除死代码去掉子类。
11. 以委托取代超类(Replace Superclass with Delegate)
-
曾用名:以委托取代继承(Replace Inheritance with Delegation)
-
时机
- 错误的继承(如父子不是同一个意义的东西,但是子还想要用超类的一些字段)。
- 超类不是所有方法都适用于子类。
- 做法
- 在子类中新建一个字段,使其引用超类的一个对象,并将这个委托引用初始化为超类的新实例。
- 针对超类的每个函数,在子类中创建一个转发函数,将调用请求转发给委托引用。每转发一块完整逻辑,都要执行测试。
- 当所有超类函数都被转发函数覆写后,就可以去掉继承
关系。