重构--重新组织函数

重构手法中,很大一部分是对函数进行整理,使之更恰当地包装代码。几乎所有问题都源于过长的函数,这很讨厌,因为它们往往包含太多的信息,这些信息又被函数错综复杂的逻辑遮盖,不易鉴别。对付过长函数,一项重要的重构手法就是把一段代码从原先函数中提取出来,放进一个单独函数中。
提取函数最大的困难就是处理局部变量,而临时变量则是其中一个主要的困难源头。参数带来的问题比临时变量稍微少一些,前提是你不在函数内赋值给他们。函数分解完毕后,我们就可以知道如何让他工作的更好,也许我们还会发现算法可以改进,从而使代码更清晰。

提炼函数

比如,有一段代码:

void printOwing(double amount) {
    printBanner();

    // print Details
    System.out.println("name:" + name);
    System.out.println("amount:" + amount);
}

将这段代码放进一个独立函数中,并让函数名称解释这个函数的用途:

void printOwing(double amount) {
    printBanner();
    printDetails(amount);
}

void printDetails(double amount) {
    System.out.println("name:" + name);
    System.out.println("amount:" + amount);
}

提炼函数是最常用的重构手法之一,当我看见一个过长的函数或者一段需要注释才能让人理解用途的代码,我就会将这段代码放进一个独立函数中。
这么做有几个好处:

  1. 如果每个函数的粒度都很小,那么函数被复用的机会就更大
  2. 这会使高层函数读起来就像一系列注释
  3. 如果函数都是细粒度,那么函数的覆写也会容易些

提炼函数的步骤如下:

  1. 创建一个新函数,根据这个函数的意图来对他命名
    即使你想要提炼的代码非常简单,例如只是一条消息或一个函数调用,只要新函数的名称能够以更好方式昭示代码意图,你也应该提炼他。但如果你想不出一个更有意义的名称,就别动

  2. 将提炼的代码从源码函数复制到新建的目标函数中。

  3. 仔细检查提炼出来的代码,看看其中是否引用了“作用域限于原函数”的变量(包括局部变量和原函数参数)
  4. 检查是否有“仅用于被提炼代码段”的临时变量。如果有,在目标函数中将他们声明为临时变量。
  5. 检查被提炼代码段,看看是否有任何局部变量的值被它改变。如果一个临时变量值被修改了,看看是否可以将被提炼代码段处理为一个查询,并将结果赋值给相关变量。如果很难这样做,或如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动的提炼出来,你可能需要先分解临时变量,然后再次尝试提炼。也可以以查询替代临时变量。
  6. 将被提炼代码段中年需要读取的局部变量,当做参数传给目标函数。
  7. 处理完所有局部变量之后,进行编译。
  8. 在源函数中,将被提炼代码段替换为对目标函数的调用。(如果你将任何临时变量移到目标函数中,请检查他们原本的声明式是否在被提炼代码段的外围,如果是,现在你就可以删除这些声明式了)
  9. 编译,测试。

范例:无局部变量
在最简单的情况下,提炼函数易如反掌,请看下列函数:

void printOwing() {
    Enumeration e = _orders.elements();
    double outstanding = 0.0;

    //print Banner
    System.out.println("*********************");
    System.out.println("****Customer Owes****");
    System.out.println("*********************");

    // calculate outstanding
    while(e.hasMoreElements()) {
        Order each = (Order)e.nextElement();
        outstanding += each.getAmount();
    }

    //print details
    System.out.println("name:" + _name);
    System.out.println("amount:" + outstanding);
}

我们可以轻松提炼出“打印横幅”的代码。我们只需要剪切、粘贴、再插入一个函数调用动作就行了:

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
    System.out.println("name:" + _name);
    System.out.println("amount:" + outstanding);
}

void printBanner() {
    //print Banner
    System.out.println("*********************");
    System.out.println("****Customer Owes****");
    System.out.println("*********************");
}

范例:有局部变量
果真这么简单,这个重构手法的困难点在哪里?是的,就在局部变量,包括传进源函数的参数和源函数所声明的临时变量。局部变量的作用域仅限于源函数,所以当我提炼函数时,必须花费额外的功夫去处理这些变量。
局部变量简单的情况是:被提炼代码段只是读取这些变量的值,并不修改它们。这种情况下我可以简单的将他们当做参数传给目标函数。所以如果我面对下列函数:

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
    System.out.println("name:" + _name);
    System.out.println("amount:" + outstanding);
}

void printBanner() {
    //print Banner
    System.out.println("*********************");
    System.out.println("****Customer Owes****");
    System.out.println("*********************");
}

就可以将“打印详细信息”这一部分提炼为带一个参数的函数:

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);
}

private void printDetails(double outstanding) {
    //print details
    System.out.println("name:" + _name);
    System.out.println("amount:" + outstanding);
}

void printBanner() {
    //print Banner
    System.out.println("*********************");
    System.out.println("****Customer Owes****");
    System.out.println("*********************");
}

必要的话,你可以用这种手法处理多个局部变量。
如果局部变量是个对象,而被提炼代码段调用了会对该对象造成修改的函数,也可以如法炮制。你同样只需将这个对象作为参数传递给目标函数即可。只有在被提炼代码段真的对一个局部变量赋值的情况下,你才必须采取其他措施。

范例:对局部变量再赋值
如果被提炼代码段对局部变量赋值,问题就变得复杂了。这里我们只讨论临时变量的问题。如果你发现源函数的参数被赋值,应该马上移除对参数的赋值。
被赋值的临时变量也分两种情况。较简单的情况是:这个变量只在被提炼代码段中使用。果真如此,你可以将这个临时变量的声明移到被提炼代码段中,然后一起提炼出去。另一种情况是:被提炼代码段之外的代码也使用了这个变量。这又分两种情况:如果这个变量在被提炼代码段之后未再被使用,你只需直接在目标函数中修改它就可以了;如果被提炼代码段之后还是用了这个变量,你就需要让目标函数返回该变量的值。我以下列代码说明这几种不同情况:

    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 printOwing() {
    printBanner();
    double outstanding = getOutstanding();
    printDetails(outstanding);
}

private double getOutstanding() {
    Enumeration e = _orders.elements();
    double outstanding = 0.0;
    while(e.hasMoreElements()) {
        Order each = (Order)e.nextElement();
        outstanding += each.getAmount();
    }
    return outstanding;
}

Enumeration变量e只在被提炼代码段中用到,所以可以将它整个搬到新函数中。double变量outstanding在被提炼代码段内外都被用到,所以必须让提炼出来的新函数返回它。编译测试完成后,我就把回传值改名,遵循我的一贯命名原则:

    private double getOutstanding() {
        Enumeration e = _orders.elements();
        double result = 0.0;
        while(e.hasMoreElements()) {
            Order each = (Order)e.nextElement();
            result += each.getAmount();
        }
        return result;
    }

本例中的outstanding变量只是很单纯地被初始化为一个明确初值,所以我可以只在新函数中对他初始化。如果代码还对这个变量做了其他处理,就必须将它的值作为参数传给目标函数。对于这种变化,最初代码可能是这样:

     void printOwing(double previousAmount) {
        Enumeration e = _orders.elements();
        double outstanding = previousAmount * 1.2;

        printBanner();

        // calculate outstanding
        while(e.hasMoreElements()) {
            Order each = (Order)e.nextElement();
            outstanding += each.getAmount();
        }

        printDetails(outstanding);
    }

提炼后的代码可能是这样:

    void printOwing(double previousAmount) {
        double outstanding = previousAmount * 1.2;
        printBanner();
        outstanding = getOutstanding(outstanding);
        printDetails(outstanding);
    }

    double getOutstanding(double initialValue) {
        double result = initialValue;
        Enumeration e = _orders.elements();
        while(e.hasMoreElements()) {
            Order each = (Order)e.nextElement();
            result += each.getAmount();
        }
        return result;
    }

编译并测试后,我再将变量outstanding的初始化过程整理一下:

    void printOwing(double previousAmount) {
        printBanner();
        double outstanding = getOutstanding(previousAmount * 1.2);
        printDetails(outstanding);
    }

这时候,你可能会问:如果返回的变量不止一个,又该怎么办呢?
有几种选择,最好的选择通常是:挑选另一块代码来提炼。我比较喜欢让每个函数都返回一个值,所以会安排多个函数,用以返回多个值。

内联函数

–在函数调用点插入函数本体,然后移除该函数。

    int getRating() {
        return (moreThanFiveLateDeliveries()) ? 2 : 1;
    }

    boolean moreThanFiveLateDeliveries() {
        return _numberOfLateDeliveries > 5;
    }
    int getRating() {
        return (_numberOfLateDeliveries > 5) ? 2 : 1;
    }

动机
重构中常以简短的函数表现动作意图,这样会使代码更清晰易读。但有时候会遇到某些函数其内容代码和函数名称同样清晰易读。亦可能你重构了该函数,使得其内容和其名称变得同样清晰,果真如此,你就应该去掉这个函数,直接使用其中的代码。间接性可能带来帮助,但非必要的间接性总让人不舒服。凡事有个度。

内联临时变量

–你有一个临时变量,只被一个简单表达式赋值一次,而他妨碍了其他重构手法。

    double basePrice = anOrder.basePrice();
    return (basePrice > 1000);
    return anOrder.basePrice() > 1000;

以查询取代临时变量

–你的程序以一个临时变量保存某一表达式的运算结果。
将这个表达式提炼到一个独立函数中,将这个临时变量的所有引用点替换为对新函数的调用,此后新函数就可以被其他函数使用。

double basePrice = _quantity * _itemPrice;
if (basePrice > 1000)
    return basePrice * 0.95;
else
    return basePrice * 0.98;
if (basePrice() > 1000)
    return basePrice() * 0.95;
else
    return basePrice() * 0.98;

...
double basePrice() {
    return _quantity * _itemPrice;
}

动机
临时变量的问题在于:他们是暂时的,而且只能在所属函数内使用。由于临时变量只在所属函数内可见,所以他会驱使你写出更长的函数,因为只有这样你才能访问到需要的临时变量。如果把临时变量替换为一个查询,那么同一个类中的所有函数都讲可以获得这份信息。这将带给你极大帮助,使你能够为这个类编写更清晰的代码。

未完,待续。。。

你可能感兴趣的:(Java)