设想有一个戏剧演出团,演员们经常要去各种场合表演戏剧。通常客户(customer)会指定几出剧目,而剧团则根据观众(audience)人数及剧目类型来向客户收费。该团目前出演两种戏剧:悲剧(tragedy)和喜剧(comedy)。给客户发出账单时,剧团还会根据到场观众的数量给出“观众量积分”(volumecredit)优惠,下次客户再请剧团表演时可以使用积分获得折扣——你可以把它看作一种提升客户忠诚度的方式。
该剧团将剧目的数据存储在一个简单的结构体中。
#include
#define comedy 1
#define tragedy 0
/******定义话剧结构体*******/
typedef struct{
char name[15]; /* 话剧名 */
int type; /* 话剧类型 */
}Drama;
/******定义表演结构体*******/
typedef struct{
char name[15]; /* 话剧名 */
int audience; /* 观众人数 */
}Performance;
/******定义账单结构体*******/
typedef struct{
char* Customer_Name; /* 顾客姓名 */
Performance perf[3]; /* 顾客观看话剧数据 */
}Bill;
/*************函数声明**************/
void Print_Bill(Bill Bigco, Drama plays[3])
{
/* 定义总金额 */
int totalamount = 0;
/* 定义金额 */
int thisamount = 0;
/* 定义积分 */
int VolumeCredits = 0;
int i = 0;
for(i = 0; i < 3; i++)
{
switch (plays[i].type)
{
case tragedy:
{
thisamount = 40000;
if (Bigco.perf[i].audience > 30)
{
thisamount += 1000 * (Bigco.perf[i].audience - 30);
}
}
break;
case comedy:
{
thisamount = 30000;
if (Bigco.perf[i].audience > 20)
{
thisamount += 10000 + 500 * (Bigco.perf[i].audience - 20);
}
thisamount += 300 * Bigco.perf[i].audience;
}
break;
default:
return;
}
VolumeCredits += ((Bigco.perf[i].audience - 30 > 0) ? Bigco.perf[i].audience - 30 : 0);
/*向下取整*/
if (comedy == plays[i].type)
{
VolumeCredits += Bigco.perf[i].audience / 5;
}
totalamount += thisamount;
/*打印每种话剧的明细*/
printf(" %s:$%d (%d seats)\r\n", plays[i].name, thisamount/100, Bigco.perf[i].audience);
}
/*打印积分和花费*/
printf("Amount owed is $%d\r\n", totalamount/100);
printf("You earned %d credits\r\n", VolumeCredits);
}
int main()
{
/*初始化三种话剧*/
Drama plays[3] = {{"Hamlet", 0}, {"As You Like It", 1}, {"Othello", 0}};
/*初始化三种表演*/
Performance performances[3] = {{"Hamlet", 55}, {"As You Like It", 35}, {"Othello", 40}};
/*初始化Bigco的账单结构体*/
Bill Bigco = {"Bigco", {performances[0], performances[1], performances[2]}};
printf("Print_Bill for %s\r\n", Bigco.Customer_Name);
/*调用函数*/
Print_Bill(Bigco, plays);
return 0;
}
打印结果如下图所示:
Print_Bill for Bigco
Hamlet:$650 (55 seats)
As You Like It:$580 (35 seats)
Othello:$500 (40 seats)
Amount owed is $1730
You earned 47 credits
如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易
于进行更改,那就先重构那个程序,使其比较容易添加该特性,然后再添加该
特性。
确保即将修改的代码拥有一组可靠的测试。
拥有几组确定的可用于测试代码是否准确的输入输出数据。
重构前,先检查自己是否有一套可靠的测试集。这些测试必须有自我检验能力。
每当看到这样长长的函数,我便下意识地想从整个函数中分离出不同的关注点。第一个引起我注意的就是中间那段switch语句。看着这块代码,我就知道它在计算一场戏剧演出的费用。这是我的直觉。不过正如Ward Cunningham所说,这种理解只是我脑海中转瞬即逝的灵光。我需要梳理这些灵感,将它们从脑海中搬回到代码里去,以免忘记。这样当我回头看时,代码就能告诉我它在干什么,我不需要重新思考一遍。
要将我的理解转化到代码里,得先将这块代码抽取成一个独立的函数,按它所干的事情给它命名,比如叫amountFor(performance)。每次想将一块代码抽取成一个函数时,我都会遵循一个标准流程,最大程度减少犯错的可能。我把这个流程记录了下来,并将它命名为提炼函数(106),以便日后可以方便地引用。
首先,我需要检查一下,如果我将这块代码提炼到自己的一个函数里,有哪些变量会离开原本的作用域。在此示例中,是type、audience和thisAmount这3个变量。前两个变量会被提炼后的函数使用,但不会被修改,那么我就可以将它们以参数方式传递进来。我更关心那些会被修改的变量。这里只有唯一一个——thisAmount,因此可以将它从函数中直接返回。我还可以将其初始化放到提炼后的函数里。修改后的代码如下所示。
int amountFor(Performance perf, Drama plays)
{
int thisAmount = 0;
switch (plays.type)
{
case tragedy:
{
thisAmount = 40000;
if (perf.audience > 30)
{
thisAmount += 1000 * (perf.audience - 30);
}
}
break;
case comedy:
{
thisAmount = 30000;
if (perf.audience > 20)
{
thisAmount += 10000 + 500 * (perf.audience - 20);
}
thisAmount += 300 * perf.audience;
}
break;
default:
printf("unknown type\n");
}
return thisAmount;
}
现在原Print_Bill函数可以直接调用这个新函数来初始化thisAmount。
#include
#define comedy 1
#define tragedy 0
/******定义话剧结构体*******/
typedef struct{
char name[15]; /* 话剧名 */
int type; /* 话剧类型 */
}Drama;
/******定义表演结构体*******/
typedef struct{
char name[15]; /* 话剧名 */
int audience; /* 观众人数 */
}Performance;
/******定义账单结构体*******/
typedef struct{
char* Customer_Name; /* 顾客姓名 */
Performance perf[3]; /* 顾客观看话剧数据 */
}Bill;
/*************函数声明**************/
int amountFor(Performance perf, Drama plays)
{
...
}
void Print_Bill(Bill Bigco, Drama plays[3])
{
/* 定义总金额 */
int totalamount = 0;
/* 定义金额 */
int thisamount = 0;
/* 定义积分 */
int VolumeCredits = 0;
int i = 0;
for(i = 0; i < 3; i++)
{
thisamount = amountFor(Bigco.perf[i], plays[i]);
VolumeCredits += ((Bigco.perf[i].audience - 30 > 0) ? Bigco.perf[i].audience - 30 : 0);
/*向下取整*/
if (comedy == plays[i].type)
{
VolumeCredits += Bigco.perf[i].audience / 5;
}
totalamount += thisamount;
/*打印每种话剧的明细*/
printf(" %s:$%d (%d seats)\r\n", plays[i].name, thisamount/100, Bigco.perf[i].audience);
}
/*打印积分和花费*/
printf("Amount owed is $%d\r\n", totalamount/100);
printf("You earned %d credits\r\n", VolumeCredits);
}
int main()
{
/*初始化三种话剧*/
Drama plays[3] = {{"Hamlet", 0}, {"As You Like It", 1}, {"Othello", 0}};
/*初始化三种表演*/
Performance performances[3] = {{"Hamlet", 55}, {"As You Like It", 35}, {"Othello", 40}};
/*初始化Bigco的账单结构体*/
Bill Bigco = {"Bigco", {performances[0], performances[1], performances[2]}};
printf("Print_Bill for %s\r\n", Bigco.Customer_Name);
/*调用函数*/
Print_Bill(Bigco, plays);
return 0;
}
做完这个改动后,我会马上编译并执行一遍测试,看看有无破坏了其他东西。无论每次重构多么简单,养成重构后即运行测试的习惯非常重要。犯错误是很容易的——至少我知道我是很容易犯错的。做完一次修改就运行测试,这样在我真的犯了错时,只需要考虑一个很小的改动范围,这使得查错与修复问题易如反掌。这就是重构过程的精髓所在:小步修改,每次修改后就运行测试。如果我改动了太多东西,犯错时就可能陷入麻烦的调试,并为此耗费大把时间。小步修改,以及它带来的频繁反馈,正是防止混乱的关键。
重构技术就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。
提炼函数(106)是一个常见的可自动完成的重构。如果我是用Java编程,我会本能地使用IDE的快捷键来完成这项重构。在我撰写本书时,JavaScript工具对此重构的支持仍不是很健壮,因此我必须手动重构。这不是很难,当然我还是需要小心处理那些局部作用域的变量。完成提炼函数(106)手法后,我会看看提炼出来的函数,看是否能进一步提升其表达能力。一般我做的第一件事就是给一些变量改名,使它们更简洁,比如将thisAmount重命名为result。
这是我个人的编码风格:永远将函数的返回值命名为“result”,这样我一眼就能知道它的作用。然后我再次编译、测试、提交代码。接着,我前往下一个目标——函数参数。
int amountFor(Performance perf, Drama plays)
{
int result = 0;
switch (plays.type)
{
case tragedy:
{
result = 40000;
if (perf.audience > 30)
{
result += 1000 * (perf.audience - 30);
}
}
break;
case comedy:
{
result = 30000;
if (perf.audience > 20)
{
result += 10000 + 500 * (perf.audience - 20);
}
result += 300 * perf.audience;
}
break;
default:
printf("unknown type\n");
}
return result;
}
这是我的另一个编码风格。使用一门动态类型语言(如JavaScript)时,跟踪变量的类型很有意义。因此,我为参数取名时都默认带上其类型名。一般我会使用不定冠词修饰它,除非命名中另有解释其角色的相关信息。这个习惯是从KentBeck那里学的[Beck SBPP],到现在我还一直觉得很有用。
傻瓜都能写出计算机可以理解的代码。唯有能写出人类容易理解的代码的,才是优秀的程序员。
好代码应能清楚地表明它在做什么,而变量命名是代码清晰的关键。只要改名能够提升代码的可读性,那就应
该毫不犹豫去做。有好的查找替换工具在手,改名通常并不困难;此外,你的测试以及语言本身的静态类型支持,都可以帮你揪出漏改的地方。如今有了自动化的重构工具,即便要给一个被大量调用的函数改名,通常也不在话下。本来下一个要改名的变量是play,但我对这个参数另有安排。
观察amountFor函数时,我会看看它的参数都从哪里来。Customer_drama是从循环变量中来,所以自然每次循环都会改变,但play变量是由performance变量计算得到的,因此根本没必要将它作为参数传入,我可以在amountFor函数中重新计算得到它。当我分解一个长函数时,我喜欢将play这样的变量移除掉,因为它们创建了很多具有局部作用域的临时变量,这会使提炼函数更加复杂。这里我要使用的重构手法是以查询取代临时变量(178)。我先从赋值表达式的右边部分提炼出一个函数来。
Drama playFor(Performance aPerformance)
{
int i = 0;
for (i = 0; i < 3; i++)
{
if (strcmp(aPerformance.name, plays[i].name) == 0)
{
return plays[i];
}
}
}
#include
#define comedy 1
#define tragedy 0
/******定义话剧结构体*******/
typedef struct{
char name[15]; /* 话剧名 */
int type; /* 话剧类型 */
}Drama;
/******定义表演结构体*******/
typedef struct{
char name[15]; /* 话剧名 */
int audience; /* 观众人数 */
}Performance;
/******定义账单结构体*******/
typedef struct{
char* Customer_Name; /* 顾客姓名 */
Performance perf[3]; /* 顾客观看话剧数据 */
}Bill;
/*初始化三种话剧*/
Drama plays[3] = {{"Hamlet", 0}, {"As You Like It", 1}, {"Othello", 0}};
/*************函数声明**************/
Drama playFor(Performance aPerformance)
{
int i = 0;
for (i = 0; i < 3; i++)
{
if (strcmp(aPerformance.name, plays[i].name) == 0)
{
return plays[i];
}
}
}
int amountFor(Performance perf, Drama plays)
{
int thisAmount = 0;
switch (plays.type)
{
case tragedy:
{
thisAmount = 40000;
if (perf.audience > 30)
{
thisAmount += 1000 * (perf.audience - 30);
}
}
break;
case comedy:
{
thisAmount = 30000;
if (perf.audience > 20)
{
thisAmount += 10000 + 500 * (perf.audience - 20);
}
thisAmount += 300 * perf.audience;
}
break;
default:
printf("unknown type\n");
}
return thisAmount;
}
void Print_Bill(Bill Bigco, Drama plays[3])
{
/* 定义总金额 */
int totalamount = 0;
/* 定义金额 */
int thisamount = 0;
/* 定义积分 */
int VolumeCredits = 0;
int i = 0;
for(i = 0; i < 3; i++)
{
const Drama play = playFor(Bigco.perf[i]);
thisamount = amountFor(Bigco.perf[i], play);
VolumeCredits += ((Bigco.perf[i].audience - 30 > 0) ? Bigco.perf[i].audience - 30 : 0);
/*向下取整*/
if (comedy == plays[i].type)
{
VolumeCredits += Bigco.perf[i].audience / 5;
}
totalamount += thisamount;
/*打印每种话剧的明细*/
printf(" %s:$%d (%d seats)\r\n", plays[i].name, thisamount/100, Bigco.perf[i].audience);
}
/*打印积分和花费*/
printf("Amount owed is $%d\r\n", totalamount/100);
printf("You earned %d credits\r\n", VolumeCredits);
}
int main()
{
/*初始化三种表演*/
Performance performances[3] = {{"Hamlet", 55}, {"As You Like It", 35}, {"Othello", 40}};
/*初始化Bigco的账单结构体*/
Bill Bigco = {"Bigco", {performances[0], performances[1], performances[2]}};
printf("Print_Bill for %s\r\n", Bigco.Customer_Name);
/*调用函数*/
Print_Bill(Bigco, plays);
return 0;
}
编译、测试、提交,然后使用内联变量(123)手法内联play变量。
thisamount = amountFor(Bigco.perf[i], playFor(Bigco.perf[i]));
编译、测试、提交。完成变量内联后,我可以对amountFor函数应用改变函数声明(124),移除play参数。我会分两步走。首先在amountFor函数内部使用新提炼的函数。编译、测试、提交,最后将参数删除。
int amountFor(Performance perf)
{
int thisAmount = 0;
switch (playFor(perf).type)
{
case tragedy:
{
thisAmount = 40000;
if (perf.audience > 30)
{
thisAmount += 1000 * (perf.audience - 30);
}
}
break;
case comedy:
{
thisAmount = 30000;
if (perf.audience > 20)
{
thisAmount += 10000 + 500 * (perf.audience - 20);
}
thisAmount += 300 * perf.audience;
}
break;
default:
printf("unknown type\n");
}
return thisAmount;
}
然后再一次编译、测试、提交。
这次重构可能在一些程序员心中敲响警钟:重构前,查找play变量的代码在每次循环中只执行了1次,而重构后却执行了3次。我会在后面探讨重构与性能之间的关系,但现在,我认为这次改动还不太可能对性能有严重影响,即便真的有所影响,后续再对一段结构良好的代码进行性能调优,也容易得多。
移除局部变量的好处就是做提炼时会简单得多,因为需要操心的局部作用域变少了。实际上,在做任何提炼前,一般都会先移除局部变量。处理完amountFor的参数后,我回过头来看一下它的调用点。它被赋值给一个临时变量,之后就不再被修改,因此我又采用内联变量(123)手法内联它。
#include
#define comedy 1
#define tragedy 0
/******定义话剧结构体*******/
typedef struct{
char name[15]; /* 话剧名 */
int type; /* 话剧类型 */
}Drama;
/******定义表演结构体*******/
typedef struct{
char name[15]; /* 话剧名 */
int audience; /* 观众人数 */
}Performance;
/******定义账单结构体*******/
typedef struct{
char* Customer_Name; /* 顾客姓名 */
Performance perf[3]; /* 顾客观看话剧数据 */
}Bill;
/*初始化三种话剧*/
Drama plays[3] = {{"Hamlet", 0}, {"As You Like It", 1}, {"Othello", 0}};
/*************函数声明**************/
Drama playFor(Performance aPerformance)
{
int i = 0;
for (i = 0; i < 3; i++)
{
if (strcmp(aPerformance.name, plays[i].name) == 0)
{
return plays[i];
}
}
}
int amountFor(Performance perf)
{
int thisAmount = 0;
switch (playFor(perf).type)
{
case tragedy:
{
thisAmount = 40000;
if (perf.audience > 30)
{
thisAmount += 1000 * (perf.audience - 30);
}
}
break;
case comedy:
{
thisAmount = 30000;
if (perf.audience > 20)
{
thisAmount += 10000 + 500 * (perf.audience - 20);
}
thisAmount += 300 * perf.audience;
}
break;
default:
printf("unknown type\n");
}
return thisAmount;
}
void Print_Bill(Bill Bigco)
{
/* 定义总金额 */
int totalamount = 0;
/* 定义金额 */
int thisamount = 0;
/* 定义积分 */
int VolumeCredits = 0;
int i = 0;
for(i = 0; i < 3; i++)
{
thisamount = amountFor(Bigco.perf[i]);
VolumeCredits += ((Bigco.perf[i].audience - 30 > 0) ? Bigco.perf[i].audience - 30 : 0);
/*向下取整*/
if (comedy == playFor(Bigco.perf[i]).type)
{
VolumeCredits += Bigco.perf[i].audience / 5;
}
totalamount += thisamount;
/*打印每种话剧的明细*/
printf(" %s:$%d (%d seats)\n", Bigco.perf[i].name, thisamount/100, Bigco.perf[i].audience);
}
/*打印积分和花费*/
printf("Amount owed is $:%d\n", totalamount/100);
printf("You earned %d credits\n", VolumeCredits);
}
int main()
{
/*初始化三种表演*/
Performance performances[3] = {{"Hamlet", 55}, {"As You Like It", 35}, {"Othello", 40}};
/*初始化Bigco的账单结构体*/
Bill Bigco = {"Bigco", {performances[0], performances[1], performances[2]}};
printf("Print_Bill for <%s>\n", Bigco.Customer_Name);
/*调用函数*/
Print_Bill(Bigco);
return 0;
}
这会儿我们就看到了移除play变量的好处,移除了一个局部作用域的变量,提炼观众量积分的计算逻辑又更简单一些。
我仍需要处理其他两个局部变量。perf同样可以轻易作为参数传入,但volumeCredits变量则有些棘手。它是一个累加变量,循环的每次迭代都会更新它的值。因此最简单的方式是,将整块逻辑提炼到新函数中,然后在新函数中直接返回volumeCredits。
int volumeCreditsFor(Performance perf)
{
int volumeCredits = 0;
volumeCredits += (perf.audience - 30 > 0) ? perf.audience - 30 : 0;
if (comedy == playFor(perf).type)
{
volumeCredits += perf.audience / 5;
}
return volumeCredits;
}
#include
#define comedy 1
#define tragedy 0
/******定义话剧结构体*******/
typedef struct{
char name[15]; /* 话剧名 */
int type; /* 话剧类型 */
}Drama;
/******定义表演结构体*******/
typedef struct{
char name[15]; /* 话剧名 */
int audience; /* 观众人数 */
}Performance;
/******定义账单结构体*******/
typedef struct{
char* Customer_Name; /* 顾客姓名 */
Performance perf[3]; /* 顾客观看话剧数据 */
}Bill;
/*初始化三种话剧*/
Drama plays[3] = {{"Hamlet", 0}, {"As You Like It", 1}, {"Othello", 0}};
/*************函数声明**************/
Drama playFor(Performance aPerformance)
{
int i = 0;
for (i = 0; i < 3; i++)
{
if (strcmp(aPerformance.name, plays[i].name) == 0)
{
return plays[i];
}
}
}
int amountFor(Performance perf)
{
int thisAmount = 0;
switch (playFor(perf).type)
{
case tragedy:
{
thisAmount = 40000;
if (perf.audience > 30)
{
thisAmount += 1000 * (perf.audience - 30);
}
}
break;
case comedy:
{
thisAmount = 30000;
if (perf.audience > 20)
{
thisAmount += 10000 + 500 * (perf.audience - 20);
}
thisAmount += 300 * perf.audience;
}
break;
default:
printf("unknown type\n");
}
return thisAmount;
}
int volumeCreditsFor(Performance perf)
{
int volumeCredits = 0;
volumeCredits += (perf.audience - 30 > 0) ? perf.audience - 30 : 0;
if (comedy == playFor(perf).type)
{
volumeCredits += perf.audience / 5;
}
return volumeCredits;
}
void Print_Bill(Bill Bigco)
{
/* 定义总金额 */
int totalamount = 0;
/* 定义积分 */
int VolumeCredits = 0;
int i = 0;
for(i = 0; i < 3; i++)
{
VolumeCredits += volumeCreditsFor(Bigco.perf[i]);
totalamount += amountFor(Bigco.perf[i]);
/*打印每种话剧的明细*/
printf(" %s:$%d (%d seats)\n", Bigco.perf[i].name, amountFor(Bigco.perf[i])/100, Bigco.perf[i].audience);
}
/*打印积分和花费*/
printf("Amount owed is $:%d\n", totalamount/100);
printf("You earned %d credits\n", VolumeCredits);
}
int main()
{
/*初始化三种表演*/
Performance performances[3] = {{"Hamlet", 55}, {"As You Like It", 35}, {"Othello", 40}};
/*初始化Bigco的账单结构体*/
Bill Bigco = {"Bigco", {performances[0], performances[1], performances[2]}};
printf("Print_Bill for <%s>\n", Bigco.Customer_Name);
/*调用函数*/
Print_Bill(Bigco);
return 0;
}
编译、测试、提交,然后对新函数里的变量改名。
int volumeCreditsFor(Performance aPerformance)
{
int volumeCredits = 0;
volumeCredits += (aPerformance.audience - 30 > 0) ? aPerformance.audience - 30 : 0;
if (comedy == playFor(aPerformance).type)
{
volumeCredits += aPerformance.audience / 5;
}
return volumeCredits;
}
这里我只展示了一步到位的改名结果,不过实际操作时,我还是一次只将一个变量改名,并在每次改名后执行编译、测试、提交。
原文中此函数是javaScript函数中对货币进行美元化的操作,在C语言中可以直接在打印时加参数实现,也可以调用setlocale函数以实现美元货币格式的格式化。
/*打印每种话剧的明细*/
printf(" %s: $%.2lf (%d seats)\n", Bigco.perf[i].name, (double)amountFor(Bigco.perf[i])/100, Bigco.perf[i].audience);
Print_Bill for <Bigco>
Hamlet: $650.00 (55 seats)
As You Like It: $580.00 (35 seats)
Othello: $500.00 (40 seats)
Amount owed is : $1730
You earned 47 credits
我的下一个重构目标是volumeCredits。处理这个变量更加微妙,因为它是在循环的迭代过程中累加得到的。第一步,就是应用拆分循环(227)将volumeCredits的累加过程分离出来。
void Print_Bill(Bill Bigco)
{
/* 定义总金额 */
int totalamount = 0;
int i = 0;
for(i = 0; i < 3; i++)
{
totalamount += amountFor(Bigco.perf[i]);
/*打印每种话剧的明细*/
printf(" %s:$%d (%d seats)\n", Bigco.perf[i].name, amountFor(Bigco.perf[i])/100, Bigco.perf[i].audience);
}
***************************************
/* 定义积分 */
int VolumeCredits = 0;
***************************************
for(i = 0; i < 3; i++)
{
VolumeCredits += volumeCreditsFor(Bigco.perf[i]);
}
/*打印积分和花费*/
printf("Amount owed is $:%d\n", totalamount/100);
printf("You earned %d credits\n", VolumeCredits);
}
完成这一步,我就可以使用移动语句(223)手法将变量声明挪动到紧邻循环的位置(在**范围内已经修改)。
把与更新volumeCredits变量相关的代码都集中到一起,有利于以查询取代临时变量(178)手法的施展。第一步同样是先对变量的计算过程应用提炼函数(106)手法。
int totalVolumeCredits(Performance perf[])
{
int volumeCredits = 0;
for (i = 0; i < sizeof(Bigco.perf) / sizeof(perf[0]); i++)
{
volumeCredits += volumeCreditsFor(perf[i]);
}
return volumeCredits;
}
void Print_Bill(Bill Bigco)
{
/* 定义总金额 */
int totalamount = 0;
int i = 0;
for(i = 0; i < 3; i++)
{
totalamount += amountFor(Bigco.perf[i]);
/*打印每种话剧的明细*/
printf(" %s:$%d (%d seats)\n", Bigco.perf[i].name, amountFor(Bigco.perf[i])/100, Bigco.perf[i].audience);
}
int volumeCredits = totalVolumeCredits(Bigco.perf);
/*打印积分和花费*/
printf("Amount owed is $:%d\n", totalamount/100);
printf("You earned %d credits\n", VolumeCredits);
}
完成函数提炼后,我再应用内联变量(123)手法内联totalVolumeCredits函数。
void Print_Bill(Bill Bigco)
{
/* 定义总金额 */
int totalamount = 0;
int i = 0;
for(i = 0; i < 3; i++)
{
totalamount += amountFor(Bigco.perf[i]);
/*打印每种话剧的明细*/
printf(" %s:$%d (%d seats)\n", Bigco.perf[i].name, amountFor(Bigco.perf[i])/100, Bigco.perf[i].audience);
}
/*打印积分和花费*/
printf("Amount owed is $:%d\n", totalamount/100);
printf("You earned %d credits\n", totalVolumeCredits(Bigco));
}
重构至此,让我先暂停一下,谈谈刚刚完成的修改。首先,我知道有些读者会再次对此修改可能带来的性能问题感到担忧,我知道很多人本能地警惕重复的循环。但大多数时候,重复一次这样的循环对性能的影响都可忽略不计。如果你在重构前后进行计时,很可能甚至都注意不到运行速度的变化——通常也确实没什么变化。许多程序员对代码实际的运行路径都所知不足,甚至经验丰富的程序员有时也未能避免。在聪明的编译器、现代的缓存技术面前,我们很多直觉都是不准确的。软件的性能通常只与代码的一小部分相关,改变其他的部分往往对总体性能贡献甚微。
#include
#define comedy 1
#define tragedy 0
/******定义话剧结构体*******/
typedef struct{
char name[15]; /* 话剧名 */
int type; /* 话剧类型 */
}Drama;
/******定义表演结构体*******/
typedef struct{
char name[15]; /* 话剧名 */
int audience; /* 观众人数 */
}Performance;
/******定义账单结构体*******/
typedef struct{
char* Customer_Name; /* 顾客姓名 */
Performance perf[3]; /* 顾客观看话剧数据 */
}Bill;
/***********前向声明***********/
Bill Bigco;
int i = 0;
/*初始化三种话剧*/
Drama plays[3] = {{"Hamlet", 0}, {"As You Like It", 1}, {"Othello", 0}};
/*************函数声明**************/
Drama playFor(Performance aPerformance)
{
int i = 0;
for (i = 0; i < 3; i++)
{
if (strcmp(aPerformance.name, plays[i].name) == 0)
{
return plays[i];
}
}
}
int amountFor(Performance perf)
{
int thisAmount = 0;
switch (playFor(perf).type)
{
case tragedy:
{
thisAmount = 40000;
if (perf.audience > 30)
{
thisAmount += 1000 * (perf.audience - 30);
}
}
break;
case comedy:
{
thisAmount = 30000;
if (perf.audience > 20)
{
thisAmount += 10000 + 500 * (perf.audience - 20);
}
thisAmount += 300 * perf.audience;
}
break;
default:
printf("unknown type\n");
}
return thisAmount;
}
int volumeCreditsFor(Performance perf)
{
int volumeCredits = 0;
volumeCredits += (perf.audience - 30 > 0) ? perf.audience - 30 : 0;
if (comedy == playFor(perf).type)
{
volumeCredits += perf.audience / 5;
}
return volumeCredits;
}
int totalVolumeCredits(Performance perf[])
{
int volumeCredits = 0;
for (i = 0; i < sizeof(Bigco.perf) / sizeof(perf[0]); i++)
{
volumeCredits += volumeCreditsFor(perf[i]);
}
return volumeCredits;
}
void Print_Bill(Bill Bigco)
{
/* 定义总金额 */
int totalamount = 0;
for(i = 0; i < 3; i++)
{
totalamount += amountFor(Bigco.perf[i]);
/*打印每种话剧的明细*/
printf(" %s:$%d (%d seats)\n", Bigco.perf[i].name, amountFor(Bigco.perf[i])/100, Bigco.perf[i].audience);
}
/*打印积分和花费*/
printf("Amount owed is $:%d\n", totalamount/100);
printf("You earned %d credits\n", totalVolumeCredits(Bigco.perf));
}
int main()
{
/*初始化三种表演*/
Performance performances[3] = {{"Hamlet", 55}, {"As You Like It", 35}, {"Othello", 40}};
/*初始化Bigco的账单结构体*/
Bill Bigco = {"Bigco", {performances[0], performances[1], performances[2]}};
printf("Print_Bill for <%s>\n", Bigco.Customer_Name);
/*调用函数*/
Print_Bill(Bigco);
return 0;
}
当然,“大多数时候”不等同于“所有时候”。有时,一些重构手法也会显著地影响性能。但即便如此,我通常也不去管它,继续重构,因为有了一份结构良好的代码,回头调优其性能也容易得多。如果我在重构时引入了明显的性能损耗,我后面会花时间进行性能调优。进行调优时,可能会回退我早先做的一些重构——但更多时候,因为重构我可以使用更高效的调优方案。最后我得到的是既整洁又高效的代码。
因此对于重构过程的性能问题,我总体的建议是:大多数情况下可以忽略它。如果重构引入了性能损耗,先完成重构,再做性能优化。其次,我希望你能注意到:我们移除volumeCredits的过程是多么小步。整个过程一共有4步,每一步都伴随着一次编译、测试以及向本地代码库的提交:
使用拆分循环(227)分离出累加过程;
使用移动语句(223)将累加变量的声明与累加过程集中到一起;
使用提炼函数(106)提炼出计算总数的函数;
使用内联变量(123)完全移除中间变量。
我得坦白,我并非总是如此小步——但在事情变复杂时,我的第一反应就是采用更小的步子。怎样算变复杂呢,就是当重构过程有测试失败而我又无法马上看清问题所在并立即修复时,我就会回滚到最后一次可工作的提交,然后以更小的步子重做。这得益于我如此频繁地提交。特别是与复杂代码打交道时,细小的步子是快速前进的关键。
接着我要重复同样的步骤来移除totalAmount。我以拆解循环开始(编译、测试、提交),然后下移累加变量的声明语句(编译、测试、提交),最后再提炼函数。这里令我有点头疼的是:最好的函数名应该是totalAmount,但它已经被变量名占用,我无法起两个同样的名字。因此,我在提炼函数时先给它随便取了一
个名字(然后编译、测试、提交)。
double appleSauce(Performance perf[])
{
double totalAmount = 0;
for (int i=0; i<sizeof(Bigco.perf) / sizeof(perf[0]); i++)
{
totalAmount += amountFor(perf[i]);
}
return totalAmount;
}
void Print_Bill(Bill Bigco)
{
for(i = 0; i < 3; i++)
{
/*打印每种话剧的明细*/
printf(" %s:$%d (%d seats)\n", Bigco.perf[i].name, amountFor(Bigco.perf[i])/100, Bigco.perf[i].audience);
}
/* 定义总金额 */
double totalamount = appleSauce(Bigco.perf);
/*打印积分和花费*/
printf("Amount owed is $:%.2lf\n", totalamount/100);
printf("You earned %d credits\n", totalVolumeCredits(Bigco.perf));
}
接着我将变量内联(编译、测试、提交),然后将函数名改回totalAmount(编译、测试、提交)。
double totalAmount(Performance perf[])
{
double totalAmount = 0;
for (i=0; i<sizeof(Bigco.perf) / sizeof(perf[0]); i++)
{
totalAmount += amountFor(perf[i]);
}
return totalAmount;
}
void Print_Bill(Bill Bigco)
{
for(i = 0; i < 3; i++)
{
/*打印每种话剧的明细*/
printf(" %s:$%d (%d seats)\n", Bigco.perf[i].name, amountFor(Bigco.perf[i])/100, Bigco.perf[i].audience);
}
/*打印积分和花费*/
printf("Amount owed is $:%.2lf\n", totalAmount(Bigco.perf)/100);
printf("You earned %d credits\n", totalVolumeCredits(Bigco.perf));
}
重构至此,是时候停下来欣赏一下代码的全貌了。
#include
#define comedy 1
#define tragedy 0
/******定义话剧结构体*******/
typedef struct{
char name[15]; /* 话剧名 */
int type; /* 话剧类型 */
}Drama;
/******定义表演结构体*******/
typedef struct{
char name[15]; /* 话剧名 */
int audience; /* 观众人数 */
}Performance;
/******定义账单结构体*******/
typedef struct{
char* Customer_Name; /* 顾客姓名 */
Performance perf[3]; /* 顾客观看话剧数据 */
}Bill;
/*************前向声明**************/
Bill Bigco;
int i = 0;
/*初始化三种话剧*/
Drama plays[3] = {{"Hamlet", 0}, {"As You Like It", 1}, {"Othello", 0}};
/*************函数声明**************/
/*根据表演结构体获取话剧结构体*/
Drama playFor(Performance aPerformance)
{
int i = 0;
for (i = 0; i < 3; i++)
{
if (strcmp(aPerformance.name, plays[i].name) == 0)
{
return plays[i];
}
}
}
/*每一场表演的金额计算函数*/
int amountFor(Performance perf)
{
int thisAmount = 0;
switch (playFor(perf).type)
{
case tragedy:
{
thisAmount = 40000;
if (perf.audience > 30)
{
thisAmount += 1000 * (perf.audience - 30);
}
}
break;
case comedy:
{
thisAmount = 30000;
if (perf.audience > 20)
{
thisAmount += 10000 + 500 * (perf.audience - 20);
}
thisAmount += 300 * perf.audience;
}
break;
default:
printf("unknown type\n");
}
return thisAmount;
}
/*每一场表演的积分计算函数*/
int volumeCreditsFor(Performance perf)
{
int volumeCredits = 0;
volumeCredits += (perf.audience - 30 > 0) ? perf.audience - 30 : 0;
if (comedy == playFor(perf).type)
{
volumeCredits += perf.audience / 5;
}
return volumeCredits;
}
/*顾客获取的总积分计算函数*/
int totalVolumeCredits(Performance perf[])
{
int volumeCredits = 0;
for (i = 0; i < sizeof(Bigco.perf) / sizeof(perf[0]); i++)
{
volumeCredits += volumeCreditsFor(perf[i]);
}
return volumeCredits;
}
/*顾客花费总金额计算函数*/
double totalAmount(Performance perf[])
{
double totalAmount = 0;
for (i=0; i<sizeof(Bigco.perf) / sizeof(perf[0]); i++)
{
totalAmount += amountFor(perf[i]);
}
return totalAmount;
}
/*账单打印函数*/
void Print_Bill(Bill Bigco)
{
for(i = 0; i < 3; i++)
{
/*打印每种话剧的明细*/
printf(" %s:$%d (%d seats)\n", Bigco.perf[i].name, amountFor(Bigco.perf[i])/100, Bigco.perf[i].audience);
}
/*打印积分和花费*/
printf("Amount owed is $:%.2lf\n", totalAmount(Bigco.perf)/100);
printf("You earned %d credits\n", totalVolumeCredits(Bigco.perf));
}
int main()
{
/*初始化三种表演*/
Performance performances[3] = {{"Hamlet", 55}, {"As You Like It", 35}, {"Othello", 40}};
/*初始化Bigco的账单结构体*/
Bill Bigco = {"Bigco", {performances[0], performances[1], performances[2]}};
printf("Print_Bill for <%s>\n", Bigco.Customer_Name);
/*调用函数*/
Print_Bill(Bigco);
return 0;
}
现在代码结构已经好多了。顶层的statement函数现在只剩7行代码,而且它处理的都是与打印详单相关的逻辑。与计算相关的逻辑从主函数中被移走,改由一组函数来支持。每个单独的计算过程和详单的整体结构,都因此变得更易理解了。