在《The Pragmatic Programmer: From Journeyman to Master》(中文译名为《程序员修炼之道--从小工到专家》)Tip25 “怎样配平资源“中有一个重构代码的经典案例,现转录如下:
提示35
Finish What You Start
要有始有终
在大多数情况下这条提示都很容易应用。它只是意味着,分配某项资源的例程或对象应该负责解除该资源的分配。让我们通过一个糟糕的代码例子来看一看该提示的应用方式——这是一个打开文件、从中读取消费者信息、更新某个字段、然后写回结果的应用。我们去除了其中的错误处理代码,以让例子更清晰:
void readCustormer(const char *fName, Customer *cRec) {
cFile = fopen(fName, "r+");
fread(cRec, sizeof(*cRec), 1, cFile);
}
void writeCustomer(Customer *cRect) {
rewind(cFile);
fwrite (cRec, sizeof(*cRec), 1, cFile);
fclose(cFile);
}
void updateCustomer(const char *fName, double newBalance) {
Customer cRec;
readCustomer (fName, &CRec);
cRec.balance = newBalance;
writeCustomer(&cRec);
}
初看上去,例程updateCustomer相当好。它似乎实现了我们所需的逻辑——读取记录,更新余额,写回记录。但是,这样的整洁掩盖了一个重大的问题。例程readCustomer和writeCustomer紧密地耦合在一起——它们共享全局变量cFile。readCustomer打开文件,并把文件指针存储在cFile中,而writeCustomer使用所存储的指针在其结束时关闭文件。这个全局变量甚至没有出现在updateCustomer例程中。
这为什么不好?让我们考虑一下,不走运的维护程序员被告知规范发生了变化——余额只应在新的值不为负时更新。她进入源码,改动updateCustomer:
void updateCustomer(const char *fName, double newBalance) {
Customer cRec;
readCustomer (fName, &CRec);
if (newBalance >= 0.0) {
cRec.balance = newBalance;
writeCustomer(&cRec);
}
}
在测试时一切似乎都很好。但是,当代码投入实际工作,若干小时后它就崩溃了,抱怨说打开的文件太多。因为writeCustomer在有些情况下不会被调用,文件也就不会被关闭。
这个问题的一个非常糟糕的解决方案是在updateCustomer中对该特殊情况进行处理:
void updateCustomer(const char *fName, double newBalance) {
Customer cRec;
readCustomer (fName, &CRec);
if (newBalance >= 0.0) {
cRec.balance = newBalance;
writeCustomer(&cRec);
}
else
fclose(cFile);
}
这可以修正问题——不管新的余额是多少,文件现在都会被关闭——但这样的修正意味着三个例程通过全局的cFile耦合在一起。我们在掉进陷阱,如果我们继续沿着这一方向前进,事情就会开始迅速变糟。
要有始有终这一提示告诉我们,分配资源的例程也应该释放它。通过稍稍重构代码,我们可以在此应用该提示:
void readCustomer(FILE *cFile, Customer *cRec) {
fread(cRec, sizeof(*cRec), 1, cFile);
}
void writeCustomer(FILE *cFile, Customer *cRec) {
rewind(cFile);
fwrite(cRec, sizeof(*cRec), 1, cFile);
}
void updateCustomer(const char *fName, double newBalance) {
FILE *cFile;
Customer cRec;
cFile = fopen(fName, "r+");
readCustomer(cFile, &cRec);
if (newBalance >= 0.0) {
cRec.balance = newBalance;
writeCustomer(cFile, &cRec);
}
fclose(cFile);
}
现在updateCustomer例程承担了关于该文件的所有责任。它打开文件并(有始有终地)在退出前关闭它。例程配平了对文件的使用:打开和关闭在同一个地方,而且显然每一次打开都有对应的关闭。重构还移除了丑陋的全局变量。
由上文可见,重构时遵循一些原则是很重要的。