提取函数(Extract Function |106)
曾用名:提炼函数(Extract Function)
反向重构:内联函数(115)
示例代码
function printOwing(invoice) {
printBanner();
let outstanding = calculateOutstanding();
//print details
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
}
function printOwing(invoice) {
printBanner();
let outstanding = calculateOutstanding();
// 提炼打印日志函数
printDetails(outstanding);
function printDetails(outstanding) {
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
}
}
动机
浏览一段代码,理解其作用,然后将其提炼到一个独立的函数中,并以这段代码的用途为这个函数命名。
何时应该把代码放进独立的函数(参考):
- 一个函数应该能在一屏中显示
- 只要被用过不止一次的代码,就应该单独放进一个函数
- 只用过一次的代码则保持内联(inline)的状态
如果需要花时间浏览一段代码才能弄清它到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的事为其命名。以后再读到这段代码时,一眼就能看到函数的用途,大多数时候根本不需要关心函数如何达成其用途这是函数体内干的事)。
函数得有个好名字才行,所以必须在命名上花心思。起好名字需要练习,不过一旦你掌握了其中的技巧,就能写出很有自描述性的代码。
在一个大函数中,一段代码的顶上放着一句注释,说明这段代码要做什么。在把这段代码提炼到自己的函数中时,这样的注释往往会提示一个好名字。
做法
-
创造一个新函数,根据这个函数的意图来对它命名(以它“做什么”来命名,而不是以它“怎样做”命名)。
-
如果想不出一个更有意义的名称,这就是一个信号,可能不应该提炼这块代码。不过,不一定非得马上想出最好的名字,有时在提炼的过程中好的名字才会出现。有时会提炼一个函数,尝试使用它,然后发现不太合适,再把它内联回去,这完全没问题。
-
如果编程语言支持嵌套函数,就把新函数嵌套在源函数里,这能减少后面需要处理的超出作用域的变量个数。
-
-
将待提炼的代码从源函数复制到新建的目标函数中。
-
仔细检查提炼出的代码,看看其中是否引用了作用域限于源函数、在提炼出的新函数中访问不到的变量。若是,以参数的形式将它们传递给新函数。
-
如果提炼出的新函数嵌套在源函数内部,就不存在变量作用域的问题了。
-
如果某个变量是在提炼部分之外声明但只在提炼部分被使用,就把变量声明也搬移到提炼部分代码中去。
-
如果变量按值传递给提炼部分又在提炼部分被赋值,就必须多加小心。如果只有一个这样的变量,可以尝试将提炼出的新函数变成一个查询(query),用其返回值给该变量赋值。
-
但有时在提炼部分被赋值的局部变量太多,这时最好是先放弃提炼。这种情况下,我会考虑先使用别的重构手法,例如拆分变量(240)或者以查询取代临时变量(178),来简化变量的使用情况,然后再考虑提炼函数。
-
-
所有变量都处理完之后,编译。
-
在源函数中,将被提炼代码段替换为对目标函数的调用。
-
测试。
-
查看其他代码是否有与被提炼的代码段相同或相似之处。如果有,考虑使用以函数调用取代内联代码(222)令其调用提炼出的新函数。
内联函数(Inline Method | 115)
曾用名:内联函数(Inline Method )
反向重构:提炼函数(106)
function getRating(driver) {
return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}
function moreThanFiveLateDeliveries(driver) {
return driver.numberOfLateDeliveries > 5;
}
function getRating(driver) {
return (driver.numberOfLateDeliveries > 5) ? 2 : 1;
}
动机
- 函数内部代码和函数名称同样清晰易读(简单易懂);或者重构后使其内容和其名称变得同样清晰。应该去掉这个函数,直接使用其中的代码。间接性可能带来帮助,但非必要的间接性总是让人不舒服。
- 有一群组织不甚合理的函数,可以将它们都内联到一个大型函数中,重新提炼出小函数。
- 如果代码中有太多间接层,使得系统中的所有函数都似乎只是对另一个函数的简单委托。
做法
- 检查函数,确定它不具多态性(如果该函数属于一个类,并且有子类继承了这个函数,那么就无法内联)。
- 找出这个函数的所有调用点。
- 将这个函数的所有调用点都替换为函数本体。
- 每次替换之后,执行测试。
- 删除该函数的定义。
提炼变量(Extract Variable | 119)
曾用名:引入解释性变量(Introduce Explaining Variable)
反向重构:内联变量(123)
return order.quantity * order.itemPrice -
Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
Math.min(order.quantity * order.itemPrice * 0.1, 100);
const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;
动机
- 表达式有可能非常复杂而难以阅读(拆分表达式,为每一个表达式起一个有意义的名称,有助与理解逻辑)。
- 方便调试(可以看到各个表达式的值)。
考虑使用提炼变量,就意味着要给代码中的一个表达式命名。考虑这个名字所处的上下文,如果这个名字只在当前函数中有意义,那么提炼变量是个不错的选择。如果这个变量名在更宽的上下文中也有意义,就会考虑将其暴露出来,通常以函数的形式。
做法
- 确认要提炼的表达式没有副作用。
- 声明一个不可修改的变量,把想要提炼的表达式复制一份,以该表达式的结果值给这个变量赋值。
- 用这个新变量取代原来的表达式。
- 测试。
如果该表达式出现了多次,请用这个新变量逐一替换,每次替换之后都要执行测试。
内联变量(Inline Variable | 123)
曾用名:内联临时变量(Inline Temp)
反向重构:提炼变量(119)
let basePrice = anOrder.basePrice;
return (basePrice > 1000);
return (anOrder.basePrice > 1000);
动机
在一个函数内部,变量能给表达式提供有意义的名字,因此通常变量是好东西。但有时候,这个名字并不比表达式本身更具表现力。还有些时候,变量可能会妨碍重构附近的代码。若果真如此,就应该通过内联的手法消除变量。
做法
- 检查确认变量赋值语句的右侧表达式没有副作用。
- 如果变量没有被声明为不可修改,先将其变为不可修改,并执行测试。(确保该变量只被赋值一次)
- 找到第一处使用该变量的地方,将其替换为直接使用赋值语句的右侧表达式。
- 测试。
- 重复前面两步,逐一替换其他所有使用该变量的地方。
- 删除该变量的声明点和赋值语句。
- 测试。
改变函数声明(Change Function Declaration | 124)
别名:函数改名(Rename Function)
曾用名:函数改名(Rename Method)
曾用名:添加参数(Add Parameter)
曾用名:移除参数(Remove Parameter)
别名:修改签名(Change Signature)
function circum(radius){...}
function circumference(radius){...}
动机
函数是组成软件系统的关键关节,函数声明则展示的这些关节如何在一起工作。
一个好名字能一眼看出函数的用途,而不必查看其实现代码。(有一个改进函数名字的好办法:先写一句注释描述这个函数的用途,再把这句注释变成函数的名字。)
函数的参数列表阐述了函数如何与外部世界共处。函数的参数设置了一个上下文,只有在这个上下文中,才能使用这个函数。修改参数列表不仅能增加函数的应用范围,还能改变连接一个模块所需的条件,从而去除不必要的耦合。
做法
简单做法
- 如果想要移除一个参数,需要先确定函数体内没有使用该参数。
- 修改函数声明,使其成为你期望的状态。
- 找出所有使用旧的函数声明的地方,将它们改为使用新的函数声明。
- 测试。
最好能把大的修改拆成小的步骤,所以如果既想修改函数名,又想添加参数,最好分成两步来做。
迁移式做法
- 如果有必要的话,先对函数体内部加以重构,使后面的提炼步骤易于开展。
- 使用提炼函数(106)将函数体提炼成一个新函数。
- 如果提炼出的函数需要新增参数,用前面的简单做法添加即可。
- 测试。
- 对旧函数使用内联函数(115)。
- 如果新函数使用了临时的名字,再次使用改变函数声明(124)将其改回原来的名字。
- 测试。
如果要重构的函数属于一个具有多态性的类,那么对于该函数的每个实现版本,你都需要通过“提炼出一个新函数”的方式添加一层间接,并把旧函数的调用转发给新函数。