第6章 第一组重构

最常用到的重构就是用提炼函数(106)将代码提炼到函数中,或者用提炼变量(119)来提炼变量。既然重构的作用就是应对变化,你应该不会感到惊讶,我也经常使用这两个重构的反向重构------内联函数(115)和内联变量(123)。

提炼的关键就在于命名,随着理解的加深,我经常需要改名。改变函数声明(124)可以用于修改函数的名字,也可以用于添加或删减参数。变量也可以用变量改名(137)来改名,不过需要先做封装变量(132)。在给函数的形式参数改名时,不妨先用引入参数对象(140)把常在一起出没的参数组合成一个对象。

形成函数并给函数命名,这是低层级重构的精髓。有了函数以后,就需要把它们组合成更高层级的模块。我会使用函数组合成类(144),把函数和它们操作的数据一起组合成类。另一条路径是用函数组合成变换(149)将函数组合成变换式(transform),这对于处理只读数据尤为利。再往前一步,常常可以用拆分阶段(154)将这些模块组成界限分明的处理阶段。

6.1 提炼函数

何时应该把代码放进独立的函数?

"将意图与实现分开 ":如果你需要花时间浏览一段代码才能弄清它到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的事为其命名。以后再读到这段代码时,你一眼就能看到函数的用途,大多数时候根本不需要关心函数如何达成其用途(这是函数体内干的事)。

创造一个新函数,根据这个函数的意图来对它命名(以它 " 做什么 " 来命名,而不是以它 " 怎样做 " 命名)。

一旦接受了这个原则,我就逐渐养成一个习惯:写非常小的函数------通常只有几行的长度。在我看来,一个函数一旦超过 6 行,就开始散发臭味

有些人担心短函数会造成大量函数调用,因而影响性能。在我尚且年轻时,有时确实会有这个问题;但如今"由于函数调用影响性能"的情况已经非常罕见了。短函数常常能让编译器的优化功能运转更良好,因为短函数可以更容易地被缓存。所以,应该始终遵循性能优化的一般指导方针,不用过早担心性能问题。

6.1.1 范例:无局部变量

javascript 复制代码
function printOwing(invoice) {

 let outstanding = 0;
 console.log("***********************");
 console.log("**** Customer Owes ****");
 console.log("***********************");

 // calculate outstanding
 for (const o of invoice.orders) {
  outstanding += o.amount;
 }

 // record due date
 const today = Clock.today;
 invoice.dueDate = new Date(today.getFullYear(), today.getMonth

(), today.getDate() + 30);

 //print details
 console.log(`name: ${invoice.customer}`);
 console.log(`amount: ${outstanding}`);
 console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);

}

我们可以轻松提炼出"打印横幅"的代码。

javascript 复制代码
function printOwing(invoice) {

 let outstanding = 0;

 printBanner();

 // calculate outstanding
 for (const o of invoice.orders) {
  outstanding += o.amount;
 }

 // record due date
 const today = Clock.today;
 invoice.dueDate = new Date(today.getFullYear(), today.getMonth

(), today.getDate() + 30);

 //print details
 console.log(`name: ${invoice.customer}`);
 console.log(`amount: ${outstanding}`);
 console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}


function printBanner() {
 console.log("***********************");
 console.log("**** Customer Owes ****");
 console.log("***********************");
}

同样,我还可以把"打印详细信息"部分也提炼出来:

javascript 复制代码
function printOwing(invoice) {

 let outstanding = 0;

 printBanner();

 // calculate outstanding
 for (const o of invoice.orders) {
  outstanding += o.amount;
 }

 // record due date
 const today = Clock.today;
 invoice.dueDate = new Date(today.getFullYear(), today.getMonth

(), today.getDate() + 30);

 printDetails();


 function printDetails()
 {
  console.log(`name: ${invoice.customer}`);
  console.log(`amount: ${outstanding}`);
  console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}

6.1.2 范例:有局部变量

局部变量最简单的情况是:被提炼代码段只是读取这些变量的值,并不修改它们。这种情况下我可以简单地将它们当作参数传给目标函数。所以,如果我面对下列函数:

cpp 复制代码
function printOwing(invoice) {

 let outstanding = 0;

 printBanner();

 // calculate outstanding

 for (const o of invoice.orders) {

  outstanding += o.amount;

 }

 // record due date

 const today = Clock.today;

 invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);

 //print details

 console.log(`name: ${invoice.customer}`);

 console.log(`amount: ${outstanding}`);

 console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);

}

就可以将"打印详细信息"这一部分提炼为带两个参数的函数:

javascript 复制代码
function printOwing(invoice) {

 let outstanding = 0;

 printBanner();

 // calculate outstanding

 for (const o of invoice.orders) {

  outstanding += o.amount;

 }

 // record due date

 const today = Clock.today;

 invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);

 printDetails(invoice, outstanding);

}

function printDetails(invoice, outstanding) {

 console.log(`name: ${invoice.customer}`);

 console.log(`amount: ${outstanding}`);

 console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);

}

如果局部变量是一个数据结构(例如数组、记录或者对象),而被提炼代码段又修改了这个结构中的数据,也可以如法炮制。所以,"设置到期日"的逻辑也可以用同样的方式提炼出来:

javascript 复制代码
function printOwing(invoice) {

 let outstanding = 0;

 printBanner();

 // calculate outstanding
 for (const o of invoice.orders) {
  outstanding += o.amount;
 }

 recordDueDate(invoice);
 printDetails(invoice, outstanding);
}

function recordDueDate(invoice) {
 const today = Clock.today;
 invoice.dueDate = new Date(today.getFullYear(), today.getMonth

(), today.getDate() + 30);

}

6.1.3 范例:对局部变量再赋值

javascript 复制代码
function printOwing(invoice) {

 let outstanding = 0;

 printBanner();

 // calculate outstanding
 for (const o of invoice.orders) {
  outstanding += o.amount;
 }

 recordDueDate(invoice);

 printDetails(invoice, outstanding);
}

首先,把变量声明移动到使用处之前。

使用 移动语句 的手法

javascript 复制代码
function printOwing(invoice) {

 printBanner();

 // calculate outstanding
 let outstanding = 0;
 for (const o of invoice.orders) {
  outstanding += o.amount;
 }

 recordDueDate(invoice);

 printDetails(invoice, outstanding);

}

然后把想要提炼的代码复制到目标函数中。

javascript 复制代码
function printOwing(invoice) {

 printBanner();

 // calculate outstanding
 let outstanding = 0;
 for (const o of invoice.orders) {
  outstanding += o.amount;
 }

 recordDueDate(invoice);

 printDetails(invoice, outstanding);
}

function calculateOutstanding(invoice) {
 let outstanding = 0;

 for (const o of invoice.orders) {
    outstanding += o.amount;
 }

 return outstanding;
}

由于outstanding变量的声明已经被搬移到提炼出的新函数中,就不需要再将其作为参数传入了。outstanding是提炼代码段中唯一被重新赋值的变量,所以我可以直接返回它。

下一件事是修改原来的代码,令其调用新函数。新函数返回了修改后的outstanding变量值,我需要将其存入原来的变量中。

javascript 复制代码
function printOwing(invoice) {

 printBanner();

 let outstanding = calculateOutstanding(invoice);

 recordDueDate(invoice);

 printDetails(invoice, outstanding);

}



function calculateOutstanding(invoice) {

 let outstanding = 0;

 for (const o of invoice.orders) {

  outstanding += o.amount;

 }

 return outstanding;

}

在收工之前,我还要修改返回值的名字,使其符合我一贯的编码风格。

javascript 复制代码
function printOwing(invoice) {

 printBanner();

 const outstanding = calculateOutstanding(invoice);

 recordDueDate(invoice);

 printDetails(invoice, outstanding);

}

 function calculateOutstanding(invoice) {
 let result = 0;

 for (const o of invoice.orders) {
  result += o.amount;
 }

 return result;
}

6.2 内联函数

javascript 复制代码
function getRating(driver)
{
 return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}

function moreThanFiveLateDeliveries(driver)
{
 return driver.numberOfLateDeliveries > 5;
}

function getRating(driver)

{

return (driver.numberOfLateDeliveries > 5) ? 2 : 1;

}

函数内部代码和函数名称同样清晰易读。

本书经常以简短的函数表现动作意图,这样会使代码更清晰易读。但有时候你会遇到某些函数,**其内部代码和函数名称同样清晰易读。**也可能你重构了该函数的内部实现,使其内容和其名称变得同样清晰。若果真如此,你就应该去掉这个函数,直接使用其中的代码。间接性可能带来帮助,但非必要的间接性总是让人不舒服。

另一种需要使用内联函数的情况是:我手上有一群组织不甚合理的函数。可以将它们都内联到一个大型函数中,再以我喜欢的方式重新提炼出小函数。

如果代码中有太多间接层,使得系统中的所有函数都似乎只是对另一个函数的简单委托,造成我在这些委托动作之间晕头转向,那么我通常都会使用内联函数。当然,间接层有其价值,但不是所有间接层都有价值。通过内联手法,我可以找出那些有用的间接层,同时将无用的间接层去除。

6.3 提炼变量(Extract Variable)

反向重构:内联变量(123)

动机

表达式有可能非常复杂而难以阅读。这种情况下,局部变量可以帮助我们将表达式分解为比较容易管理的形式。在面对一块复杂逻辑时,局部变量使我能给其中的一部分命名,这样我就能更好地理解这部分逻辑是要干什么。这样的变量在调试时也很方便,它们给调试器和打印语句提供了便利的抓手。

做法

a. 确认要提炼的表达式没有副作用。

b. 声明一个不可修改的变量,把你想要提炼的表达式复制一份,以该表达式的结果值给这个变量赋值。

c. 用这个新变量取代原来的表达式。

d. 测试。

如果该表达式出现了多次,请用这个新变量逐一替换,每次替换之后都要执行测试。

6.5 改变函数声明

auto circum(double radius){ ... }

Auto circumference(double radius){ ... }

一个好名字能让我一眼看出函数的用途,而不必查

看其实现代码。

相关推荐
AI服务老曹2 天前
云、边、端分布式一体化计算架构,进行统一调度和统一监控的智慧物流开源了
人工智能·分布式·重构·架构·开源·音视频
第八学期3 天前
用Ansible Roles重构LNMP架构(Linux+Nginx+Mariadb+PHP)
linux·nginx·重构·架构·ansible·自动化运维
AI服务老曹7 天前
具备安全生产风险管控及评分等分析功能的名厨亮灶开源了
人工智能·安全·重构·开源·自动化·音视频
网络点点滴14 天前
重构项目架构
javascript·重构
Da_un16 天前
矩阵重构——reshape函数
矩阵·重构
Da_un17 天前
矩阵重新排列——sort函数
矩阵·重构
sp42a17 天前
老旧前端项目如何升级工程化的项目
前端·webpack·重构
喵叔哟19 天前
重构代码之将双向关联改为单向关联
数据库·重构
喵叔哟22 天前
重构代码之引入本地扩展
重构