重构
变量级别重构
提炼变量
-
动机
- 表达式有可能非常复杂而难以阅读。这种情况下,局部变量可以帮助我们将表达式分解为比较容易管理的形式。在面对一块复杂逻辑时,局部变量能给其中的一部分命名,这样我就能更好地理解这部分逻辑是要干什么。
- 考虑使用提炼变量,就意味着要给代码中的一个表达式命名,且这个名字要在当前函数中有意义。
-
做法
- 确认要提炼的表达式没有副作用。
- 用这个新变量取代原来的表达式。
-
idea快捷键
- 在调用的方法上option+command+V
内联变量
-
动机
- 在一个函数内部,变量能给表达式提供有意义的名字,因此通常变量是好东西。但有时候,这个名字并不比表达式本身更具表现力。还有些时候,变量可能会妨碍重构附近的代码。
-
做法
- 检查确认变量赋值语句的右侧表达式没有副作用。
- 找到第一处使用该变量的地方,将其替换为直接使用赋值语句的右侧表达式。
- 删除该变量的声明点和赋值语句。
-
idea快捷键
- 在调用的变量上option+command+N
封装变量
-
动机
- 如果我把数据搬走,就必须同时修改所有引用该数据的代码,否则程序就不能运行。如果可访问范围变大,重构的难度就会随之增大,这也是说全局数据是大麻烦的原因。
-
做法
- 创建封装函数,在其中访问和更新变量值。
- 限制变量的可见性。
- 如果变量的值是一个记录,考虑使用封装记录
变量改名
-
动机
- 好的命名是整洁编程的核心。变量可以很好地解释一段程序在干什么,使用范围越广,名字的好坏就越重要。
-
做法
- 如果变量被广泛使用,考虑运用封装变量
- 找出所有使用该变量的代码,逐一修改
-
idea快捷键
- 在变量上 shift+F6
以查询取代临时变量
-
动机
- 临时变量的一个作用是保存某段代码的返回值,以便在函数的后面部分使用它。临时变量允许我引用之前的值,既能解释它的含义,还能避免对代码进行重复计算。但尽管使用变量很方便,很多时候还是值得更进一步,将它们抽取成函数。
- 如果我正在分解一个冗长的函数,那么将变量抽取到函数里能使函数的分解过程更简单,因为不再需要将变量作为参数传递给提炼出来的小函数。将变量的计算逻辑放到函数中,也有助于在提炼得到的函数与原函数之间设立清晰的边界,这能帮助发现并避免难缠的依赖及副作用。
- 对于那些做快照用途的临时变量,不能使用本手法,因为它可能会被修改
-
做法
- 检查变量在使用前是否已经完全计算完毕,检查计算它的那段代码是否每次都 能得到一样的值。
- 如果变量目前不是只读的,但是可以改造成只读变量,那就先改造它。
- 将为变量赋值的代码段提炼成函数。
- 应用内联变量手法移除临时变量。
拆分变量
- 动机
- 变量有各种不同的用途,其中某些用途会很自然地导致临时变量被多次赋值。很多变量用于保存一段冗长代码的运算结果,以便稍后使用。这种变量应该只被赋值一次。如果它们被赋值超过一次,就意味它们在 函数中承担了一个以上的责任。如果变量承担多个责任,它就应该被替换(分解)为多个变量,每个变量只承担一个责任。同一个变量承担两件不同的事情, 会令代码阅读者糊涂。
- 做法
- 在待分解变量的声明及其第一次被赋值处,修改其名称。
- 如果可能的话,将新的变量声明为不可修改。
- 以该变量的第二次赋值动作为界,修改此前对该变量的所有引用,让它们引用 新变量。
- 重复上述过程。每次都在声明处对变量改名,并修改下次赋值之前的引用,直至到达最后一处赋值。
方法级别重构
提炼函数
-
动机
- 浏览一段代码,理解其作用,然后将其提炼到一个独立的函数中,并以这段代码的用途为这个函数命名。
-
使用场景
- 用过不止一次的代码
- 方法内长代码
- 在方法内流程内需要写注释的代码(个人理解)
-
做法
-
创造一个新函数,根据这个函数的意图来对它命名
- 以做什么来命名
-
将待提炼的代码从源函数复制到新建的目标函数中。
-
查看其他代码是否有与被提炼的代码段相同或相似之处。如果有,考虑使用以函数调用取代内联代码令其调用提炼出的新函数。
-
-
Idea快捷键
- 选定一段方法option+command+M
内联函数
-
动机
- 但有时候会遇到某些函数,其内部代码和函数名称同样清晰易读。也可能重构了该函数的内部实现,使其内容和其名称变得同样清晰。若果真如此,你就应该去掉这个函数,直接使用其中的代码。
- 手上有一群组织不甚合理的函数。可以将它们都内联到一个大型函数中,再以喜欢的方式重新提炼出小函数。
-
做法
- 检查函数,确定它不具多态性。
- 找出这个函数的所有调用点。
- 将这个函数的所有调用点都替换为函数本体。
- 删除该函数的定义。
-
idea快捷键
- 在调用的方法上option+command+N
改变函数声明
-
动机
- 一个好名字能让我们一眼看出函数的用途,而不必查看其实现代码
-
做法
- 如果想要移除一个参数,需要先确定函数体内没有使用该参数。
- 修改函数声明,使其成为你期望的状态。
- 找出所有使用旧的函数声明的地方,将它们改为使用新的函数声明。
-
idea快捷键
- 在方法上 shift+F6
函数组合成变换
-
动机
- 在软件中,经常需要把数据“喂”给一个程序,让它再计算出各种派生信息。 这些派生数值可能会在几个不同地方用到,因此这些计算逻辑也常会在用到派生数据的地方重复。我更愿意把所有计算派生数据的逻辑收拢到一处,这样始终可以在固定的地方找到和更新这些逻辑,避免到处重复。
-
做法
- 创建一个数据变换函数,输入参数是需要变换的记录,并直接返回该记录的值。
- 挑选一块逻辑,将其主体移入变换函数中,把结果作为字段添加到输出记录中。修改客户端代码,令其使用这个新字段。
拆分阶段
-
动机
- 看见一段代码在同时处理两件不同的事,就把它拆分成各自独立的模块,因为这样到了需要修改的时候,就可以单独处理每个主题,而不必同时 在脑子里考虑两个不同的主题。如果运气够好的话,可能只需要修改其中一个模块,完全不用回忆起另一个模块的诸般细节。
- 把一大段行为分成顺序执行的两个阶段。
-
做法
- 将第二阶段的代码提炼成独立的函数。
- 引入一个中转数据结构,将其作为参数添加到提炼出的新函数的参数列表中。
- 逐一检查提炼出的“第二阶段函数”的每个参数。如果某个参数被第一阶段用到,就将其移入中转数据结构。
- 对第一阶段的代码运用提炼函数,让提炼出的函数返回中转数据结构。
替换算法
- 动机
- 其中某些方法会比另一些简单。算法也是如此。如果发现做一件事可以有更清晰的方式,就会用比较清晰的方式取代复杂的方式。
- 使用这项重构手法之前,得确定自己已经尽可能分解了原先的函数。替换一个巨大且复杂的算法是非常困难的,只有先将它分解为较简单的小型函数,才能很有把握地进行算法替换工作。
- 做法
- 整理一下待替换的算法,保证它已经被抽取到一个独立的函数中。
- 先只为这个函数准备测试,以便固定它的行为。
- 准备好另一个(替换用)算法。
- 运行测试,比对新旧算法的运行结果。如果测试通过,那就大功告成;否则, 在后续测试和调试过程中,以旧算法为比较参照标准。
搬移函数
- 动机
- 任何函数都需要具备上下文环境才能存活。这个上下文可以是全局的,但它更多时候是由某种形式的模块所提供的。对一个面向对象的程序而言,类作为最主要的模块化手段,其本身就能充当函数的上下文;通过嵌套的方式,外层函数也能为内层函数提供一个上下文。
- 搬移函数最直接的一个动因是,它频繁引用其他上下文中的元素,而对自身上下文中的元素却关心甚少。此时,让它去与那些更亲密的元素相会,通常能取得更好的封装效果,因为系统别处就可以减少对当前模块的依赖。
- 做法
- 检查函数在当前上下文里引用的所有程序元素(包括变量和函数),考虑是否需要将它们一并搬移
- 检查待搬移函数是否具备多态性。
- 将函数复制一份到目标上下文中。调整函数,使它能适应新家。
- 设法从源上下文中正确引用目标函数。
- 修改源函数,使之成为一个纯委托函数。
- 考虑对源函数使用内联函数
搬移语句到函数
- 动机
- 如果发现调用某个函数时,总有一些相同的代码也需要每次执行,那么考虑将此段代码合并到函数里头。
- 如果某些语句与一个函数放在一起更像一个整体,并且更有助于理解,那毫不犹豫地将语句搬移到函数里去。
- 做法
- 如果重复的代码段离调用目标函数的地方还有些距离,则先用移动语句将这些语句挪动到紧邻目标函数的位置。
- 如果目标函数仅被唯一一个源函数调用,那么只需将源函数中的重复代码段剪切并粘贴到目标函数中即可。
- 如果函数不止一个调用点,那么先选择其中一个调用点应用提炼函数,将待搬移的语句与目标函数一起提炼成一个新函数。给新函数取个临时的名字,只要易于搜索即可。
- 调整函数的其他调用点,令它们调用新提炼的函数。
- 完成所有引用点的替换后,应用内联函数将目标函数内联到新函数 里,并移除原目标函数。
- 对新函数应用函数改名,将其改名为原目标函数的名字。
搬移语句到调用者
- 动机
- 随着系统能力发生演进,原先设定的抽象边界总会悄无声息地发生偏移。对于函数来说,这样的边界偏移意味着曾经视为一个整体、一个单元的行为,如今可能已经分化出两个甚至是多个不同的关注点。
- 做法
- 最简单的情况下,原函数非常简单,其调用者也只有寥寥一两个,此时只需把 要搬移的代码从函数里剪切出来并粘贴回调用端去即可,必要的时候做些调整。
- 若调用点不止一两个,则需要先用提炼函数将你不想搬移的代码提炼成一个新函数,函数名可以临时起一个,只要后续容易搜索即可。
- 对原函数应用内联函数
- 对提炼出来的函数应用改变函数声明,令其与原函数使用同一个名字。
以函数调用取代内联代码
- 动机
- 善用函数可以帮助我将相关的行为打包起来,这对于提升代码的表达力大有裨益—— 一个命名良好的函数,本身就能极好地解释代码的用途,使读者不必了解其细节。
- 简单点说,内联后的代码,做的事太多了,用函数取代内联代码
- 做法
- 将内联代码替代为对一个既有函数的调用。
移动语句
- 动机
- 让存在关联的东西一起出现,可以使代码更容易理解。如果有几行代码取用了同一个数据结构,那么最好是让它们在一起出现,而不是夹杂在取用其他数据结构的代码中间。
- 做法
- 确定待移动的代码片段应该被搬往何处。仔细检查待移动片段与目的地之间的语句,看看搬移后是否会影响这些代码正常工作。如果会,则放弃这项重构。
- 剪切源代码片段,粘贴到上一步选定的位置上。
拆分循环
- 动机
- 一个循环只计算一个值,那么它直接返回该值即可;但如果循环做了太多件事,那就只得返回结构型数据或者通 过局部变量传值了。
- 这项重构手法可能让许多程序员感到不安,因为它会迫使你执行两次循环。先进行重构,然后再进行性能优化。我得先让代码结构变得清晰,才能做进一步优化;如果重构之后该循环确实成了性能的瓶颈,届时再把拆开的循环合到一起也很容易。
- 做法
- 复制一遍循环代码。
- 识别并移除循环中的重复代码,使每个循环只做一件事。
- 完成循环拆分后,考虑对得到的每个循环应用提炼函数。
以管道取代循环
- 动机
- 管道运算得到的集合可以供管道的后续流程使用。一些逻辑如果采用集合管道来编写,代码的可读性会更强——只需要从头到尾阅读一遍代码,就能弄清对象在管道中间的变换过程。
- 做法
- 创建一个新变量,用以存放参与循环过程的集合。
- 从循环顶部开始,将循环里的每一块行为依次搬移出来,在上一步创建的集合 变量上用一种管道运算替代之。每次修改后运行测试。
- 搬移完循环里的全部行为后,将循环整个删除。
分解条件表达式
- 动机
- 程序之中,复杂的条件逻辑是最常导致复杂度上升的地点之一。任何大块头代码一样,可以将它分解为多个独立的函数,根据每个小块代码的用途,为分解而得的新函数命名,并将原函数中对应的代码改为调用新函数,从而更清楚地表达自己的意图。
- 本重构手法其实只是提炼函数的一个应用场景。
- 做法
- 对条件判断和每个条件分支分别运用提炼函数手法。
合并条件表达式
- 动机
- 有这样一串条件检查:检查条件各不相同,最终行为却一致。如果发现这种情况,就应该使用“逻辑或”和“逻辑与”将它们合并为一个条件表达式。
- 有两个重要原因。
- 首先,合并后的条件代码会表 述“实际上只有一次条件检查,只不过有多个并列条件需要检查而已”,从而使这 一次检查的用意更清晰
- 其次,这项重构往往可以为使用提炼函数做好准备。将检查条件提炼成一个独立的函数对于理清代码意义非常有用,因为它把描述“做什么”的语句 换成了“为什么这样做”。
- 条件语句的合并理由也同时指出了不要合并的理由:如果认为这些检查的确彼此独立,的确不应该被视为同一次检查,我就不会使用本项重构。
- 做法
- 确定这些条件表达式都没有副作用。
- 使用适当的逻辑运算符,将两个相关条件表达式合并为一个。
- 重复前面的合并过程,直到所有相关的条件表达式都合并到一起。
以卫语句取代嵌套条件表达式
- 动机
- 否定先行
- 做法
- 选中最外层需要被替换的条件逻辑,将其替换为卫语句。
- 如果所有卫语句都引发同样的结果,可以使用合并条件表达式合并之。
引入特例
- 动机
- 一种常见的重复代码是这种情况:一个数据结构的使用者都在检查某个特殊的值,并且当这个特殊值出现时所做的处理也都相同。如果发现代码库中有多处以同样方式应对同一个特殊值,就要把这个处理逻辑收拢到一处。
- 一个通常需要特例处理的值就是null,这也是这个模式常被叫作“Null对象”(Null Object)模式的原因。Null对象是特例的一种特例。
- 做法
- 给重构目标添加检查特例的属性,令其返回false。
- 创建一个特例对象,其中只有检查特例的属性,返回true。
- 对“与特例值做比对”的代码运用提炼函数,确保所有客户端都使用这个新函数,而不再直接做特例值的比对。
- 将新的特例对象引入代码中,可以从函数调用中返回,也可以在变换函数中生成。
- 修改特例比对函数的主体,在其中直接使用检查特例的属性。
- 使用函数组合成类或函数组合成变换,把通用的特例处理逻辑都搬移到新建的特例对象中。
- 对特例比对函数使用内联函数,将其内联到仍然需要的地方。
移除标记参数
- 动机
- “标记参数”是这样的一种参数:调用者用它来指示被调函数应该执行哪一部分逻辑。
- 做法
- 针对参数的每一种可能值,新建一个明确函数。
- 对于“用字面量值作为参数”的函数调用者,将其改为调用新建的明确函数。
对象级别重构
封装记录
-
动机
- 把零散的记录封装程类对象,对象可以隐藏结构的细节,仅为这几个值提供对应的方法。该对象的用户不必追究存储的细节和计算的过程。同时,这种封装还有助于字段的改名:可以重新命名字段,但同时提供新老字段名的访问方法,这样我就可以渐进地修改调用方,直到替换全部完成。
- 记录型结构可以有两种类型:
一种需要声明合法的字段名字(class)
另一种可以随 便用任何字段名字(map)
-
做法
- 对持有记录的变量使用封装变量,将其封装到一个函数中。
- 创建一个类,将记录包装起来,并将记录变量的值替换为该类的一个实例。
- 新建一个函数,让它返回该类的对象,而非那条原始的记录。
- 对于该记录的每处使用点,将原先返回记录的函数调用替换为那个返回实例对象的函数调用
- 移除类对原始记录的访问函数
- 如果记录中的字段本身也是复杂结构,考虑对其再次应用封装记录或封装集合手法。
封装集合
-
动机
- 只对集合变量的访问进行了封装,但依然让取值函数返回集合本身。这使得 集合的成员变量可以直接被修改,而封装它的类则全然不知,无法介入。
-
做法
- 如果集合的引用尚未被封装起来,先用封装变量封装它。
- 在类上添加用于“添加集合元素”和“移除集合元素”的函数。
- 查找集合的引用点。如果有调用者直接修改集合,令该处调用使用新的添加/ 移除元素的函数。每次修改后执行测试。
- 修改集合的取值函数,使其返回一份只读的数据,可以使用只读代理或数据副本。
以对象取代基本类型
-
动机
- 开发初期,往往决定以简单的数据项表示简单的情况,比如使用数字或字符串等。但随着开发的进行,可能会发现,这些简单数据项不再那么简单了。 比如说,一开始你可能会用一个字符串来表示“电话号码”的概念,但是随后它又需要“格式化”“抽取区号”之类的特殊行为。这类逻辑很快便会占领代码库,制造出许多重复代码,增加使用时的成本。
-
做法
- 如果变量尚未被封装起来,先使用封装变量
- 为这个数据值创建一个简单的类。
- 修改第一步得到的设值函数,令其创建一个新类的对象并将其存入字段,如果 有必要的话,同时修改字段的类型声明。
- 修改取值函数,令其调用新类的取值函数,并返回结果。
- 考虑对第一步得到的访问函数使用函数改名,以便更好反映其用途。
搬移字段
- 动机
- 一个适应于问题域的良好数据结构,可以让行为代码变得简单明了,而一个糟糕的数据结构则将招致许多无用代码,这些代码更多是在差劲的数据结构中间纠缠不清,而非为系统实现有用的行为。代码凌乱,势必难以理解;不仅如此,坏的数据结构本身也会掩藏程序的真实意图。
- “什么是理想的数据结构”,这个星期看来合理而正确的设计决策,到了下个星期可能就不再正确了。发现数据结构已经不适应于需求,就应该马上修缮它。
- 做法
- 确保源字段已经得到了良好封装。
- 在目标对象上创建一个字段(及对应的访问函数)。
- 确保源对象里能够正常引用目标对象。
- 调整源对象的访问函数,令其使用目标对象的字段。
- 移除源对象上的字段。
以工厂函数取代构造函数
- 动机
- 如,Java的构造函数只能返回当前所调用类的实例,无法根据环境或参数信息返回子类实例或代理对象;构造函数的名字是固定的,因此无法使用比默认名字更清晰的函数名;构造函数需要通过特殊的操作符来调用,所以在要求普通函数的场合就难以使用。
- 工厂函数就不受这些限制。工厂函数的实现内部可以调用构造函数,但也可 以换成别的方式实现。
- 做法
- 新建一个工厂函数,让它调用现有的构造函数。
- 将调用构造函数的代码改为调用工厂函数。
- 尽量缩小构造函数的可见范围。
类级别重构
引入参数对象
-
动机
- 一组数据项总是结伴同行,出没于一个又一个函数。这样一组数据就是所谓的数据泥团。
- 一旦识别出新的数据结构,就可以重组程序的行为来使用这些结构。会创建出函数来捕捉围绕这些数据的共用行为,也可能用一个类把数据结构与使用数据的函数组合起来。这个过程会改变代码的概念图景,将这些数据结构提升为新的抽象概念,可以帮助我更好地理解问题域
-
做法
- 如果暂时还没有一个合适的数据结构,就创建一个。
- 使用改变函数声明给原来的函数新增一个参数,类型是新建的数据结构
- 调整所有调用者,传入新数据结构的适当实例。
- 用新数据结构中的每项元素,逐一取代参数列表中与之对应的参数项,然后删 除原来的参数。
函数组合成类
-
动机
- 发现一组函数形影不离地操作同一块数据,是时候组建一个类了。类能明确地给这些函数提供一 个共用的环境,在对象内部调用这些函数可以少传许多参数,从而简化函数调 用,并且这样一个对象也可以更方便地传递给系统的其他部分。
-
做法
- 运用封装记录对多个函数共用的数据记录加以封装
- 对于使用该记录结构的每个函数,运用搬移函数将其移入新类。
- 用以处理该数据记录的逻辑可以用提炼函数提炼出来,并移入新类。
提炼类
-
动机
- 类会不断成长扩展。你会在这儿加入一些功能,在那儿加入一些数据。给某个类添加一项新责任时,会觉得不值得为这项责任分离出一个独立的类。于是,随着责任不断增加,这个类会变得过分复 杂。很快,你的类就会变成一团乱麻。
-
做法
- 决定如何分解类所负的责任。
- 创建一个新的类,用以表现从旧类中分离出来的责任。
- 构造旧类时创建一个新类的实例,建立“从旧类访问新类”的连接关系。
- 对于你想搬移的每一个字段,运用搬移字段搬移之。
- 检查两个类的接口,去掉不再需要的函数,必要时为函数重新取一个适合新环 境的名字。
内联类
-
动机
- 如果一个类不再承担足够责任,不再有单独存在的理由(这通常是因为此前的重构动作移走了这个类的责任)挑选这一“萎缩类”的最频繁类,以本手法将“萎缩类”塞进另一个类中。
-
做法
- 对于待内联类(源类)中的所有public函数,在目标类上创建一个对应的函数,新创建的所有函数应该直接委托至源类。
- 修改源类public方法的所有引用点,令它们调用目标类对应的委托方法。每次更改后运行测试。
- 将源类中的函数与数据全部搬移到目标类,每次修改之后进行测试,直到源类变成空壳为止。
- 删除源类
隐藏委托关系
-
动机
- 如果某些客户端先通过服务对象的字段得到另一个对象(受托类),然后调用后者的函数,那么客户就必须知晓这一层委托关系。
万一受托类修改了接口,变化会波及通过服务对象使用它的所有客户端。可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,从而去除这种依赖。
- 如果某些客户端先通过服务对象的字段得到另一个对象(受托类),然后调用后者的函数,那么客户就必须知晓这一层委托关系。
-
做法
- 对于每个委托关系中的函数,在服务对象端建立一个简单的委托函数。
- 调整客户端,令它只调用服务对象提供的函数。每次调整后运行测试。
- 如果将来不再有任何客户端需要取用Delegate(受托类),便可移除服务对象中的相关访问函数。
移除死代码
- 动机
- 当你尝试阅读代码、理解软件的运作原理时,无用代码确实会带来很多额外的思维负 担。它们周围没有任何警示或标记能告诉程序员,让他们能够放心忽略这段函数,因为已经没有任何地方使用它了。当程序员花费了许多时间,尝试理解它的工作原理时,却发现无论怎么修改这段代码都无法得到期望的输出。
- 做法
- 如果死代码可以从外部直接引用,比如它是一个独立的函数时,先查找一下还 有无调用点。
- 将死代码移除。
以多态取代条件表达式
- 动机
- 设计模式解决,如策略模式。
- 大部分条件逻辑只用到了基本的条件语句——if/else和 switch/case,并不需要劳师动众地引入多态。但如果发现复杂条件逻辑,多态是改善这种情况的有力工具。
- 做法
- 如果现有的类尚不具备多态行为,就用工厂函数创建之,令工厂函数返回恰当的对象实例。
- 在调用方代码中使用工厂函数获得对象实例。
- 将带有条件逻辑的函数移到超类中。
- 任选一个子类,在其中建立一个函数,使之覆写超类中容纳条件表达式的那个 函数。将与该子类相关的条件表达式分支复制到新函数中,并对它进行适当调整。
- 重复上述过程,处理其他条件分支。
- 在超类函数中保留默认情况的逻辑。或者,如果超类应该是抽象的,就把该函数声明为abstract,或在其中直接抛出异常,表明计算责任都在子类中。
函数上移
-
动机
- 如果某个函数在各个子类中的函数体都相同,这就是最显而易见的函数上移适用场合。
- 如果两个函数工作流程大体相似,但实现细节略有差异,那么考虑先借助塑造模板函数构造出相同的函数,然后再提升它们。
-
做法
- 检查待提升函数,确定它们是完全一致的。
- 检查函数体内引用的所有函数调用和字段都能从超类中调用到。
- 如果待提升函数的签名不同,使用改变函数声明将那些签名都修改为你想要在超类中使用的签名。
- 在超类中新建一个函数,将某一个待提升函数的代码复制到其中。
- 移除一个待提升的子类函数。测试
- 逐一移除待提升的子类函数,直到只剩下超类中的函数为止。
函数下移
- 动机
- 如果超类中的某个函数只与一个(或少数几个)子类有关,那么最好将其从超类中挪走,放到真正关心它的子类中去。
- 做法
- 将超类中的函数本体复制到每一个需要此函数的子类中。
- 删除超类中的函数。
- 将该函数从所有不需要它的那些子类中删除。
字段上移
- 动机
- 如果各子类是分别开发的,或者是在重构过程中组合起来的,会发现它们拥有重复特性,特别是字段更容易重复。判断若干字段是否重复,唯一的办法就是观察函数如何使用它们。如果它们被使用的方式很相似,我就可以将它们提升到超类中去。
- 做法
- 针对待提升之字段,检查它们的所有使用点,确认它们以同样的方式被使用。
- 如果这些字段的名称不同,先使用变量改名为它们取个相同的名字。
- 在超类中新建一个字段。
- 移除子类中的字段。
字段下移
- 动机
- 如果某个字段只被一个子类(或者一小部分子类)用到,就将其搬移到需要 该字段的子类中。
- 做法
- 在所有需要该字段的子类中声明该字段。
- 将该字段从超类中移除。
- 将该字段从所有不需要它的那些子类中删掉。
提炼子类
- 动机
- 软件系统经常需要表现“相似但又不同的东西”,通过类中的某一个类型码,去判断执行的逻辑,这个时候引入子类,通过子类继承的方式,同时使用以多态取代条件表达式,去子类取代类型码。
- 做法
- 自封装类型码字段。
- 任选一个类型码取值,为其创建一个子类。覆写类型码类的取值函数,令其返 回该类型码的字面量值。
- 创建一个选择器逻辑,把类型码参数映射到新的子类。
- 针对每个类型码取值,重复上述“创建子类、添加选择器逻辑”的过程。每次修改后执行测试。
- 去除类型码字段。
- 使用函数下移和以多态取代条件表达式处理原本访问了类型码的函数。全部处理完后,就可以移除类型码的访问函数。
移除子类
- 动机
- 随着软件的演化,子类所支持的变化可能会被搬移到别处, 甚至完全去除,这时子类就失去了价值。有时添加子类是为了应对未来的功能, 结果构想中的功能压根没被构造出来,或者用了另一种方式构造,使该子类不再被需要了。
- 子类存在着就有成本,阅读者要花心思去理解它的用意,所以如果子类的用处太少,就不值得存在了。此时,最好的选择就是移除子类,将其替换为超类中的一个字段。
- 做法
- 使用以工厂函数取代构造函数,把子类的构造函数包装到超类的工厂函数中。
- 如果有任何代码检查子类的类型,先用提炼函数把类型检查逻辑包装 起来,然后用搬移函数将其搬到超类。
- 新建一个字段,用于代表子类的类型。
- 将原本针对子类的类型做判断的函数改为使用新建的类型字段。
- 删除子类。
以委托取代子类
- 动机
- 继承也有其短板。最明显的是,继承这张牌只能打一次。导致行为不同的原因可能有多种,但继承只能用于处理一个方向上的变化。对于不同的变化原因,可以委托给不同的类。委托是对象之间常规的关系。与继承关系相比,使用委托关系时接口更清晰、耦合更少。因此,继承关系遇到问题时运用以委托取代子类是常见的情况。
- 做法
- 如果构造函数有多个调用者,首先用以工厂函数取代构造函数把构造 函数包装起来。
- 创建一个空的委托类,这个类的构造函数应该接受所有子类特有的数据项,并 且经常以参数的形式接受一个指回超类的引用。
- 在超类中添加一个字段,用于安放委托对象。
- 修改子类的创建逻辑,使其初始化上述委托字段,放入一个委托对象的实例。
- 选择一个子类中的函数,将其移入委托类。
- 使用搬移函数手法搬移上述函数,不要删除源类中的委托代码。
- 如果被搬移的源函数还在子类之外被调用了,就把留在源类中的委托代码从子类移到超类,并在委托代码之前加上卫语句,检查委托对象存在。如果子类之外已经没有其他调用者,就用移除死代码去掉已经没人使用的委托代码。
- 重复上述过程,直到子类中所有函数都搬到委托类。
- 找到所有调用子类构造函数的地方,逐一将其改为使用超类的构造函数。
- 运用移除死代码去掉子类。
以委托取代超类
- 动机
- 有一个经典的误用继承的例子:让栈(stack)继承列表(list)。这个想法的出发点是想复用列表类的数据存储和操作能力。虽说复用是一件好事,但这个继承关系有问题:列表类的所有操作都会出现在栈类的接口上,然而其中大部分操作对一个栈来说并不适用。更好的做法应该是把列表作为栈的字段,把必要的操作委派给列表就行了。
- 如果超类的一些函数对子类并不适用,就说明不应该通过继承来获得超类的功能。
- 做法
- 在子类中新建一个字段,使其引用超类的一个对象,并将这个委托引用初始化为超类的新实例。
- 针对超类的每个函数,在子类中创建一个转发函数,将调用请求转发给委托引用。每转发一块完整逻辑,都要执行测试。
- 当所有超类函数都被转发函数覆写后,就可以去掉继承关系。
评论区