重构:改善现有代码的设计
2021-07-05 19:21:56 2 举报
AI智能生成
重构:改善现有代码的设计
作者其他创作
大纲/内容
重构方法
提炼函数
创造一个新函数,根据这个函数的意图来对它命名(以它“做什么”来命名,而不是以它“怎样做”命名)。
将待提炼的代码从源函数复制到新建的目标函数中
仔细检查提炼出的代码,看看其中是否引用了作用域限于源函数、在提炼出的新函数中访问不到的变量。若是,以参数的形式将它们传递给新函数
所有变量都处理完之后,编译运行
在源函数中,将被提炼代码段替换为对目标函数的调用
测试
查看其他代码是否有与被提炼的代码段相同或相似之处。如果有,考虑使用以函数调用取代内联代码令其调用提炼出的新函数
内联函数
检查函数,确定它不具多态性
找出这个函数的所有调用点
将这个函数的所有调用点都替换为函数本体
每次替换之后,执行测试。
删除该函数的定义
提炼变量
确认要提炼的表达式没有副作用
声明一个不可修改的变量,把你想要提炼的表达式复制一份,以该表达式的结果值给这个变量赋值
用这个新变量取代原来的表达式
测试
内联变量
检查确认变量赋值语句的右侧表达式没有副作用
如果变量没有被声明为不可修改,先将其变为不可修改,并执行测试
找到第一处使用该变量的地方,将其替换为直接使用赋值语句的右侧表达式
测试
重复前面两步,逐一替换其他所有使用该变量的地方
删除该变量的声明点和赋值语句
测试
改变函数声明
如果想要移除一个参数,需要先确定函数体内没有使用该参数
如果想要移除一个参数,需要先确定函数体内没有使用该参数
找出所有使用旧的函数声明的地方,将它们改为使用新的函数声明
测试
如果有必要的话,先对函数体内部加以重构,使后面的提炼步骤易于开展
使用提炼函数将函数体提炼成一个新函数
如果提炼出的函数需要新增参数,用前面的简单做法添加即可
测试
对旧函数使用内联函数
如果新函数使用了临时的名字,再次使用改变函数声明将其改回原来的名字
测试
封装变量
创建封装函数,在其中访问和更新变量值
执行静态检查
逐一修改使用该变量的代码,将其改为调用合适的封装函数。每次替换之后,执行测试
限制变量的可见性
测试
如果变量的值是一个记录,考虑使用封装记录
变量改名
如果变量被广泛使用,考虑运用封装变量将其封装起来
找出所有使用该变量的代码,逐一修改
测试
引入参数对象
如果暂时还没有一个合适的数据结构,就创建一个
测试
使用改变函数声明给原来的函数新增一个参数,类型是新建的数据结
构
构
测试
调整所有调用者,传入新数据结构的适当实例。每修改一处,执行测试
用新数据结构中的每项元素,逐一取代参数列表中与之对应的参数项,然后删除原来的参数。测试
函数组合成类
运用封装记录对多个函数共用的数据记录加以封装
对于使用该记录结构的每个函数,运用搬移函数将其移入新类
用以处理该数据记录的逻辑可以用提炼函数提炼出来,并移入新类
函数组合成变换
创建一个变换函数,输入参数是需要变换的记录,并直接返回该记录的值
挑选一块逻辑,将其主体移入变换函数中,把结果作为字段添加到输出记录中。修改客户端代码,令其使用这个新字段
测试
针对其他相关的计算逻辑,重复上述步骤。
拆分阶段
将第二阶段的代码提炼成独立的函数
测试
引入一个中转数据结构,将其作为参数添加到提炼出的新函数的参数列表中
测试
逐一检查提炼出的“第二阶段函数”的每个参数。如果某个参数被第一阶段用到,就将其移入中转数据结构。每次搬移之后都要执行测试
对第一阶段的代码运用提炼函数,让提炼出的函数返回中转数据结
构
构
封装记录
对持有记录的变量使用封装变量,将其封装到一个函数中
创建一个类,将记录包装起来,并将记录变量的值替换为该类的一个实例。然
后在类上定义一个访问函数,用于返回原始的记录。修改封装变量的函数,令
其使用这个访问函数
后在类上定义一个访问函数,用于返回原始的记录。修改封装变量的函数,令
其使用这个访问函数
测试
新建一个函数,让它返回该类的对象,而非那条原始的记录
对于该记录的每处使用点,将原先返回记录的函数调用替换为那个返回实例对
象的函数调用。使用对象上的访问函数来获取数据的字段,如果该字段的访问
函数还不存在,那就创建一个。每次更改之后运行测试
象的函数调用。使用对象上的访问函数来获取数据的字段,如果该字段的访问
函数还不存在,那就创建一个。每次更改之后运行测试
移除类对原始记录的访问函数,那个容易搜索的返回原始数据的函数也要一并删除
测试
如果记录中的字段本身也是复杂结构,考虑对其再次应用封装记录或封装集合手法
封装集合
如果集合的引用尚未被封装起来,先用封装变量封装它
在类上添加用于“添加集合元素”和“移除集合元素”的函数
执行静态检查
查找集合的引用点。如果有调用者直接修改集合,令该处调用使用新的添加/移除元素的函数。每次修改后执行测试
修改集合的取值函数,使其返回一份只读的数据,可以使用只读代理或数据副本
测试
以对象取代基本类型
如果变量尚未被封装起来,先使用封装变量封装它
为这个数据值创建一个简单的类。类的构造函数应该保存这个数据值,并为它提供一个取值函数
执行静态检查
修改第一步得到的设值函数,令其创建一个新类的对象并将其存入字段,如果有必要的话,同时修改字段的类型声明
修改取值函数,令其调用新类的取值函数,并返回结果
测试
考虑对第一步得到的访问函数使用函数改名,以便更好反映其用途
考虑应用将引用对象改为值对象或将值对象改为引用对象,明确指出新对象的角色是值对象还是引用对象
以查询取代临时变量
检查变量在使用前是否已经完全计算完毕,检查计算它的那段代码是否每次都能得到一样的值
如果变量目前不是只读的,但是可以改造成只读变量,那就先改造它
测试
将为变量赋值的代码段提炼成函数
测试
应用内联变量手法移除临时变量
提炼类
决定如何分解类所负的责任
创建一个新的类,用以表现从旧类中分离出来的责任
构造旧类时创建一个新类的实例,建立“从旧类访问新类”的连接关系
对于你想搬移的每一个字段,运用搬移字段搬移之。每次更改后运行测试
使用搬移函数将必要函数搬移到新类。先搬移较低层函数(也就是“被其他函数调用”多于“调用其他函数”者)。每次更改后运行测试
检查两个类的接口,去掉不再需要的函数,必要时为函数重新取一个适合新环境的名字
决定是否公开新的类。如果确实需要,考虑对新类应用将引用对象改为值对象使其成为一个值对象
内联类
对于待内联类(源类)中的所有public函数,在目标类上创建一个对应的函数,新创建的所有函数应该直接委托至源类
修改源类public方法的所有引用点,令它们调用目标类对应的委托方法。每次更改后运行测试
将源类中的函数与数据全部搬移到目标类,每次修改之后进行测试,直到源类变成空壳为止
删除源类,为它举行一个简单的“丧礼”
隐藏委托关系
对于每个委托关系中的函数,在服务对象端建立一个简单的委托函数
调整客户端,令它只调用服务对象提供的函数。每次调整后运行测试。
如果将来不再有任何客户端需要取用Delegate(受托类),便可移除服务对象中的相关访问函数
测试
移除中间人
为受托对象创建一个取值函数
对于每个委托函数,让其客户端转为连续的访问函数调用。每次替换后运行测试
替换算法
整理一下待替换的算法,保证它已经被抽取到一个独立的函数中
先只为这个函数准备测试,以便固定它的行为
准备好另一个(替换用)算法
执行静态检查
运行测试,比对新旧算法的运行结果。如果测试通过,那就大功告成;否则,在后续测试和调试过程中,以旧算法为比较参照标准
搬移函数
检查函数在当前上下文里引用的所有程序元素(包括变量和函数),考虑是否需要将它们一并搬移
检查待搬移函数是否具备多态性
将函数复制一份到目标上下文中。调整函数,使它能适应新家
执行静态检查
设法从源上下文中正确引用目标函数
设法从源上下文中正确引用目标函数
测试
考虑对源函数使用内联函数
搬移字段
确保源字段已经得到了良好封装
测试
在目标对象上创建一个字段(及对应的访问函数)
执行静态检查
确保源对象里能够正常引用目标对象
调整源对象的访问函数,令其使用目标对象的字段
测试
移除源对象上的字段
测试
搬移语句到函数
如果重复的代码段离调用目标函数的地方还有些距离,则先用移动语句将这些语句挪动到紧邻目标函数的位置
如果目标函数仅被唯一一个源函数调用,那么只需将源函数中的重复代码段剪切并粘贴到目标函数中即可,然后运行测试。本做法的后续步骤至此可以忽略
如果函数不止一个调用点,那么先选择其中一个调用点应用提炼函数,将待搬移的语句与目标函数一起提炼成一个新函数。给新函数取个临时的名字,只要易于搜索即可
调整函数的其他调用点,令它们调用新提炼的函数。每次调整之后运行测试
完成所有引用点的替换后,应用内联函数将目标函数内联到新函数里,并移除原目标函数
对新函数应用函数改名,将其改名为原目标函数的名字
搬移语句到调用者
最简单的情况下,原函数非常简单,其调用者也只有寥寥一两个,此时只需把要搬移的代码从函数里剪切出来并粘贴回调用端去即可,必要的时候做些调整。运行测试。如果测试通过,那就大功告成,本手法可以到此为止
若调用点不止一两个,则需要先用提炼函数将你不想搬移的代码提炼成一个新函数,函数名可以临时起一个,只要后续容易搜索即可。
对原函数应用内联函数
对提炼出来的函数应用改变函数声明,令其与原函数使用同一个名字
以函数调用取代内联代码
将内联代码替代为对一个既有函数的调用
测试
移动语句
确定待移动的代码片段应该被搬往何处。仔细检查待移动片段与目的地之间的语句,看看搬移后是否会影响这些代码正常工作。如果会,则放弃这项重构
剪切源代码片段,粘贴到上一步选定的位置上
测试
拆分循环
复制一遍循环代码
识别并移除循环中的重复代码,使每个循环只做一件事
测试
以管道取代循环
创建一个新变量,用以存放参与循环过程的集合
从循环顶部开始,将循环里的每一块行为依次搬移出来,在上一步创建的集合变量上用一种管道运算替代之。每次修改后运行测试
搬移完循环里的全部行为后,将循环整个删除
移除死代码
如果死代码可以从外部直接引用,比如它是一个独立的函数时,先查找一下还有无调用点
将死代码移除
测试
拆分变量
在待分解变量的声明及其第一次被赋值处,修改其名称
如果可能的话,将新的变量声明为不可修改
以该变量的第二次赋值动作为界,修改此前对该变量的所有引用,让它们引用新变量
测试
重复上述过程。每次都在声明处对变量改名,并修改下次赋值之前的引用,直至到达最后一处赋值
字段改名
如果记录的作用域较小,可以直接修改所有该字段的代码,然后测试。后面的步骤就都不需要了
如果记录还未封装,请先使用封装记录
在对象内部对私有字段改名,对应调整内部访问该字段的函数
测试
如果构造函数的参数用了旧的字段名,运用改变函数声明将其改名
运用函数改名给访问函数改名
以查询取代派生变量
识别出所有对变量做更新的地方。如有必要,用拆分变量分割各个更新点
新建一个函数,用于计算该变量的值
用引入断言断言该变量和计算函数始终给出同样的值
测试
修改读取该变量的代码,令其调用新建的函数
测试
用移除死代码去掉变量的声明和赋值
将引用对象改为值对象
检查重构目标是否为不可变对象,或者是否可修改为不可变对象
用移除设值函数逐一去掉所有设值函
提供一个基于值的相等性判断函数,在其中使用值对象的字段
将值对象改为引用对象
为相关对象创建一个仓库(如果还没有这样一个仓库的话)
为相关对象创建一个仓库(如果还没有这样一个仓库的话)
修改宿主对象的构造函数,令其从仓库中获取关联对象。每次修改后执行测试
分解条件表达式
对条件判断和每个条件分支分别运用提炼函数手法
合并条件表达式
确定这些条件表达式都没有副作用
使用适当的逻辑运算符,将两个相关条件表达式合并为一个
测试
重复前面的合并过程,直到所有相关的条件表达式都合并到一起
可以考虑对合并后的条件表达式实施提炼函数
以卫语句取代嵌套条件表达式
选中最外层需要被替换的条件逻辑,将其替换为卫语句
测试
有需要的话,重复上述步骤
如果所有卫语句都引发同样的结果,可以使用合并条件表达式合并之
以多态取代条件表达式
如果现有的类尚不具备多态行为,就用工厂函数创建之,令工厂函数返回恰当的对象实例
在调用方代码中使用工厂函数获得对象实例
将带有条件逻辑的函数移到超类中
任选一个子类,在其中建立一个函数,使之覆写超类中容纳条件表达式的那个函数。将与该子类相关的条件表达式分支复制到新函数中,并对它进行适当调整
重复上述过程,处理其他条件分支
在超类函数中保留默认情况的逻辑。或者,如果超类应该是抽象的,就把该函数声明为abstract,或在其中直接抛出异常,表明计算责任都在子类中。
引入特例
给重构目标添加检查特例的属性,令其返回false
创建一个特例对象,其中只有检查特例的属性,返回true
对“与特例值做比对”的代码运用提炼函数,确保所有客户端都使用这个新函数,而不再直接做特例值的比对
将新的特例对象引入代码中,可以从函数调用中返回,也可以在变换函数中生成
修改特例比对函数的主体,在其中直接使用检查特例的属性
测试
使用函数组合成类或函数组合成变换,把通用的特例处理逻辑都搬移到新建的特例对象中
对特例比对函数使用内联函数,将其内联到仍然需要的地方
引入断言
如果你发现代码假设某个条件始终为真,就加入一个断言明确说明这种情况
将查询函数和修改函数分离
复制整个函数,将其作为一个查询来命名
从新建的查询函数中去掉所有造成副作用的语句。
执行静态检查
查找所有调用原函数的地方。如果调用处用到了该函数的返回值,就将其改为调用新建的查询函数,并在下面马上再调用一次原函数。每次修改之后都要测试
从原函数中去掉返回值
测试
函数参数化
从一组相似的函数中选择一个
运用改变函数声明,把需要作为参数传入的字面量添加到参数列表中
修改该函数所有的调用处,使其在调用时传入该字面量值
测试
修改该函数所有的调用处,使其在调用时传入该字面量值
对于其他与之相似的函数,逐一将其调用处改为调用已经参数化的函数。每次修改后都要测试
移除标记参数
针对参数的每一种可能值,新建一个明确函数
对于“用字面量值作为参数”的函数调用者,将其改为调用新建的明确函数
保持对象完整
新建一个空函数,给它以期望中的参数列表(即传入完整对象作为参数)
在新函数体内调用旧函数,并把新的参数(即完整对象)映射到旧的参数列表(即来源于完整对象的各项数据)
执行静态检查
逐一修改旧函数的调用者,令其使用新函数,每次修改之后执行测试
所有调用处都修改过来之后,使用内联函数把旧函数内联到新函数体内
给新函数改名,从重构开始时的容易搜索的临时名字,改为使用旧函数的名字,同时修改所有调用处
以查询取代参数
如果有必要,使用提炼函数将参数的计算过程提炼到一个独立的函数中。
将函数体内引用该参数的地方改为调用新建的函数。每次修改后执行测试
全部替换完成后,使用改变函数声明将该参数去掉。
以参数取代查询
对执行查询操作的代码使用提炼变量,将其从函数体中分离出来
现在函数体代码已经不再执行查询操作(而是使用前一步提炼出的变量),对这部分代码使用提炼函数
使用内联变量,消除刚才提炼出来的变量
对原来的函数使用内联函数
对新函数改名,改回原来函数的名字
移除设值函数
如果构造函数尚无法得到想要设入字段的值,就使用改变函数声明将这个值以参数的形式传入构造函数。在构造函数中调用设值函数,对字段设值。
移除所有在构造函数之外对设值函数的调用,改为使用新的构造函数。每次修改之后都要测试
使用内联函数消去设值函数。如果可能的话,把字段声明为不可变
测试
以工厂函数取代构造函数
新建一个工厂函数,让它调用现有的构造函数
将调用构造函数的代码改为调用工厂函数
每修改一处,就执行测试
尽量缩小构造函数的可见范围
以命令取代函数
为想要包装的函数创建一个空的类,根据该函数的名字为其命名
使用搬移函数把函数移到空的类里
可以考虑给每个参数创建一个字段,并在构造函数中添加对应的参数
以函数取代命令
运用提炼函数,把“创建并执行命令对象”的代码单独提炼到一个函数中
对命令对象在执行阶段用到的函数,逐一使用内联函数
使用改变函数声明,把构造函数的参数转移到执行函数
对于所有的字段,在执行函数中找到引用它们的地方,并改为使用参数。每次修改后都要测试
把“调用构造函数”和“调用执行函数”两步都内联到调用方(也就是最终要替换命令对象的那个函数)
测试
用移除死代码把命令类消去
函数上移
检查待提升函数,确定它们是完全一致的
检查函数体内引用的所有函数调用和字段都能从超类中调用到
如果待提升函数的签名不同,使用改变函数声明将那些签名都修改为你想要在超类中使用的签名
在超类中新建一个函数,将某一个待提升函数的代码复制到其中
执行静态检查
移除一个待提升的子类函数
测试
逐一移除待提升的子类函数,直到只剩下超类中的函数为止
字段上移
针对待提升之字段,检查它们的所有使用点,确认它们以同样的方式被使用
如果这些字段的名称不同,先使用变量改名为它们取个相同的名字
在超类中新建一个字段
子主题
子主题
子主题
子主题
子主题
子主题
重构的原则
重构的定义
(n)对软件内部结构的一种调整,目的是在不改变软件可观察行
为的前提下,提高其可理解性,降低其修改成本
为的前提下,提高其可理解性,降低其修改成本
(v)使用一系列重构手法,在不改变软件可观察行为的前提下,
调整其结构
调整其结构
重构的关键
运用大量微小且保持软件行为的步骤,一步步达成大规模的修改
代码很少进入不可工作的状态,即便重构没有完成,也可以在任何时刻停下来
整体而言,经过重构之后的代码所做的事应该与重构之前大致一样
和“结构调整”的区别
“结构调整”泛指对代码库进行的各种形式的重新组织或清理,重构则是特定的一类结构调整
和“性能优化”的区别
相同点:两者都需要修改代码,并且两者都不会改变程序的整体功能。
不同点:目的不同-重构是为了让代码更容易理解,更易于修改,性能优化是为了让程序运行得更快;时机不同:重构可以在任何时期,性能优化一般在开发阶段的后期
软件开发的两顶帽子
添加新功能:不应该修改既有代码,只管添加新功能并通过测试
重构:不能再添加功能,只管调整代码的结构,并通过已有测试
为何重构
改进软件设计:如果没有重构,程序的内部设计(或者叫架构)会逐渐腐败变质;消除重复代码
使软件更容易理解:让代码更易读
帮助找到bug:重构时对代码的理解,可以帮助找到bug
提高编程速度:添加新的功能时候顾虑少一点,思路清晰
何时重构
三次法则:事不过三,三则重构
添加新功能时(预备性重构):让修改多处的代码变成修改一处,让添加新功能更容易,降低出现bug的概率
看代码时(帮助理解的重构):使代码更容易懂,让代码做到一目了然
code review时(捡垃圾式重构):复审代码时感觉不好 如果有时间就改了
有计划的重构、长期重构:大多数重构可以在几分钟最多几小时内完成。但有一些大型的重构
可能要花上几个星期
可能要花上几个星期
何时不应该重构
重写比重构简单时
不需要修改的代码
项目接近最后期限时,应该避免重构
代码的坏味道
神秘命名:难以理解,背后的设计也可能有问题
整洁代码最重要的一环就是好的名字,所以改名可能是最常用的重构手法,包括改变函数声明(用于给函数改名)、变
量改名、字段改名等
量改名、字段改名等
重复代码:难以修改,容易遗漏
要修改重复代码,就必须找出所有的副本来修改。
过长函数:难以理解
活得最长、最好的程序,其中的函数都比较短。活得最长、最好的程序,其中的函数都比较短
过长参数列表:难以理解和使用
全局数据:容易被污染
可变数据:经常导致出乎意料的结果和难以发现的bug
发散式变化:一处有变化要改多处(如不同模块使用同一个函数,如果函数的参数个数变化,那在多个模块调用的时候都要改)。(应该把函数拆分在不同的模块中,不共用)
霰弹式修改:一处有变化要改多处。(应该定义一个统一的常量,其他地方引用,这样有变化时只修改一处就可以了)
依恋情结:一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流
数据泥团:两个类中相同的字段、许多函数签名中相同的参数总是以字段的形式出现(这些总是绑在一起出现的数据应该提炼到一个独立的对象中)
基本类型偏执:把钱、坐标、范围等非基本类型的值作为字符串进行操作。(可以运用以对象取代基本类型将原本单独存在的数据值替换为对象)
循环语句:可以使用以管道取代循环,管道操作(如filter和map)可以帮助我们更快地看清被处理的元素以及处理它们的动作
冗赘的元素:程序元素(如类和函数)能给代码增加结构,从而支持变化、促进复用或者哪怕只是提供更好的名字也好,但有时我们真的不需要这层额外的结构
夸夸其谈通用性:以各式各样的钩子和特殊情况来处理一些非必要的事情
临时字段
过长的消息链:向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象……这就是消息链
中间人:到某个类的接口有一半的函数都委托给其他类,这样就是过度运用
内幕交易:模块之间的数据交换很难完全避免,但应该都放到明面上来
过大的类:利用单个类做过多事情
异曲同工的类
纯数据类:们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物
被拒绝的遗赠:子类继承父类的所有函数和数据,子类只挑选几样来使用。为子类新建一个兄弟类,再运用下移方法和下移字段把用不到的函数下推个兄弟类。子类只复用了父类的行为,却不想支持父类的接口。运用委托替代继承来达到目的
注释:注释不是用来补救劣质代码的,事实上如果我们去除了代码中的所有坏味道,当劣质代码都被移除的时候,注释已经变得多余,因为代码已经讲清楚了一切
收藏
0 条评论
下一页