重构—改善既有代码的设计5——重新组织函数
问题源于:long method
包含太多信息,而信息又被函数错综复杂的逻辑掩盖,不易鉴别。
解决:
extract method:一段代码提取出来,放进一个单独函数中
最大困难:处理局部遍历。临时变量则是其中一个主要的困难源头
解决:
repalce temp with query:去掉所有可去掉的临时变量
split temporary variable:使临时变量变得比较容易替换
replace method with method object:临时变量太混乱,可分解哪怕最混乱的函数,代价则是引入一个新的类
remove assignments to parameters:在函数内赋值给参数
inline method:相反,将一个函数调用动作替换为该函数本体。
substitute algorithm:引入更清晰的算法
1. extract method:提炼函数
一段代码可以被组织在一起并独立出来
针对:
一个过长的函数
一段需要注释,才能让人理解用途的代码
简短、命名良好的函数:
函数粒度小,被复用机会大
高层函数读起来就像一系列注释
函数的细粒度,覆写也更容易。
只有能给小型函数很好地命名时,它们才能真正起作用=》需要在函数名称上下点功夫。
函数的长度:关键在于函数名称、函数本体之间的语义距离。
如果提炼可以强化代码的清晰度,那就去做。就算函数名称比提炼出来的代码还长,也无所谓。
做法:
- 创造一个新函数,根据函数的意图来命名:以“做什么”来命名,而不是以“怎样做”命名。
即使想要提炼的代码非常简单(一条消息,一个函数调用),只要新函数的名称能够以更好方式昭示代码意图,也应该提炼它。如果想不出一个更有意义的名称,就别动
- 将提炼出的代码从源函数复制到新建的目标函数中
- 仔细检查提炼出的代码,看是否引用了“作用域限于源函数”的变量,包括局部变量、源函数参数
- 检查被提炼的代码段,看看是否有任何局部变量的值被它改变。
如果一个临时变量值被修改了,看是否可以将被提炼的代码段处理为一个查询,并将结果赋值给相关变量
如果很难这样做,或被修改的变量不止一个,就不能仅仅将这段代码原封不动地提炼出来。使用 split temporary variable,再尝试提炼;或使用 replace temp with query 将临时变量消灭掉
- 将被提炼代码段中需要读取的局部变量,当作参数传给目标函数
- 处理完所有局部变量之后,进行编译
- 在源函数中,将被提炼代码段替换为对目标函数的调试
如果将任何临时变量移到目标函数中,请检查它们原本的声明式是否在被提炼代码段的外围。如果是,则可以删除这些声明式了
- 编译、测试
2.inline method:内联函数
3.inline temp:内联临时变量
有一个临时变量,只被一个简单表达式赋值一次,妨碍了其他重构手法。
解决:将所有对该变量的引用动作,替换为对它赋值的那个表达式自身
情境:
多半作为replace temp with query的一部分使用,所以真正的动机出现在后者那儿
唯一单独使用inline temp,发现某个临时变量被赋予某个函数调用的返回值。一般这样的临时变量不会有任何危害,可以放心地把它留在那儿。如果这个临时变量妨碍了其他的重构手法,可以使用extract method内联化。
做法:
1.检查给临时变量赋值的语句,确保等号右边的表达式没有副作用
2.如果临时变量未被声明为final,就将它声明为final,然后编译。(可以检查该临时变量是否真的只被赋值一次)
3.找到该临时变量的所有引用点,将其替换为“为临时变量赋值”的表达式
4.每次修改后,编译并测试
5.修改完所有的引用点之后,删除该临时变量的声明、赋值语句
6.编译、测试
4.replace temp with query:以查询取代临时变量
以一个临时变量保存某一表达式的运算结果
解决:将这个表达式提炼到一个独立函数中。将这个临时变量的所有引用点替换为对新函数的调用。此后,新函数就可被其他函数使用
动机:
临时变量的问题:它们是暂时的,而且只能在所属函数内使用。
由于临时变量只在所属函数内可见,会驱使你写出更长的函数。
如果将临时变量替换为一个查询,那么同一个类中的所有函数都将获得这份信息。有助于为此类编写更为清晰的代码
replace temp with query 往往是运用extract method之前必不可少的一个步骤。局部变量会使代码难以被提炼,应尽可能将其替换为查询式
简单:临时变量只被赋值一次,或赋值给临时变量的表达式不受其他条件影响
复杂:需要先运用split temporary variable、separate query from modifier使情况变得简单一些,然后再替换临时变量。
如果想替换的临时变量是用来收集结果的,需要将某些程序逻辑复制到查询函数去
做法:
如果某个临时变量被赋值超过一次,使用split temporary variable将其分割成多个变量
确保提炼出的函数无副作用。即函数并不修改任何对象内容,如果有副作用,进行seperate query from modifler
性能:不要担心性能问题,9/10不会有任何影响。真有影响,可以再优化时期解决。代码组织良好,往往可以发现更有效的优化方案,如果没有进行重构,好的优化方案就可能与你失之交臂。如果性能实在太糟,将临时变量放回去也是很容易的
5.introduce explaining variable:引入解释性变量
有一个复杂的表达式
将该表达式(或其中一部分)的结果放进一个临时变量,以此临时变量名称来解释表达式用途
动机:
表达式非常复杂,难以阅读。临时变量可以帮助将表达式分解为较为容易管理的形式
条件逻辑中,特别有价值:将每个条件子句提炼出来,用良好命名的临时变量来解释对应条件子句的意义
较长的算法中,用临时变量来解释每一步运算的意义
不常用,尽量使用extract method来解释一段代码的意义。临时变量只有再所处的那个函数中才有意义,局限性较大,函数则可以在对象的整个声明周期都有用,且可被其他对象使用
当局部变量使用extract method难以进行时,使用introduce explaining variable
做法:
如果被替换的这一部分在代码中重复出现,可以每一次一个,逐一替换
6.split temporary variable:分解临时变量
某个临时变量被赋值超过一次,既不是循环变量,也不被用于收集计算结果。
解决:针对每次赋值,创造一个独立、应对的临时变量
动机:
临时变量有各种不同用途,某些用途会很自然地导致临时变量被多次赋值。“循环变量”、“结果收集变量”
临时变量用于保存一段冗长代码的运算结果,以便稍后使用。这种临时变量应只被赋值一次。对超过一次,意味着在函数中承担了一个以上的责任。
如果临时变量承担了多个责任,应该被替换、分解为多个临时变量,每个变量只承担一个责任。否则会令代码阅读者糊涂
做法:
如果稍后的赋值语句【i=i+某表达式】。意味着是个“结果收集变量”=>不要分解它。“结果收集变量”的作用通常是累加、字符串接合、写入流、向集合添加元素
7.remove assignments to parameters:移除对参数的赋值
代码对一个参数进行赋值。
以一个临时变量取代该参数的位置。
动机:
对参数赋值,意味着改变参数,使其指向另一个对象的引用。
如果在“被传入对象”身上进行操作,则不是问题
使用“out 参数”的,可以不必遵循这条规则。但应尽力避免
缺点:
-
- 降低了代码的清晰度,混用了按值传递、按引用传递,这两种参数传递方式。
按值传递,对参数的任何修改,不会对调用端造成任何影响;按引用传递,会产生影响
-
- 在函数本体内,只以参数表示:被传递进来的东西,代码会清晰很多。此用法在所有语言中都表现出相同语义
做法:
-
- 不要对参数赋值:可使用remove assignments to parameters来避免
- 如果代码是“按引用传递”的,请在调用端检查调用后是否还使用了这个参数
- 要检查有多少个按引用传递的参数被赋值后又被使用
- 请尽量以return方式返回一个值。如果需返回的值不止一个,看是否可把需返回的大堆数据变成一个单一对象,或干脆为每个返回值设计对应的一个独立函数
- 可为参数加上final关键词,使之遵循“不对参数赋值”,这一惯例。
- 不建议使用,对于提高函数清晰度没有太大的帮助。
- 通常用在较长的函数中,帮助检查参数是否被修改
8.replace method with method object:以函数对象取代函数
有一个大型函数,其中对局部变量的使用,使人无法采用extract method
做法:
将这个函数放进一个单独对象中,这样局部变量就成了对象内的字段。然后可以在同一个对象中,将这个大型函数分解为多个小型函数
动机:
小型函数优美动人。只要将相对对立的代码从大型函数中提炼出来,可以大大提高代码的可读性
局部变量的存在会增加函数的分解难度。如果一个函数中局部变量泛滥成灾,想分解这个函数是非常困难的
replace temp with query 可以帮助减轻这一负担。但有时候会发现根本无法拆解一个需要拆解的函数
replace method with method object 将所有局部变量都变成函数对象的字段=》对这个新对象使用extract method 创造出新函数,从而将原来的大型函数拆解变短
做法:
建立一个新类,根据待处理函数的用途为此类命名
在新类中建立一个final字段,用以保存原先大型函数所在的对象。即“源对象”。针对源函数的每个临时变量,每个参数在新类中建立一个对应的字段保存
在新类中建立一个构造函数,接受源对象、源函数的所有参数作为参数
在新类中建立一个compute()函数
将原函数中的代码赋值到compute()函数,对需要调用源对象的任何函数,通过源对象字段调用
编译
将旧函数的函数本体替换为这样一条语句“创建上述新类的一个新对象,而后调用其中的compute()函数”
所有的局部变量都变成了字段,可以任意分解这个大型函数,不必传递任何参数
9.substitute algorithm:替换算法
把某个算法替换为另一个更清晰的算法
将函数本体替换为另一个算法
动机:
解决问题有好几种方法,某些方法会比另一些简单,算法也是如此
如果做一件事儿,可以有更清晰的方式,应该以比较清晰的方式取代复杂的方式
随着对问题有更多理解,往往发现在原先的做法之外,有更简单的解决方案,就需要改变原先的算法
如果开始使用程序库,而其中提供的某些功能、特性与你自己的代码重复,则需要改变原先的算法
“重构”可以将一些复杂的东西分解为较简单的小块,但有时必须壮士断腕,删掉整个算法,代之以较为简单的算法
有时想要修改原先的算法,让其做一件与原先略有差异的事。可以先把原先的算法替换为一个较易修改的算法,后续的修改会轻松许多
使用此项重构手法之前,先确定自己已经尽可能分解了原先函数。替换一个巨大、复杂的算法是非常困难的,只有先将它分解为较简单的小型函数,然后才可很有把握的进行算法替换工作
做法:
对于每个测试用例,分别以新旧两种算法执行,并观察两者结果是否相同。可以帮助看到哪个测试用例出现麻烦,以及出现了怎样的麻烦
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· 面试官:你是如何进行SQL调优的?