《重构:改善既有代码的设计(第2版)》图解
2022-05-16 20:36:53 13 举报
AI智能生成
《重构:改善既有代码的设计(第2版)》图解
作者其他创作
大纲/内容
bad_smell速查
速查1
速查2
重构的原则
什么是重构
动词
使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构
名词
对软件内部结构的一种调整,目的是在不改变软件可观察行 为的前提下,提高其可理解性,降低其修改成本。
重构的关键
运用大 量微小且保持软件行为的步骤,一步步达成大规模的修改
每个单独的重构要么 很小,要么由若干小步骤组合而成
即便重构没有完成,也可以在任何时刻停下来
和性能优化的区别
相似点
两者都需要修改代码,并且两者都不会改 变程序的整体功能
差别
重构是为了让代码“更容易理解, 更易于修改”。
只关心让程序运行得更快,最终得到的代码有可能更难理解和维护
结构调整
泛指对代码库进行的各种形式的重新组 织或清理
重构是一种特殊的结构调整
两顶帽子
重构
不能再添加功能,只管调整代码的结构。此时我不 应该添加任何测试
添加新功能
不应该 修改既有代码,只管添加新功能。通过添加测试并让测试正常运行
重构的目的
改进软件的设计
理解架构的整体设计
改进设计的一个重要方向就是 消除重复代码
确定所有事物和行为在代码中只表述一次,这正是优秀设计的根本。
使软件更容易理解
让代码更好地表达自己的意图——更清晰地说出我想要 做的
帮助找bug
重构可以帮助我们更好的理解代码的所作所为
重构能够帮助我们更 有效地写出健壮的代码
提高编程速度
通过投入精力改善内部设计,我们增 加了软件的耐久性,从而可以更长时间地保持开发的快速
何时重构
三次法则
见机行事式的重构
预备性重构
修改现有代码,让添加新功能更容易
帮助理解的重构
使代码更易懂
捡垃圾式重构
发现代码问题,但有更重要工作时,小步骤的缓慢重构
有计划重构
长期重构
复审代码时重构
何时不应该重构
重写比重构还容易
不需要修改它,那么我就不需要重构它
重构中的问题或挑战
延缓新功能开发
代码所有权
接口的使用者(客户端)与声 明者彼此独立,声明者无权修改使用者的代码。
不要搞细粒度的强代码所有制
分支
过长时间的分支或过大的分支合并产生合并困难
特性分支的周期应当很短
持续集成,每天至少合并一次
测试
应当快速发现重构中产生的问题
构建自动化测试,避免大多数引入bug的情况
自测试代码是持续交付的关键环节
遗留代码
没测试就加测试
先找到程序的接缝,在接缝处 插入测试
每次触碰一块代码时,我会尝试把 它变好一点点
数据库
借助数据迁移脚本,将数据库结构的修 改与代码相结合,使大规模的、涉及数据库的修改可以比较容易地开展
性能问题
大多数情况下可以忽略 它。如果重构引入了性能损耗,先完成重构,再做性能优化
坏味道bad smell
神秘命名
函数声明
变量命名
字段命名
重复代码
问题
需要仔细甄别代码中的细微差别
一旦修改,需要找出所有副本
解决
过长函数
小函数
更好的阐释力
容易分享
更多选择
原则
大段函数
大段switch、if
大段循环
提炼方法
如果参数过多,查询取代临时变量来消除这些临时元素
引入参数对象和保持对象完整则 可以将过长的参数列表变得更简洁
过长参数列表
以查 询取代参数
保持对象完整
引入参数对象,将其 合并成一个对象
多个函数有同样的几个参数,引入一 个类。可以使用函数组合成类,将这些共同的参数变成 这个类的字段
全局数据
产生问题的点
单例
类变量
全局变量
解决
封装变量
可变数据
封装变量
来确保所有数据更新操作都通过很少几个函数来进 行,使其更容易监控和演进
拆分变量
拆分为各自不同用途的变量
移动语句
提炼函数
把逻辑从处理更新操作的 代码中搬移出来,将没有副作用的代码与执行数据更新操作的代码分开
查询取代派生变量
函数组合成类
发散式变化
“每次只关心一个上下文
拆分阶段
霰弹式修改
搬移函数
搬移字段
把所有需要修 改的代码放进同一个模块里
函 数组合成类
函数组合成变换
拆分阶段
依恋情结
搬移函 数
原则
最根本的原则
数据泥团
提炼类
基本类型偏执
对象取代基本类型
重复的switch
多态
循环语句
以管道取代循环
冗赘的元素
夸夸其谈通用性
临时字段
提炼类
用引入 特例
过长的消息链
隐藏委托
中间人
移除中间人
内幕交易
过大的类
提炼类产生新类
提炼超类
以子类取代类型码
异曲同工的类???
纯数据类
被拒绝的遗赠
注释
当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多 余
构筑测试体系
自测试代码的价值
重构手法依然需要通过测试集合来保障
减少查找bug的时间
TDD测试驱动开发
先写测试,再实现接口
一旦业务逻辑的 部分开始变复杂,把逻辑与UI分离开,以便能更好地理解和测试业务逻辑
mocha/jest
总是确保测试不该通过时真的会失败。
频繁地运行测试。对于你正在处理的代码,与其对应的测试至少每隔 几分钟就要运行一次,每天至少运行一次所有的测试。
观察被测试类应该做的所 有事情,然后对这个类的每个行为进行测试,包括各种可能使它发生异常的边界 条件
编写未臻完善的测试并经常运行,好过对完美测试的无尽等待
个it语句中最好只有一个验证语句
每当你收到bug报告,请先写一个单元测试来暴露这个bug。
重构方法
提炼函数
动机
“将意图与实现分开
做法
创造一个新函数,根据这个函数的意图来对它命名
将待提炼的代码从源函数复制到新建的目标函数中
仔细检查提炼出的代码,看看其中是否引用了作用域限于源函数、在提炼出的 新函数中访问不到的变量。若是,以参数的形式将它们传递给新函数。
所有变量都处理完之后,编译
在源函数中,将被提炼代码段替换为对目标函数的调用
测试
查看其他代码是否有与被提炼的代码段相同或相似之处。如果有,考虑使用以 函数调用取代内联代码(222)令其调用提炼出的新函数
内联函数
动机
某些函数,其内部代码和函数名称同样清晰易读
有一群组织不甚合理的函数。可
以将它们都内联到一个大型函数中,再以我喜欢的方式重新提炼出小函数。
以将它们都内联到一个大型函数中,再以我喜欢的方式重新提炼出小函数。
做法
检查函数,确定它不具多态性
找出这个函数的所有调用点
将这个函数的所有调用点都替换为函数本体。
每次替换之后,执行测试。
删除该函数的定义。
提炼变量
动机
表达式有可能非常复杂而难以阅读
做法
确认要提炼的表达式没有副作用。
声明一个不可修改的变量,把你想要提炼的表达式复制一份,以该表达式的结 果值给这个变量赋值
用这个新变量取代原来的表达式。
测试
内联变量
动机
这个名字并不比表达式本身更具表现力
变量可能 会妨碍重构附近的代码。
做法
检查确认变量赋值语句的右侧表达式没有副作用
如果变量没有被声明为不可修改,先将其变为不可修改,并执行测试
找到第一处使用该变量的地方,将其替换为直接使用赋值语句的右侧表达式。
测试。
重复前面两步,逐一替换其他所有使用该变量的地方。
删除该变量的声明点和赋值语句。
测试
改变函数声明
动机
一个好名字能让人一眼 看出函数的用途,而不必查看其实现代码
修改参数列表,增加函数的应用范围
修改参数列表,改变连接一个模块所需的条 件,从而去除不必要的耦合
做法
简单做法
如果想要移除一个参数,需要先确定函数体内没有使用该参数。
修改函数声明,使其成为你期望的状态。
找出所有使用旧的函数声明的地方,将它们改为使用新的函数声明
测试。
迁移式做法
如果有必要的话,先对函数体内部加以重构,使后面的提炼步骤易于开展
使用提炼函数(106)将函数体提炼成一个新函数。
如果提炼出的函数需要新增参数,用前面的简单做法添加即可。
测试
对旧函数使用内联函数
如果新函数使用了临时的名字,再次使用改变函数声明(124)将其改回原来 的名字。
测试。
封装变量
动机
如果把数据搬走, 就必须同时修改所有引用该数据的代码,否则程序就不能运行
搬移一处被广泛使用的数据,最好的办法往往是先以函数形
式封装所有对该数据的访问
式封装所有对该数据的访问
可以监控 数据的变化和使用情况;
可以轻松地添加数据被修改时的验证或后续逻辑
做法
创建封装函数,在其中访问和更新变量值
执行静态检查。
逐一修改使用该变量的代码,将其改为调用合适的封装函数。每次替换之后, 执行测试
限制变量的可见性。
测试
如果变量的值是一个记录,考虑使用封装记录
变量改名
动机
好的命名是整洁编程的核心
做法
如果变量被广泛使用,考虑运用封装变量(132)将其封装起来。
找出所有使用该变量的代码,逐一修改
测试。
引入参数对象
动机
一组数据项总是结伴同行,出没于一个又一个函数。这样一组 数据就是所谓的数据泥团
让数据项之间的关系变得明 晰
参数的参数列表也能缩短
使 用该数据结构的函数都会通过同样的名字来访问其中的元素,从而提升代码的一 致性
催生代码中更深层次的改变。一旦识别出 新的数据结构,我就可以重组程序的行为来使用这些结构
做法
如果暂时还没有一个合适的数据结构,就创建一个。
测试。
使用改变函数声明(124)给原来的函数新增一个参数,类型是新建的数据结 构。
测试。
调整所有调用者,传入新数据结构的适当实例。每修改一处,执行测试。
用新数据结构中的每项元素,逐一取代参数列表中与之对应的参数项,然后删 除原来的参数。测试
函数组合成类
动机
一组函数形影不离地操作同一块数据,组建一个类了。类能明确地给这些函数提供一 个共用的环境,在对象内部调用这些函数可以少传许多参数,从而简化函数调 用,并且这样一个对象也可以更方便地传递给系统的其他部分
其他 的计算逻辑,将它们也重构到新的类当中
做法
运用封装记录(162)对多个函数共用的数据记录加以封装
对于使用该记录结构的每个函数,运用搬移函数(198)将其移入新类
用以处理该数据记录的逻辑可以用提炼函数(106)提炼出来,并移入新类
函数组合成变换
动机
数据计算出各种派生信息。 这些派生数值可能会在几个不同地方用到,因此这些计算逻辑也常会在用到派生 数据的地方重复
孤立存在的函数常 常很难找到
做法
创建一个变换函数,输入参数是需要变换的记录,并直接返回该记录的值
挑选一块逻辑,将其主体移入变换函数中,把结果作为字段添加到输出记录 中。修改客户端代码,令其使用这个新字段
测试
针对其他相关的计算逻辑,重复上述步骤
拆分阶段
动机
一段代码在同时处理两件不同的事,拆分成各自独立的 模块
果一块代码中出现了上下几 段,各自使用不同的一组数据和函数,这就是最明显的线索
做法
将第二阶段的代码提炼成独立的函数
测试
引入一个中转数据结构,将其作为参数添加到提炼出的新函数的参数列表中
测试
逐一检查提炼出的“第二阶段函数”的每个参数。如果某个参数被第一阶段用 到,就将其移入中转数据结构。每次搬移之后都要执行测试
创建一个类,将记录包装起来,并将记录变量的值替换为该类的一个实例。然 后在类上定义一个访问函数,用于返回原始的记录。修改封装变量的函数,令 其使用这个访问函数
对第一阶段的代码运用提炼函数(106),让提炼出的函数返回中转数据结 构
封装记录
动机
记录强迫清晰地区分“记 录中存储的数据”和“通过计算得到的数据”。
对象可以隐藏结构的细节,仅为值提供对应的方法。该对象的用户不必追究存储的 细节和计算的过程
“数据结构不直观”
做法
对持有记录的变量使用封装变量(132),将其封装到一个函数中
测试
新建一个函数,让它返回该类的对象,而非那条原始的记录
对于该记录的每处使用点,将原先返回记录的函数调用替换为那个返回实例对 象的函数调用。使用对象上的访问函数来获取数据的字段,如果该字段的访问 函数还不存在,那就创建一个。每次更改之后运行测试。
移除类对原始记录的访问函数,那个容易搜索的返回原始数据的函数也要一并 删除。
测试
如果记录中的字段本身也是复杂结构,考虑对其再次应用封装记录(162)或 封装集合(170)手法
封装集合
动机
只对集合变量的访问进行了封装,但依然让取值函数返回集合本身
这使得 集合的成员变量可以直接被修改,而封装它的类则全然不知,无法介入
不要让集合的取值函数返回原始集 合,这就避免了客户端的意外修改
为集合提供一个取值函数,但令其返回一个集合的副 本。这样即使有人修改了副本,被封装的集合也不会受到影响
做法
如果集合的引用尚未被封装起来,先用封装变量(132)封装它
在类上添加用于“添加集合元素”和“移除集合元素”的函数。
执行静态检查。
查找集合的引用点。如果有调用者直接修改集合,令该处调用使用新的添加/ 移除元素的函数。每次修改后执行测试。
修改集合的取值函数,使其返回一份只读的数据,可以使用只读代理或数据副 本
测试
以对象取代基本类型
动机
简单数据项不再那么简单
简单数据相关的逻辑很快便会占领代码库,制造 出许多重复代码,增加使用时的成本
做法
如果变量尚未被封装起来,先使用封装变量(132)封装它
为这个数据值创建一个简单的类。类的构造函数应该保存这个数据值,并为它 提供一个取值函数
执行静态检查
修改第一步得到的设值函数,令其创建一个新类的对象并将其存入字段,如果 有必要的话,同时修改字段的类型声明
修改取值函数,令其调用新类的取值函数,并返回结果
测试
考虑对第一步得到的访问函数使用函数改名(124),以便更好反映其用途
考虑应用将引用对象改为值对象(252)或将值对象改为引用对象(256),明 确指出新对象的角色是值对象还是引用对象
以查询取代临时变量
动机
变量抽取到函数里能使函数的分解 过程更简单,因为我就不再需要将变量作为参数传递给提炼出来的小函数
有助于在提炼得到的函数与原函数之间设立清晰的 边界,这能帮我发现并避免难缠的依赖及副作用
避免了在多个函数中重复编写计算逻辑
做法
检查变量在使用前是否已经完全计算完毕,检查计算它的那段代码是否每次都 能得到一样的值
如果变量目前不是只读的,但是可以改造成只读变量,那就先改造它
测试。
将为变量赋值的代码段提炼成函数
测试
应用内联变量(123)手法移除临时变量。
提炼类
动机
如果某些数据和某些函数总是一起出现,某些数据经常同时变化甚至彼此相依, 这就表示你应该将它们分离出去
如果子类化只
影响类的部分特性,或如果某些特性需要以一种方式来子类化,某些特性 则需要以另一种方式子类化,这就需要分解原来的类。
影响类的部分特性,或如果某些特性需要以一种方式来子类化,某些特性 则需要以另一种方式子类化,这就需要分解原来的类。
做法
决定如何分解类所负的责任
创建一个新的类,用以表现从旧类中分离出来的责任。
构造旧类时创建一个新类的实例,建立“从旧类访问新类”的连接关系。
对于你想搬移的每一个字段,运用搬移字段(207)搬移之。每次更改后运行 测试
使用搬移函数(198)将必要函数搬移到新类。先搬移较低层函数(也就是“被 其他函数调用”多于“调用其他函数”者)。每次更改后运行测试
检查两个类的接口,去掉不再需要的函数,必要时为函数重新取一个适合新环 境的名字
决定是否公开新的类。如果确实需要,考虑对新类应用将引用对象改为值对象 (252)使其成为一个值对象。
内联类
动机
如果一个类不再承担足够责任,不再有 单独存在的理由,挑选这一“萎缩类”的最频繁用户(也是一个类),以本手法将“萎缩类”塞进另一 个类中
两个类,想重新安排它们肩负的职 责,并让它们产生关联。这时我发现先用本手法将它们内联成一个类再用提炼类 (182)去分离其职责会更加简单
做法
对于待内联类(源类)中的所有public函数,在目标类上创建一个对应的函 数,新创建的所有函数应该直接委托至源类
修改源类public方法的所有引用点,令它们调用目标类对应的委托方法。每次 更改后运行测试
将源类中的函数与数据全部搬移到目标类,每次修改之后进行测试,直到源类 变成空壳为止
删除源类,为它举行一个简单的“丧礼
隐藏委托关系
动机
封装”意味着每个模块都应该尽可能少了解系统的其他部分
做法
对于每个委托关系中的函数,在服务对象端建立一个简单的委托函数
调整客户端,令它只调用服务对象提供的函数。每次调整后运行测试
如果将来不再有任何客户端需要取用Delegate(受托类),便可移除服务对象 中的相关访问函数。
测试。
移除中间人
动机
受托类的特性(功能)越来越多,更多的转 发函数就会使人烦躁。服务类完全变成了一个中间人(81),此时就应该让客户 直接调用受托类
做法
为受托对象创建一个取值函数
对于每个委托函数,让其客户端转为连续的访问函数调用。每次替换后运行测 试。
替换算法
动机
如果做一件事可以有更清晰的方式,用 比较清晰的方式取代复杂的方式
做法
整理一下待替换的算法,保证它已经被抽取到一个独立的函数中
先只为这个函数准备测试,以便固定它的行为
准备好另一个(替换用)算法
执行静态检查
运行测试,比对新旧算法的运行结果。如果测试通过,那就大功告成;否则, 在后续测试和调试过程中,以旧算法为比较参照标准。
搬移函数
动机
任何函数都需要具备上下文环境才能存活
搬移函数最直接的一个动因是,它频繁引用其他上下文中的元素,而对自身 上下文中的元素却关心甚少
发现需要频繁调用一个别处的函数,我也会考 虑搬移这个函数
做法
检查函数在当前上下文里引用的所有程序元素(包括变量和函数),考虑是否 需要将它们一并搬移
检查待搬移函数是否具备多态性
将函数复制一份到目标上下文中。调整函数,使它能适应新家
执行静态检查
设法从源上下文中正确引用目标函数
修改源函数,使之成为一个纯委托函数。
测试
考虑对源函数使用内联函数
搬移字段
动机
每当调用某个函数时,除了传入一
个记录参数,还总是需要同时传入另一条记录的某个字段一起作为参数
个记录参数,还总是需要同时传入另一条记录的某个字段一起作为参数
如果修改一条记录时, 总是需要同时改动另一条记录,那么说明很可能有字段放错了位置。
更新一个字段时,需要同时在多个结构中做出修改,那也是一个征兆
做法
确保源字段已经得到了良好封装
测试
在目标对象上创建一个字段(及对应的访问函数)。
执行静态检查。
确保源对象里能够正常引用目标对象
调整源对象的访问函数,令其使用目标对象的字段
测试。
移除源对象上的字段。
测试。
搬移语句到函数
动机
消除重复
某些语句与一个函数放在一起更像一个整体,并且更有助于理解,那我 就会毫不犹豫地将语句搬移到函数里去
做法
如果重复的代码段离调用目标函数的地方还有些距离,则先用移动语句 (223)将这些语句挪动到紧邻目标函数的位置
如果目标函数仅被唯一一个源函数调用,那么只需将源函数中的重复代码段剪 切并粘贴到目标函数中即可,然后运行测试。本做法的后续步骤至此可以忽 略
如果函数不止一个调用点,那么先选择其中一个调用点应用提炼函数 (106),将待搬移的语句与目标函数一起提炼成一个新函数。给新函数取个 临时的名字,只要易于搜索即可
调整函数的其他调用点,令它们调用新提炼的函数。每次调整之后运行测试。
完成所有引用点的替换后,应用内联函数(115)将目标函数内联到新函数 里,并移除原目标函数
对新函数应用函数改名(124),将其改名为原目标函数的名字
搬移语句到调用者
动机
以往在多个地方共用的行为,如今需要在 某些调用点面前表现出不同的行为。于是,我们得把表现不同的行为从函数里挪 出,并搬移到其调用处
做法
最简单的情况下,原函数非常简单,其调用者也只有寥寥一两个,此时只需把 要搬移的代码从函数里剪切出来并粘贴回调用端去即可,必要的时候做些调 整。运行测试。如果测试通过,那就大功告成,本手法可以到此为止。
若调用点不止一两个,则需要先用提炼函数(106)将你不想搬移的代码提炼 成一个新函数,函数名可以临时起一个,只要后续容易搜索即可。
对原函数应用内联函数
对提炼出来的函数应用改变函数声明(124),令其与原函数使用同一个名 字。
以函数调用取代内联代码
动机
一些内联代码,它们做的事情仅仅是已有函数的重复,我通常会 以一个函数调用取代内联代码
做法
将内联代码替代为对一个既有函数的调用
测试。
移动语句
动机
让存在关联的东西一起出现,可以使代码更容易理解
如果有几行代码取用 了同一个数据结构,那么最好是让它们在一起出现,而不是夹杂在取用其他数据 结构的代码中间
第一次需要使用变量的地 方再声明变量
做法
确定待移动的代码片段应该被搬往何处。仔细检查待移动片段与目的地之间的 语句,看看搬移后是否会影响这些代码正常工作。如果会,则放弃这项重构
剪切源代码片段,粘贴到上一步选定的位置上
测试。
拆分循环
动机
但如果你在一次循环中做了两件不同的事,那么每 当需要修改循环时,你都得同时理解这两件事情。如果能够将循环拆分,让一个 循环只做一件事情,那就能确保每次修改时你只需要理解要修改的那块代码的行 为就可以了
拆分循环还能让每个循环更容易使用
做法
复制一遍循环代码。
识别并移除循环中的重复代码,使每个循环只做一件事。
测试。
以管道取代循环
动机
集合管道[mf-cp] 是这样一种技术,它允许我使用一组运算来描述集合的迭代过程,其中每种运算 接收的入参和返回值都是一个集合。
map
filter
做法
创建一个新变量,用以存放参与循环过程的集合
从循环顶部开始,将循环里的每一块行为依次搬移出来,在上一步创建的集合 变量上用一种管道运算替代之。每次修改后运行测试
搬移完循环里的全部行为后,将循环整个删除。
移除死代码
动机
尝试阅读代码、理解软件的运作原理时,无用代码确实会带来很多额外的思维负 担
一旦代码不再被使用,我们就该立马删除它
做法
如果死代码可以从外部直接引用,比如它是一个独立的函数时,先查找一下还 有无调用点。
将死代码移除。
测试
拆分变量
动机
如果变量承担多个责任,它就应该被替换(分 解)为多个变量,每个变量只承担一个责任。同一个变量承担两件不同的事情,
会令代码阅读者糊涂
会令代码阅读者糊涂
做法
在待分解变量的声明及其第一次被赋值处,修改其名称。
如果可能的话,将新的变量声明为不可修改
以该变量的第二次赋值动作为界,修改此前对该变量的所有引用,让它们引用 新变量。
测试
重复上述过程。每次都在声明处对变量改名,并修改下次赋值之前的引用,直 至到达最后一处赋值。
字段改名
动机
命名很重要,对于程序中广泛使用的记录结构,其中字段的命名格外重要
数据结构对于帮助阅读者理解特别重要
做法
如果记录的作用域较小,可以直接修改所有该字段的代码,然后测试。后面的 步骤就都不需要了
如果记录还未封装,请先使用封装记录(162)。
在对象内部对私有字段改名,对应调整内部访问该字段的函数
测试
如果构造函数的参数用了旧的字段名,运用改变函数声明(124)将其改名
运用函数改名(124)给访问函数改名。
以查询取代派生变量
动机
可变数据是软件中最大的错误源头之一
对数据的修改常常导致代码的各个 部分以丑陋的形式互相耦合:在一处修改数据,却在另一处造成难以发现的破 坏
做法
识别出所有对变量做更新的地方。如有必要,用拆分变量(240)分割各个更 新点。
新建一个函数,用于计算该变量的值。
用引入断言(302)断言该变量和计算函数始终给出同样的值
测试
修改读取该变量的代码,令其调用新建的函数。
测试。
用移除死代码(237)去掉变量的声明和赋值
将引用对象改为值对象
动机
值对象通常更容易理解,主要因为它们是不可变的
如果我想 在几个对象之间共享一个对象,以便几个对象都能看见对共享对象的修改,那么 这个共享的对象就应该是引用
做法
检查重构目标是否为不可变对象,或者是否可修改为不可变对象
用移除设值函数(331)逐一去掉所有设值函数
提供一个基于值的相等性判断函数,在其中使用值对象的字段
将值对象改为引用对象
动机
对于一个客观实体,只有一个代表 它的对象。这通常意味着我会需要某种形式的仓库,在仓库中可以找到所有这些 实体对象。只为每个实体创建一次对象,以后始终从仓库中获取该对象。
做法
为相关对象创建一个仓库(如果还没有这样一个仓库的话)
确保构造函数有办法找到关联对象的正确实例。
修改宿主对象的构造函数,令其从仓库中获取关联对象。每次修改后执行测 试
分解条件表达式
动机
复杂的条件逻辑是最常导致复杂度上升的地点之一。
对于条件逻辑,将每个分支条件分解成新函 数还可以带来更多好处:可以突出条件逻辑,更清楚地表明每个分支的作用,并 且突出每个分支的原因。
做法
对条件判断和每个条件分支分别运用提炼函数(106)手法。
合并条件表达式
动机
检查条件各不相同,最终行为却一致
做法
确定这些条件表达式都没有副作用
使用适当的逻辑运算符,将两个相关条件表达式合并为一个。
测试。
重复前面的合并过程,直到所有相关的条件表达式都合并到一起。
可以考虑对合并后的条件表达式实施提炼函数(106)。
以卫语句取代嵌套条件表达式
动机
如果某个条件 极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样 的单独检查常常被称为“卫语句
以卫语句取代嵌套条件表达式的精髓就是:给某一条分支以特别的重视。
做法
选中最外层需要被替换的条件逻辑,将其替换为卫语句。
测试
有需要的话,重复上述步骤
如果所有卫语句都引发同样的结果,可以使用合并条件表达式(263)合并 之
以多态取代条件表达式
动机
复杂的条件逻辑是编程中最难理解的东西之一
将条件逻辑拆分到不同的场景
可以针对switch语句中的每种分支逻辑创建一个类,用多 态来承载各个类型特有的行为,从而去除重复的分支逻辑。
有一个基础逻辑,可以把基础逻辑放进超类,这样我可以首先理解 这部分逻辑,暂时不管各种变体,然后我可以把每种变体逻辑单独放进一个子 类,其中的代码着重强调与基础逻辑的差异
做法
如果现有的类尚不具备多态行为,就用工厂函数创建之,令工厂函数返回恰当 的对象实例
在调用方代码中使用工厂函数获得对象实例
将带有条件逻辑的函数移到超类中。
任选一个子类,在其中建立一个函数,使之覆写超类中容纳条件表达式的那个 函数。将与该子类相关的条件表达式分支复制到新函数中,并对它进行适当调 整。
重复上述过程,处理其他条件分支
在超类函数中保留默认情况的逻辑。或者,如果超类应该是抽象的,就把该函 数声明为abstract,或在其中直接抛出异常,表明计算责任都在子类中
引入特例
动机
一个数据结构的使用者都在检查某个特殊 的值,并且当这个特殊值出现时所做的处理也都相同。如果我发现代码库中有多 处以同样方式应对同一个特殊值,我就会想要把这个处理逻辑收拢到一处。
创建一个特 例元素,用以表达对这种特例的共用行为的处理。这样我就可以用一个函数调用 取代大部分特例检查逻辑。
做法
给重构目标添加检查特例的属性,令其返回false。
创建一个特例对象,其中只有检查特例的属性,返回true。
对“与特例值做比对”的代码运用提炼函数(106),确保所有客户端都使用这 个新函数,而不再直接做特例值的比对。
将新的特例对象引入代码中,可以从函数调用中返回,也可以在变换函数中生 成
修改特例比对函数的主体,在其中直接使用检查特例的属性
测试
使用函数组合成类(144)或函数组合成变换(149),把通用的特例处理逻辑 都搬移到新建的特例对象中。
对特例比对函数使用内联函数(115),将其内联到仍然需要的地方
引入断言
动机
断言是一个条件表达式,应该总是为真。如果它失败,表示程序员犯了错 误
做法
如果你发现代码假设某个条件始终为真,就加入一个断言明确说明这种情况
将查询函数和修改函数分离
动机
任何有返回值的函数,都不应该有看得到的副作用—— 命令与查询分离
将查询所得结果缓存于某个字段中,这样一来后续的重复查询就可以 大大加快速度
做法
复制整个函数,将其作为一个查询来命名
从新建的查询函数中去掉所有造成副作用的语句
执行静态检查
查找所有调用原函数的地方。如果调用处用到了该函数的返回值,就将其改为 调用新建的查询函数,并在下面马上再调用一次原函数。每次修改之后都要测 试。
从原函数中去掉返回值
测试
函数参数化
动机
如果我发现两个函数逻辑非常相似,只有一些字面量值不同,可以将其合并 成一个函数,以参数的形式传入不同的值,从而消除重复
做法
从一组相似的函数中选择一个。
运用改变函数声明(124),把需要作为参数传入的字面量添加到参数列表 中
修改该函数所有的调用处,使其在调用时传入该字面量值
测试。
修改函数体,令其使用新传入的参数。每使用一个新参数都要测试
对于其他与之相似的函数,逐一将其调用处改为调用已经参数化的函数。每次 修改后都要测试
移除标记参数
动机
“标记参数”是这样的一种参数:调用者用它来指示被调函数应该执行哪一部 分逻辑
移除标记参数不仅使代码更整洁,并且能帮助开发工具更好地发挥作用
做法
针对参数的每一种可能值,新建一个明确函数。
如果主函数有清晰的条件分发逻辑,可以用分解条件表达式(260)创建 明确函数;否则,可以在原函数之上创建包装函数。
对于“用字面量值作为参数”的函数调用者,将其改为调用新建的明确函数。
保持对象完整
动机
代码从一个记录结构中导出几个值,然后又把这几个值一起传递 给一个函数,我会更愿意把整个记录传给这个函数,在函数体内部导出所需的 值
“传递整个记录”的方式能更好地应对变化
调用者将自己的若干数据作为参数,传递给被调 用函数。这种情况下,我可以将调用者的自我引用(在JavaScript中就是this)作 为参数,直接传递给目标函数
做法
新建一个空函数,给它以期望中的参数列表(即传入完整对象作为参数)
在新函数体内调用旧函数,并把新的参数(即完整对象)映射到旧的参数列表 (即来源于完整对象的各项数据)
执行静态检查。
逐一修改旧函数的调用者,令其使用新函数,每次修改之后执行测试。
所有调用处都修改过来之后,使用内联函数(115)把旧函数内联到新函数体 内。
给新函数改名,从重构开始时的容易搜索的临时名字,改为使用旧函数的名 字,同时修改所有调用处。
以查询取代参数
动机
函数的参数列表应该总结该函数的可变性,标示出函数可能体现出行为差异 的主要方式
参数列表应该尽量避免重复,并且参数 列表越短就越容易理解
如果调用函数时传入了一个值,而这个值由函数自己来获得也是同样容易, 这就是重复
如果在处理的函数具有引用透明性(referential transparency,即,不论任何时候,只要传入相同的参数值,该函数的行为永远一 致),这样的函数既容易理解又容易测试,我不想使其失去这种优秀品质。我不 会去掉它的参数,让它去访问一个可变的全局变量。
做法
如果有必要,使用提炼函数(106)将参数的计算过程提炼到一个独立的函数 中。
将函数体内引用该参数的地方改为调用新建的函数。每次修改后执行测试
全部替换完成后,使用改变函数声明(124)将该参数去掉。
以参数取代查询
动机
在浏览函数实现时,我有时会发现一些令人不快的引用关系,例如,引用一 个全局变量,或者引用另一个我想要移除的元素
改变代码的依赖关系——为了让目标 函数不再依赖于某个元素,我把这个元素的值以参数形式传递给该函数
做法
对执行查询操作的代码使用提炼变量(119),将其从函数体中分离出来
现在函数体代码已经不再执行查询操作(而是使用前一步提炼出的变量),对 这部分代码使用提炼函数(106)。
使用内联变量(123),消除刚才提炼出来的变量
对原来的函数使用内联函数
对新函数改名,改回原来函数的名字。
移除设值函数
动机
如果为某个字段提供了设值函数,这就暗示这个字段可以被改变
如果不希 望在对象创建之后此字段还有机会被改变,那就不要为它提供设值函数(同时将 该字段声明为不可变)
做法
如果构造函数尚无法得到想要设入字段的值,就使用改变函数声明(124)将 这个值以参数的形式传入构造函数。在构造函数中调用设值函数,对字段设 值。
移除所有在构造函数之外对设值函数的调用,改为使用新的构造函数。每次修 改之后都要测试。
使用内联函数(115)消去设值函数。如果可能的话,把字段声明为不可变
测试
以工厂函数取代构造函数
动机
构造函数又常 有一些丑陋的局限性
做法
新建一个工厂函数,让它调用现有的构造函数
将调用构造函数的代码改为调用工厂函数
每修改一处,就执行测试。
尽量缩小构造函数的可见范围
以命令取代函数
动机
函数,不管是独立函数,还是以方法(method)形式附着在对象上的函数, 是程序设计的基本构造块
将函数封装成自己的对象,有时也是一种有用 的办法。这样的对象我称之为“命令对象”
这种对象大多只服务于单一函数,获得对该函数的请求,执 行该函数,就是这种对象存在的意义。
命令对象的灵活性也是以复杂性作为 代价的。所以,如果要在作为一等公民的函数和命令对象之间做个选择,95%的 时候我都会选函数
做法
为想要包装的函数创建一个空的类,根据该函数的名字为其命名
使用搬移函数(198)把函数移到空的类里。
以函数取代命令
动机
。大多数时候,我只是想调用一个函数,让它完成自己的工作就好。如果这个 函数不是太复杂,那么命令对象可能显得费而不惠,我就应该考虑将其变回普通 的函数。
做法
运用提炼函数(106),把“创建并执行命令对象”的代码单独提炼到一个函数 中。
对命令对象在执行阶段用到的函数,逐一使用内联函数
使用改变函数声明(124),把构造函数的参数转移到执行函数
对于所有的字段,在执行函数中找到引用它们的地方,并改为使用参数。每次 修改后都要测试。
把“调用构造函数”和“调用执行函数”两步都内联到调用方(也就是最终要替换 命令对象的那个函数)。
测试
用移除死代码(237)把命令类消去。
函数上移
动机
如果某个函数在各个子类中的函数体都相同(它们很可能是通过复制粘贴得 到的),这就是最显而易见的函数上移适用场合
做法
检查待提升函数,确定它们是完全一致的
检查函数体内引用的所有函数调用和字段都能从超类中调用到
如果待提升函数的签名不同,使用改变函数声明(124)将那些签名都修改为 你想要在超类中使用的签名
在超类中新建一个函数,将某一个待提升函数的代码复制到其中
执行静态检查。
移除一个待提升的子类函数。
测试
逐一移除待提升的子类函数,直到只剩下超类中的函数为止。
字段上移
动机
子类字段重复
做法
针对待提升之字段,检查它们的所有使用点,确认它们以同样的方式被使用
如果这些字段的名称不同,先使用变量改名(137)为它们取个相同的名字
在超类中新建一个字段。新字段需要对所有子类可见
移除子类中的字段。
测试
构造函数本体上移
动机
如果我看见各个子类中的函数有共同行为,我的第一个念头就是使用提炼函 数(106)将它们提炼到一个独立函数中,然后使用函数上移(350)将这个函数 提升至超类
做法
如果超类还不存在构造函数,首先为其定义一个。确保让子类调用超类的构造 函数。
使用移动语句(223)将子类中构造函数中的公共语句移动到超类的构造函数 调用语句之后。
逐一移除子类间的公共代码,将其提升至超类构造函数中。对于公共代码中引 用到的变量,将其作为参数传递给超类的构造函数。
测试
如果存在无法简单提升至超类的公共代码,先应用提炼函数(106),再利用 函数上移(350)提升之。
函数下移
动机
如果超类中的某个函数只与一个(或少数几个)子类有关,那么最好将其从 超类中挪走,放到真正关心它的子类中去。
做法
将超类中的函数本体复制到每一个需要此函数的子类中
删除超类中的函数
测试
将该函数从所有不需要它的那些子类中删除。
测试
字段下移
动机
如果某个字段只被一个子类(或者一小部分子类)用到,就将其搬移到需要 该字段的子类中。
做法
在所有需要该字段的子类中声明该字段。
将该字段从超类中移除
测试
将该字段从所有不需要它的那些子类中删掉
测试
以子类取代类型码
动机
软件系统经常需要表现“相似但又不同的东西”,比如员工可以按职位分类 (工程师、经理、销售),订单可以按优先级分类(加急、常规)
表现分类关 系的第一种工具是类型码字段——根据具体的编程语言,可能实现为枚举、符 号、字符串或者数字
继承有两个诱人之处。首先,你可以用多态来处理条件逻辑。
有些字段或函数只对特定的类型码取值才有意义,以创建子类,然后用字段下移(361)把 这样的字段放到合适的子类中去,子类的形式能更明确地表达数据与类型之间 的关系
做法
自封装类型码字段
任选一个类型码取值,为其创建一个子类。覆写类型码类的取值函数,令其返 回该类型码的字面量值。
创建一个选择器逻辑,把类型码参数映射到新的子类。
测试
针对每个类型码取值,重复上述“创建子类、添加选择器逻辑”的过程。每次修 改后执行测试。
去除类型码字段
测试
使用函数下移(359)和以多态取代条件表达式(272)处理原本访问了类型码
的函数。全部处理完后,就可以移除类型码的访问函数。
的函数。全部处理完后,就可以移除类型码的访问函数。
移除子类
动机
子类存在着就有成本,阅读者要花心思去理解它的用意,所以如果子类的用 处太少,就不值得存在
做法
使用以工厂函数取代构造函数(334),把子类的构造函数包装到超类的工厂 函数中
如果有任何代码检查子类的类型,先用提炼函数(106)把类型检查逻辑包装 起来,然后用搬移函数(198)将其搬到超类。每次修改后执行测试。
新建一个字段,用于代表子类的类型。
将原本针对子类的类型做判断的函数改为使用新建的类型字段。
删除子类。
测试
提炼超类
动机
如果我看见两个类在做相似的事,可以利用基本的继承机制把它们的相似之 处提炼到超类。
之目的都是把重复的行为收拢一处。
做法
为原本的类新建一个空白的超类
测试
使用构造函数本体上移(355)、函数上移(350)和字段上移(353)手法, 逐一将子类的共同元素上移到超类
检查留在子类中的函数,看它们是否还有共同的成分。如果有,可以先用提炼 函数(106)将其提炼出来,再用函数上移(350)搬到超类。
检查所有使用原本的类的客户端代码,考虑将其调整为使用超类的接口
折叠继承体系
动机
一个类与其超类已经没多大差别,不值得再作为独立的类存 在。此时我就会把超类和子类合并起来。
做法
选择想移除的类:是超类还是子类
使用字段上移(353)、字段下移(361)、函数上移(350)和函数下移 (359),把所有元素都移到同一个类中。
调整即将被移除的那个类的所有引用点,令它们改而引用合并后留下的类。
移除我们的目标;此时它应该已经成为一个空类。
测试
以委托取代子类
动机
继承也有其短板,继承只能用于处理一个方向上的变化
继承给类之间引入了非常紧密的关系。在超类上做任何修 改,都很可能破坏子类
有一条流行的原则:“对象组合优于类继承”。就是用状态 (State)模式或者策略(Strategy)模式取代子类
做法
如果构造函数有多个调用者,首先用以工厂函数取代构造函数(334)把构造 函数包装起来
创建一个空的委托类,这个类的构造函数应该接受所有子类特有的数据项,并 且经常以参数的形式接受一个指回超类的引用
在超类中添加一个字段,用于安放委托对象。
修改子类的创建逻辑,使其初始化上述委托字段,放入一个委托对象的实例
选择一个子类中的函数,将其移入委托类
使用搬移函数(198)手法搬移上述函数,不要删除源类中的委托代码。
如果被搬移的源函数还在子类之外被调用了,就把留在源类中的委托代码从子 类移到超类,并在委托代码之前加上卫语句,检查委托对象存在。如果子类之 外已经没有其他调用者,就用移除死代码(237)去掉已经没人使用的委托代码
测试
重复上述过程,直到子类中所有函数都搬到委托类
找到所有调用子类构造函数的地方,逐一将其改为使用超类的构造函数。
测试
运用移除死代码去掉子类
以委托取代超类
动机
如果超类的一些函数对子 类并不适用,就说明我不应该通过继承来获得超类的功能
做法
在子类中新建一个字段,使其引用超类的一个对象,并将这个委托引用初始化 为超类的新实例。
针对超类的每个函数,在子类中创建一个转发函数,将调用请求转发给委托引 用。每转发一块完整逻辑,都要执行测试。
当所有超类函数都被转发函数覆写后,就可以去掉继承关系。
0 条评论
下一页