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