AI写作智能体 自主规划任务,支持联网查询和网页读取,多模态高效创作各类分析报告、商业计划、营销方案、教学内容等。 广告
曾用名:合并重复的代码片段(Consolidate Duplicate Conditional Fragments) ![](https://box.kancloud.cn/d1da86d6c1e9dfb5756ac35d7833739b_311x151.jpeg) ``` const pricingPlan = retrievePricingPlan(); const order = retreiveOrder(); let charge; const chargePerUnit = pricingPlan.unit; ``` ![](https://box.kancloud.cn/a3bed334e2e1f6d1a46c5039deb25af9_91x152.jpeg) ``` const pricingPlan = retrievePricingPlan(); const chargePerUnit = pricingPlan.unit; const order = retreiveOrder(); let charge; ``` ### 动机 让存在关联的东西一起出现,可以使代码更容易理解。如果有几行代码取用了同一个数据结构,那么最好是让它们在一起出现,而不是夹杂在取用其他数据结构的代码中间。最简单的情况下,我只需使用移动语句就可以让它们聚集起来。此外还有一种常见的“关联”,就是关于变量的声明和使用。有人喜欢在函数顶部一口气声明函数用到的所有变量,我个人则喜欢在第一次需要使用变量的地方再声明它。 通常来说,把相关代码搜集到一处,往往是另一项重构(通常是在提炼函数(106))开始之前的准备工作。相比于仅仅把几行相关的代码移动到一起,将它们提炼到独立的函数往往能起到更好的抽象效果。但如果起先存在关联的代码就没有彼此在一起,那么我也很难应用提炼函数(106)的手法。 ### 做法 - 确定待移动的代码片段应该被搬往何处。仔细检查待移动片段与目的地之间的语句,看看搬移后是否会影响这些代码正常工作。如果会,则放弃这项重构。 > 往前移动代码片段时,如果片段中声明了变量,则不允许移动到任何变量的声明语句之前。往后移动代码片段时,如果有语句引用了待移动片段中的变量,则不允许移动到该语句之后。往后移动代码片段时,如果有语句修改了待移动片段中引用的变量,则不允许移动到该语句之后。往后移动代码片段时,如果片段中修改了某些元素,则不允许移动到任何引用了这些元素的语句之后。 - 剪切源代码片段,粘贴到上一步选定的位置上。 - 测试。 如果测试失败,那么尝试减小移动的步子:要么是减少上下移动的行数,要么是一次搬移更少的代码。 ### 范例 移动代码片段时,通常需要想清楚两件事:本次调整的目标是什么,以及该目标能否达到。第一件事通常取决于代码所在的上下文。最简单的情况是,我希望元素的声明点和使用点互相靠近,因此移动语句的目标便是将元素的声明语句移动到靠近它们的使用处。不过大多数时候,我移动代码的动机都是因为想做另一项重构,比如在应用提炼函数(106)之前先将相关的代码集中到一块,以方便做函数提炼。 确定要把代码移动到哪里之后,我就需要思考第二个问题,也就是此次搬移能否做到的问题。为此我需要观察待移动的代码,以及移动中间经过的代码段,我得思考这个问题:如果我把代码移动过去,执行次序的不同会不会使代码之间产生干扰,甚至于改变程序的可观测行为? 请观察以下代码片段: ``` 1 const pricingPlan = retrievePricingPlan(); 2 const order = retreiveOrder(); 3 const baseCharge = pricingPlan.base; 4 let charge; 5 const chargePerUnit = pricingPlan.unit; 6 const units = order.units; 7 let discount; 8 charge = baseCharge + units * chargePerUnit; 9 let discountableUnits = Math.max(units - pricingPlan.discountThreshold, 0); 10 discount = discountableUnits * pricingPlan.discountFactor; 11 if (order.isRepeat) discount += 20; 12 charge = charge - discount; 13 chargeOrder(charge); ``` 前七行是变量的声明语句,移动它们通常很简单。假如我想把与处理折扣(discount)相关的代码搬移到一起,那么我可以直接将第7行(`let discount`)移动到第10行上面(`discount = ...`那一行)。因为变量声明没有副作用,也不会引用其他变量,所以我可以很安全地将声明语句往后移动,一直移动到引用`discount`变量的语句之上。此种类型的语句移动也十分常见——当我要提炼函数(106)时,通常得先将相关变量的声明语句搬移过来。 我会再寻找类似的没有副作用的变量声明语句。类似地,我可以毫无障碍地把声明了`order`变量的第2行(`const order = ...`)移动到使用它的第6行(`const units = ...`)上面。 上面搬移变量声明语句之所以顺利,除了因为语句本身没有副作用,还得益于我移动语句时跨过的代码片段同样没有副作用。事实上,对于没有副作用的代码,我几乎可以随心所欲地编排它们的顺序,这也是优秀的程序员都会尽量编写无副作用代码的原因之一。 当然,这里还有一个小细节,那就是我从何得知第2行代码没有副作用呢?我只有深入检查`retrieveOrder()`函数的内部实现,才能真正确保它确实没有副作用(除了检查函数本身,还得检查它内部调用的函数都没有副作用,以及它调用的函数内部调用的函数都没有副作用……一直检查到调用链的底端)。实践中,我编写代码总是尽量遵循命令与查询分离(Command-Query Separation)\[mf-cqs\]原则,在这个前提下,我可以确定任何有返回值的函数都不存在副作用。但只有在我了解代码库的前提下才如此自信;如果我对代码库还不熟悉,我就得更加小心。但在我自己的编码过程中,我确实总是尽量遵循命令与查询分离的模式,因为它让我一眼就能看清代码有无副作用,而这件事情真是价值不菲。 如果待移动的代码片段本身有副作用,或者它需要跨越的代码存在副作用,移动它们时就必须加倍小心。我得仔细寻找两个代码片段中间的代码有没有副作用,是不是对执行次序敏感。因此,假设我想将第11行(`if(order.isRepeat)...`)挪动到段落底部,我会发现行不通,因为中间第12行语句引用了`discount`变量,而我在第11行中可能改动这个变量;类似地,假设我想将第13行(`chargeOrder(charge)`)往上搬移,那也是行不通的,因为第13行引用的`charge`变量在第12行会被修改。不过,如果我想将第8行代码(`charge = baseCharge + ...`)移动到第9行到第11行中间的任意地方则是可行的,因为这几行都未修改任何变量的状态。 移动代码时,最容易遵守的一条规则是,如果待移动代码片段中引用的变量在另一个代码片段中被修改了,那我就不能安全地将前者移动到后者之后;同样,如果前者会修改后者中引用的变量,也一样不能安全地进行上述移动。但这条规则仅仅作为参考,它也不是绝对的,比如下面这个例子,虽然两个语句都修改了彼此之间的变量,但我仍能安全地调整它们的先后顺序。 ``` a = a + 10; a = a + 5; ``` 但无论如何,要判断一次语句移动是否安全,都意味着我得真正理解代码的工作原理,以及运算符之间的组合方式等。 正因此项重构如此需要关注状态更新,所以我会尽量移除那些会更新元素状态的代码。比如此例中的`charge`变量,在移动其相关的代码之前,我会先看看是否能对它应用拆分变量(240)手法。 以上的分析都比较简单,没什么难度,因为代码里修改的都是局部变量,局部变量是比较好处理的。但处理更复杂的数据结构时,情况就不同了,判断代码段之间是否存在相互干扰会困难得多。这时测试扮演了重要角色:每次移动代码之后运行测试,看看有没有任何测试失败。如果我的测试覆盖足够全面,我就会对这次重构比较有信心;但如果测试覆盖不够,我就得小心一些了——通常,我会先改善代码的测试,然后再进行重构。 如果移动过后测试失败了,那么意味着我得减小移动的步子,比如一次先移动5行,而不是10行;或者先移动到那些看着比较可能出错的代码上面,但不越过它,看看效果。同时,测试失败也可能是一个征兆,提醒我这次移动可能还不是时候,可能还需要在别处先做一些其他的工作。 ### 范例:包含条件逻辑的移动 对于拥有条件逻辑的代码,移动手法同样适用。当从条件分支中移走代码时,通常是要消除重复逻辑;将代码移入条件分支时,通常是反过来,有意添加一些重复逻辑。 在下面这个例子中,两个条件分支里都有一个相同的语句: ``` let result; if (availableResources.length === 0) {  result = createResource();  allocatedResources.push(result); } else {  result = availableResources.pop();  allocatedResources.push(result); } return result; ``` 我可以将这两句重复代码从条件分支中移走,只在`if-else`块的末尾保留一句。 ``` let result; if (availableResources.length === 0) {  result = createResource(); } else {  result = availableResources.pop(); } allocatedResources.push(result); return result; ``` 这个手法同样可以反过来用,也就是把一个语句分别搬移到不同的条件分支里,这样会在每个条件分支里留下同一段重复的代码。 ### 延伸阅读 除了我介绍的这个方法,我还见过一个十分相似的重构手法,名字叫作“交换语句位置”(Swap Statement)\[wake-swap\]。该手法同样适用于移动相邻的代码片段,只不过它适用的是只有一条语句的片段。你可以把它想成移动语句手法的一个特例,也就是待移动的代码片段以及它所跨过的代码片段,都只有一条语句。我对这项重构手法很感兴趣,毕竟我也一直在强调小步修改——有时甚至小步到于初学重构的人看来都很不可思议的地步。 但最后,我还是选择在本重构手法中介绍如何移动范围更大的代码片段,因为我自己平时就是这么做的。我只有在处理大范围的语句移动遇到困难时才会变得小步、一次只移动一条语句,但即便是这样的困难我也很少遇见。无论如何,当代码过于复杂凌乱时,小步的移动通常会更加顺利。