重构,大概是程序员每天都会用到的技术。
什么是重构?
重构是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。
这就好比在不改变房屋功能的前提下进行装修,让房屋更好看,使用和维护更方便。
有人说重构是代数,像化简求值,因式分解,合并同类项,应用代数规则,通过简单的运算小步前进,最后得到答案。
重构在哪儿?
重构是敏捷方法XP的一个实践,XP的其他实践还包括:持续集成,测试优先,结对编程,等等。
敏捷开发,是从20世纪90年代开始活跃起来的,项目开发的轻量级解决方案。
重构的著作包括:
《Refactoring Improving the Design of Existing Code》,Martin Fowler,1999,书中大概有72个重构
《Refactoring to Patterns》,Joshua Kerievsky,2004,书中大约有27种重构
重构的动机?
消除重复代码,改进软件设计,使代码更好理解,更利于被重用,更容易找到Bug,从而更好维护,更容易进行性能改善,编码更快更有乐趣。
何时重构?
在同一个问题出现三次时,重构;添加功能时,重构;修补错误时,重构;复审代码时,重构。
需要注意什么?
要注意“两顶帽子”,重构时绝不添加新功能,否则你可能无法回到前一个正确的地点。
要搭建自动测试体系。
怎么重构?
我觉得最有用的重构,莫过于Extract Method(提炼函数)。
它的概念也很简单,就是把一段代码提取到一个函数,并让函数名解释这段代码的用途。例如:
void printOwing(double amount) {
printBanner();
// print details
printf(“name: %s/n”, _name);
printf(“amount %g/n“, amount);
}
重构以后就变成了:
void printOwing(double amount) {
printBanner();
pintDetails(amount);
}
void printDetails(double amount) {
printf(“name: %s/n”, _name);
printf(“amount %g/n“, amount);
}
需要注意一点,经过提炼以后的代码应该在细节的同一层面上。
private void paintCard(Graphics g) {
Image image = null;
if (card.getType().equals("Problem")) {
image = explanations.getGameUI().problem;
} else if (card.getType().equals("Solution")) {
image = explanations.getGameUI().solution;
} else if (card.getType().equals("Value")) {
image = explanations.getGameUI().value;
}
g.drawImage(image,0,0,explanations.getGameUI());
if (shouldHighlight()) {
paintCardHighlight(g);
}
paintCardText(g);
}
显然,这段代码的后半段比前半段更加抽象,我们把它改一下:
private void paintCard(Graphics g) {
paintCardImage(g);
if (shouldHighlight()) {
paintCardHighlight(g);
}
paintCardText(g);
}
遵循这个原则,使得上面这段代码在问题领域的层面上就能被理解,而不是在细节层面上去理解它。从某种意义上说,Extract Method降低了代码的复杂度。
如果把Extract Method用在条件表达式里面,就有了另一种方法,Decompose Conditional(分解条件表达式)。比如下面这段代码:
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
charge = quantify * _winterRate * _winterServiceCharge;
}
else {
charge = quantity * _summerRate;
}
重构一下:
if (notSummer(date)) {
charge = quantify * _winterRate * _winterServiceCharge;
}
else {
charge = quantity * _summerRate;
}
是不是更好理解了。
其实他也更方便我们查出错误,只需要看notSummer的内容是不是和它的函数名表达一致。
除了去除重复代码,Extract Method也擅长于分解过长函数。
对于过长函数,用Extract Method提取出功能单一的小函数,他们更容易被重用。这就像手上有了一大把工具,你的工作是不是更容易了。
实现Extract Method的主要困难在于临时变量(局部变量),就像下面这段代码
void printOwing() {
Enumeration e = _orders.elements();
double outstanding = 0.0;
// print banner
printf(“*****************************/n”);
printf(“***** Customer Owes *******/n”);
printf(“*****************************/n”);
// calculate outstanding
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
// print details
printf(“name: %s/n”, _name);
printf(“amount %g/n“, amount);
}
第一段代码print banner,没有局部变量,于是直接用Extract Method:
void printOwing() {
Enumeration e = _orders.elements();
double outstanding = 0.0;
printBanner();
// calculate outstanding
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
// print details
printf(“name: %s/n”, _name);
printf(“amount %g/n“, amount);
}
void printBanner() {
printf(“*****************************/n”);
printf(“***** Customer Owes *******/n”);
printf(“*****************************/n”);
}
第三段代码print details有局部变量,但它只读取不修改局部变量,那么将局部变量作为参数传递给提炼出来的函数:
void printOwing() {
Enumeration e = _orders.elements();
double outstanding = 0.0;
printBanner();
// calculate outstanding
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
printDetails(outstanding);
}
void printDetails(double outstanding) {
printf(“name: %s/n”, _name);
printf(“amount %g/n“, outstanding);
}
第二段代码calculate outstanding,问题变得更复杂,有局部变量且会修改这个变量,我们让被提炼函数返回局部变量改变后的值:
void printOwing() {
printBanner();
outstanding = getOutstanding();
printDetails(outstanding);
}
double getOutstanding() {
Enumeration e = _orders.elements();
double result = 0.0;
while (e.hasMoreElements()) {
Order each = (Order) e.nextElement();
result += each.getAmount();
}
return result;
}
临时变量往往为数众多,它们零零散散,错综复杂,让你的提炼工作举步维艰。
例如有一些时候,你的代码在很多地方使用了同一个局部变量:
double temp = 2 * (_height + _width);
cout << temp << endl;
temp = _height * _width;
cout << temp << endl;
这是一种不好的编程习惯。
前两行代码中的temp与后两行代码的temp没有任何关系,在两个位置使用同一变量,会使两者看上去彼此相关。
正确的做法是为变量指定单一的用途,使用Split Temporary Variable(分解临时变量):
double perimeter = 2 * (_height + _width);
cout << perimeter << endl;
double area = _height * _width;
cout << area << endl;
运用Split Temporary Variable以后,这两部分代码变得更容易提炼。
在使用了Split Temporary Variable之后,我们还可以进一步使用Replace Temp with Query(一查询取代临时变量)去掉临时变量:
cout << perimeter() << endl;
cout << area() << endl;
Replace Temp with Query将表达式提炼到独立函数中,并将这个临时变量的所有引用点替换为对新函数的调用。这些小函数很容易被其他用户重用。
如果最后你还是无法提炼函数,那么使用奥义Replace Method with Method Object(以函数对象取代函数)。将这个函数放进一个单独的类,如此一来,局部变量就成了类的成员变量,然后你就可以用Extract Method 将大型函数分解为多个小函数了。
举个例子:
Class Account
int gamma (int inputVal, int quantity, int yearToDate) {
int importantValue1 = (inputVal * quantity) + delta();
int importantValue2 = (inputVal * yearToDate) + 100;
if ((yearToDate – importantValue1) > 100) {
importantValue2 -= 20;
}
int importantValue3 = importantValue2 * 7;
return importantValue3 – 2 * importantValue1;
}
我们来创建一个类,把所有局部变量变成类的成员变量:
class Gamma {
private:
Account _account;
int inputVal;
int quantity;
int yearToDate;
int importantValue1;
int importantValue2;
int importantValue3;
public:
Gamma (Account source, int inputValArg, int quantityArg, int yearToDateArg) {
_account = source;
inputVal = inputValArg;
quantity = quantityArg;
yearToDate = yearToDateArg;
}
int compute() {
importantValue1 = (inputVal * quantity) + _account.delta();
importantValue2 = (inputVal * yearToDate) + 100;
if ((yearToDate – importantValue1) > 100) {
importantValue2 -= 20;
}
importantValue3 = importantValue2 * 7;
return importantValue3 – 2 * importantValue1;
}
};
然后,修改旧函数,将它的工作委托给这个新创建的函数对象:
int gamma (int inputVal, int quantity, int yearToDate) {
return new Gamma(this, inputVal, quantity, yearToDate).compute()
}
也可以把Gamma构造函数的部分参数放到compute()中去:
class Gamma {
private:
Account _account;
int importantValue1;
int importantValue2;
int importantValue3;
public:
Gamma (Account source) {
_account = source;
}
int compute(int inputVal, int quantity, int yearToDate) {
importantValue1 = (inputVal * quantity) + _account.delta();
importantValue2 = (inputVal * yearToDate) + 100;
if ((yearToDate – importantValue1) > 100) {
importantValue2 -= 20;
}
importantValue3 = importantValue2 * 7;
return importantValue3 – 2 * importantValue1;
}
};
int gamma (int inputVal, int quantity, int yearToDate) {
return new Gamma(this).compute( inputVal, quantity, yearToDate)
}
当我们把过长函数都分解为功能单一的小函数以后,代码更容易让人理解了,我们可能发现算法可以改进,我们就更容易的找到性能热点,然后用Substitute Algorithm引入更清晰的算法。