重构的回顾(一)


重构,大概是程序员每天都会用到的技术。

 

什么是重构?

重构是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。

这就好比在不改变房屋功能的前提下进行装修,让房屋更好看,使用和维护更方便。

有人说重构是代数,像化简求值,因式分解,合并同类项,应用代数规则,通过简单的运算小步前进,最后得到答案。

 

重构在哪儿?

重构是敏捷方法XP的一个实践,XP的其他实践还包括:持续集成,测试优先,结对编程,等等。

敏捷开发,是从20世纪90年代开始活跃起来的,项目开发的轻量级解决方案。

重构的著作包括:

Refactoring Improving the Design of Existing Code》,Martin Fowler1999,书中大概有72个重构

Refactoring to Patterns》,Joshua Kerievsky2004,书中大约有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引入更清晰的算法。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值