重新组织函数

0. 本章内容导图

重新组织函数_第1张图片
重新组织函数

1. 重构手法

1.1 提炼函数

概要:
有一段代码可以被组织在一起并独立出来。
将这段代码放进一个独立函数中,并让函数名称解释该函数的用途。
动机:
a. 函数保持较小粒度,容易被复用
b. 使得高层函数读起来就像是一系列注释
c. 细粒度的函数更容易被覆写
示例:
重构前:

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 内联函数

概要:
一个函数的本体与名称同样清楚易懂。
在函数调用点插入函数本体,然后移除该函数。
动机:
a. 去除间接性带来的思维跳跃
b. 高层函数组织不合理,内联后重新组织提炼新的函数
c. 去除过多的间接性委托
示例:
重构前:

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

boolean moreThanFiveLateDeliveries() {
    return numberOfLateDeliveries > 5;
}

重构后:

int getRating() {
    return (numberOfLateDeliveries > 5) ? 2 : 1;
}

总结:
    不必要的间接性会阻碍理解的连贯性。
    代码在经多次修改后,组织可能已经不甚合理,将这些函数内联后重新组织提炼,可以使之继续散发芬芳。

1.3 内联临时变量

概要:
有一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其他重构手法。
将所有对该变量的引用动作,替换为对它赋值的那个表达式自身。
动机:
a. 此临时变量给其他重构带来困难,其本身也没什么存在价值
b. 给临时变量赋值的表达式在其他函数中也有使用,可提炼为一个查询函数
示例:
重构前:

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

重构后:

return (anOrder.basePrice() > 1000);

总结:
    此方法可以去除一些不必要的临时变量,或者作为其他重构手法中的一环。

1.4 以查询取代临时变量

概要:
程序中以一个临时变量保存某一表达式的运算结果。
将这个表达式提炼到一个独立函数中,将这个临时变量的所有引用点替换为对新函数的调用。此后,新函数也可以被其他函数调用。
动机:
a. 类中的其他部分也需要访问临时变量保存的信息
b. 提炼函数时受临时变量困扰,为消除临时变量
示例:
重构前:

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

1.5 引入解释性变量

概要:
你有一个复杂的表达式。
将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。
动机:
a. 简化条件表达式
示例:
重构前:

if ( (platform.toUpperCase().indexOf("MAC") > -1)
        && (browser.toUpperCase().indexOf("IE") > -1)
        && wasInitialized() && resize > 0 ) {
    // do something
}

重构后:

final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
final boolean wasResized = resize > 0;
if ( isMacOs && isIEBrowser && wasInitialized() && wasResized ) {
    // do something
}

总结:
    引入解释性变量常用来简化条件表达式,也可以提取函数来封装复杂的表达式,用函数名来解释复杂表达式的功能。

1.6 分解临时变量

概要:
程序中有某个临时变量被赋值超过一次,它既不是循坏变量,也不被用于收集计算结果。
针对每次赋值,创造一个独立、对应的临时变量。
动机:
a. 消除变量被多次赋值造成的理解上的胡乱
示例:
重构前:

double tmp = 2 * (height + width);
System.out.println(tmp);
tmp = height * width;
System.out.println(tmp);

重构后:

final double perimeter = 2 * (height + width);
System.out.println(perimeter);
final double area = height * width;
System.out.println(area);

总结:
    单一职责不仅只针对类而言,函数、临时变量都应该保持单一职责。对于临时变量而言,前后不一致的职责势必会造成理解混乱,也容易产生bug。

1.7 移除对参数的赋值

概要:
代码对一个参数进行赋值。
以一个临时变量取代该参数的位置。
动机:
a. 消除值传递、引用传递在语言间易造成的误解
b. 让参数只代表“传递进来的东西”,保持函数本体内的代码清晰
示例:
重构前:

int discount(int inputVal, int quantity, int yearToDate) {
    if (inputVal > 50) {
        inputVal -= 2;
    }
}

重构后:

int discount(int inputVal, int quantity, int yearToDate) {
    int result = inputVal;
    if (inputVal > 50) {
        result -= 2;
    }
}

总结:
    要理解按值传递和按引用传递的含义。Java中都是按值传递的,当参数传递的是对象时,实际上是多了一个临时变量指向此对象所在的内存区域,由于都指向同一块内存区域,因此也是可以在“被传入对象”上进行操作的。
    当传入的参数是集合对象时,要尤其谨慎。

1.8 以函数对象取代函数

概要:
有一个大型函数,里面使用了很多局部变量,使得你无法采用提炼函数的手法对其进行重构。
可将这个函数放进一个单独对象中,这样一来,局部变量就成了对象的字段,就可以在同一个对象中将这个大型函数分解为多个小型函数。
动机:
a. 消除大型函数中因局部变量太多而无法分解成多个小函数的问题
示例:
重构前:

class Order {
    // field
    ...

    //类中的大型方法,局部变量过多难以提炼函数
    double price() {
        double var1;
        double var2;
        ...
        double varN;

        // long computation
        ...
    } 

    // other method
    ...
}

重构后:

// 新建一个类,将上述大型方法放入此类,局部变量变为类的字段
class PriceCalculator {
    private double var1;
    private double var2;
    ...
    private double varN;

    public double price() {
        calculate1();
        calculate2();
        // call other method
        ...
    }

    private void calculate1() {
        // some operation
    }

    private void calculate2() {
        // some operation
    }

    // other small method
    ...

}

class Order {
    ...
    private PriceCalculator calculator;

    public double price() {
        calculator.price();
    }
    ...
}

总结:
    提炼函数的困难在于对局部变量的处理,当大型函数里的局部变量太多,由于作用域的关系,使得根本无法提炼函数对其进行分解,为这个大型函数新建一个类,将函数局部变量变为类的字段,就可以轻松拆分这个大型函数了。

1.9 替换算法

概要:
你想要把某个算法替换为另一个更清晰的算法。
将函数本体替换为另一个算法。
动机:
a. 为了采用更清晰、更简单或性能更好的算法
示例:
重构前:

String findPerson(String[] people) {
    for(int i = 0; i < people.length; i++) {
        if(people[i].equals("Don")) {
            return "Don";
        }
        if(people[i].equals("John")) {
            return "John";
        }
        if(people[i].equals("Kent")) {
            return "Kent";
        }
    }
    return "";
}

重构后:

String findPerson(String[] people) {
    List candidates = Arrays.asList(new String[] {"Don", "John", "Kent"});
    for(int i = 0; i < people.length; i++) {
        if(candidates.contains(people[i])) {
            return people[i];
        }
    }
    return "";
}

总结:
    对问题的了解越深,可以想到的更好的解决方案就会越多,或者需要对原有的算法进行性能优化,此时你就需要改变原先的算法。替换一个巨大而复杂的算法是非常困难的,在替换之前,你可能需要先将它分解成较为简单的小型函数。

你可能感兴趣的:(重新组织函数)