为了进一步研究,我需要引入一些代码。在本例中,是我在世纪之交编写的原始示例的JavaScript版本。
function statement(customer, movies) {
let totalAmount = 0;
let frequentRenterPoints = 0;
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
let movie = movies[r.movieID];
let thisAmount = 0;
// 确定每部电影的数量
switch (movie.code) {
case "regular":
thisAmount = 2;
if (r.days > 2) {
thisAmount += (r.days - 2) * 1.5;
}
break;
case "new":
thisAmount = r.days * 3;
break;
case "childrens":
thisAmount = 1.5;
if (r.days > 3) {
thisAmount += (r.days - 3) * 1.5;
}
break;
}
//增加频繁租借点
frequentRenterPoints++;
// add bonus for a two day new release rental
if(movie.code === "new" && r.days > 2) frequentRenterPoints++;
//print figures for this rental
result += `\t${movie.title}\t${thisAmount}\n` ;
totalAmount += thisAmount;
}
// add footer lines
result += `Amount owed is ${totalAmount}\n`;
result += `You earned ${frequentRenterPoints} frequent renter points\n`;
return result;
}
我这里用的是ES6。代码对两个数据结构进行操作,这两个数据结构都是json记录的列表。客户记录是这样的:
{
"name": "martin",
"rentals": [
{"movieID": "F001", "days": 3},
{"movieID": "F002", "days": 1},
]
}
电影结构是这样的:
{
"F001": {"title": "Ran", "code": "regular"},
"F002": {"title": "Trois Couleurs: Bleu", "code": "regular"},
// etc
}
在最初的书中,电影只是作为java对象结构中的对象出现。对于本文,我更喜欢将json结构作为参数传入。我将假设使用某种全局查找,例如存储库,不适合这个应用程序。
statement方法打印租借语句的简单文本输出:
Rental Record for martin
Ran 3.5
Trois Couleurs: Bleu 2
Amount owed is 5.5
You earned 2 frequent renter points
即使按照示例代码的标准,这个输出也是粗糙的。我甚至都懒得把这些数字格式化一下吗?但是,请记住,这本书是在String之前使用Java 1.1编写的。格式被添加到语言中。这也许能部分原谅我的懒惰。
语句函数是smell Long方法的一个例子。它的大小足以让我怀疑。但是仅仅因为代码闻起来不好并不足以成为重构它的理由。分解不良的代码是一个问题,因为它很难理解。很难理解的代码很难修改,无论是添加新特性还是调试。因此,如果您不需要阅读和理解某些代码,那么它糟糕的结构不会对您造成伤害,您可以暂时不去管它。所以为了激发我们对这段代码的兴趣,我们需要一个改变它的理由。正如我在书中使用的,我们的原因是编写语句方法的HTML版本,它会打印出类似这样的东西。
<h1>Rental Record for <em>martin</em></h1>
<table>
<tr><td>Ran</td><td>3.5</td></tr>
<tr><td>Trois Couleurs: Bleu</td><td>2</td></tr>
</table>
<p>Amount owed is <em>5.5</em></p>
<p>You earned <em>2</em> frequent renter points</p>
正如我前面所指出的,在本文中,我将探索重构这段代码的多种方法,以便更容易地添加额外的输出呈现。所有这些都有相同的开始:将单个方法分解为一组函数来捕获逻辑的不同部分。一旦我完成了这个分解,我将探索四种不同的方法来安排这些函数来支持替代渲染。
1. 分解成几个函数
每当我处理这样一个过长的函数时,我的第一个想法是使用 Extract function 来查找逻辑代码块并将它们转换成自己的函数。首先吸引我的是switch语句。
function statement(customer, movies) {
let totalAmount = 0;
let frequentRenterPoints = 0;
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
let movie = movies[r.movieID];
let thisAmount = 0;
// determine amount for each movie
switch (movie.code) {
case "regular":
thisAmount = 2;
if (r.days > 2) {
thisAmount += (r.days - 2) * 1.5;
}
break;
case "new":
thisAmount = r.days * 3;
break;
case "childrens":
thisAmount = 1.5;
if (r.days > 3) {
thisAmount += (r.days - 3) * 1.5;
}
break;
}
//add frequent renter points
frequentRenterPoints++;
// add bonus for a two day new release rental
if(movie.code === "new" && r.days > 2) frequentRenterPoints++;
//print figures for this rental
result += `\t${movie.title}\t${thisAmount}\n` ;
totalAmount += thisAmount;
}
// add footer lines
result += `Amount owed is ${totalAmount}\n`;
result += `You earned ${frequentRenterPoints} frequent renter points\n`;
return result;
}
我的IDE (IntelliJ)为我提供了自动进行重构的功能,但它做得并不正确——它的JavaScript功能不如Java重构那么可靠或成熟。所以我用手工的方式来做,包括查看候选提取所使用的数据。这里有三位数据:
- 这个值是由提取的代码计算的值。我可以在函数中初始化它并在最后返回它
- r是循环中检查的租金,我可以把它作为参数传递进来。
- 电影是出租的电影,它是之前制作的临时影片。像这样的临时变量通常会在重构过程代码时产生阻碍,所以我更喜欢先使用Replace Temp with Query将它们转换成一个函数,以便在任何提取的代码中调用。
在用查询替换临时变量之后,代码如下所示。
function statement(customer, movies) {
let totalAmount = 0;
let frequentRenterPoints = 0;
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
let thisAmount = 0;
// determine amount for each movie
switch (movieFor(r).code) {
case "regular":
thisAmount = 2;
if (r.days > 2) {
thisAmount += (r.days - 2) * 1.5;
}
break;
case "new":
thisAmount = r.days * 3;
break;
case "childrens":
thisAmount = 1.5;
if (r.days > 3) {
thisAmount += (r.days - 3) * 1.5;
}
break;
}
//add frequent renter points
frequentRenterPoints++;
// add bonus for a two day new release rental
if(movieFor(r).code === "new" && r.days > 2) frequentRenterPoints++;
//print figures for this rental
result += `\t${movieFor(r).title}\t${thisAmount}\n` ;
totalAmount += thisAmount;
}
// add footer lines
result += `Amount owed is ${totalAmount}\n`;
result += `You earned ${frequentRenterPoints} frequent renter points\n`;
return result;
function movieFor(rental) {return movies[rental.movieID];}
}
现在我提取switch语句。
function statement(customer, movies) {
let totalAmount = 0;
let frequentRenterPoints = 0;
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
const thisAmount = amountFor(r);
//add frequent renter points
frequentRenterPoints++;
// add bonus for a two day new release rental
if(movieFor(r).code === "new" && r.days > 2) frequentRenterPoints++;
//print figures for this rental
result += `\t${movieFor(r).title}\t${thisAmount}\n` ;
totalAmount += thisAmount;
}
// add footer lines
result += `Amount owed is ${totalAmount}\n`;
result += `You earned ${frequentRenterPoints} frequent renter points\n`;
return result;
function movieFor(rental) {return movies[rental.movieID];}
function amountFor(r) {
let thisAmount = 0;
// determine amount for each movie
switch (movieFor(r).code) {
case "regular":
thisAmount = 2;
if (r.days > 2) {
thisAmount += (r.days - 2) * 1.5;
}
break;
case "new":
thisAmount = r.days * 3;
break;
case "childrens":
thisAmount = 1.5;
if (r.days > 3) {
thisAmount += (r.days - 3) * 1.5;
}
break;
}
return thisAmount;
}
}
现在我将注意力转移到计算频繁租户点上。我可以对它的代码进行类似的提取
function statement(customer, movies) {
let totalAmount = 0;
let frequentRenterPoints = 0;
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
const thisAmount = amountFor(r);
frequentRenterPointsFor(r);
//print figures for this rental
result += `\t${movieFor(r).title}\t${thisAmount}\n` ;
totalAmount += thisAmount;
}
// add footer lines
result += `Amount owed is ${totalAmount}\n`;
result += `You earned ${frequentRenterPoints} frequent renter points\n`;
return result;
…
function frequentRenterPointsFor(r) {
//add frequent renter points
frequentRenterPoints++;
// add bonus for a two day new release rental
if (movieFor(r).code === "new" && r.days > 2) frequentRenterPoints++;
}
尽管我提取了这个函数,但我不喜欢它通过更新父变量作用域来工作的方式。这些副作用使得代码很难理解,所以我对其进行了修改,使其在主体中没有副作用。
function statement(customer, movies) {
let totalAmount = 0;
let frequentRenterPoints = 0;
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
const thisAmount = amountFor(r);
frequentRenterPoints += frequentRenterPointsFor(r);
//print figures for this rental
result += `\t${movieFor(r).title}\t${thisAmount}\n` ;
totalAmount += thisAmount;
}
// add footer lines
result += `Amount owed is ${totalAmount}\n`;
result += `You earned ${frequentRenterPoints} frequent renter points\n`;
return result;
…
function frequentRenterPointsFor(r) {
let result = 1;
if (movieFor(r).code === "new" && r.days > 2) result++;
return result;
}
在我理解这两个提取的函数的同时,我想借此机会对它们进行一些清理。
function amountFor(rental) {
let result = 0;
switch (movieFor(rental).code) {
case "regular":
result = 2;
if (rental.days > 2) {
result += (rental.days - 2) * 1.5;
}
return result;
case "new":
result = rental.days * 3;
return result;
case "childrens":
result = 1.5;
if (rental.days > 3) {
result += (rental.days - 3) * 1.5;
}
return result;
}
return result;
}
function frequentRenterPointsFor(rental) {
return (movieFor(rental).code === "new" && rental.days > 2) ? 2 : 1;
}