第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){ ... }

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

看其实现代码。

相关推荐
二川bro3 小时前
飞算智造JavaAI:智能编程革命——AI重构Java开发新范式
java·人工智能·重构
创小匠9 小时前
创客匠人视角下创始人 IP 打造与知识变现的底层逻辑重构
人工智能·tcp/ip·重构
文火冰糖的硅基工坊9 小时前
[创业之路-458]:企业经营层 - 蓝海战略 - 重构价值曲线、整合产业要素、创造新需求
科技·重构·架构·创业·业务
哲科软件10 小时前
从“电话催维修“到“手机看进度“——售后服务系统开发如何重构客户体验
大数据·智能手机·重构
倔强的石头1061 天前
飞算JavaAI:重构软件开发范式的智能引擎
java·数据库·重构
zzywxc7871 天前
AI大模型的技术演进、流程重构、行业影响三个维度的系统性分析
人工智能·重构
点控云1 天前
智能私域运营中枢:从客户视角看 SCRM 的体验革新与价值重构
大数据·人工智能·科技·重构·外呼系统·呼叫中心
zhaoyi_he1 天前
多模态大模型的技术应用与未来展望:重构AI交互范式的新引擎
人工智能·重构
zkmall1 天前
企业电商解决方案哪家好?ZKmall模块商城全渠道支持 + 定制化服务更省心
大数据·运维·重构·架构·开源
杨云强2 天前
万能公式基分析重构补丁复分析和欧拉公式原理推导
重构