MENU

《重构》第六章学习笔记

May 15, 2022 • 《重构》学习笔记

《重构》第六章学习笔记

重新组织函数

本章介绍了一些对函数整理的重构手法。

提炼函数(Extract Method)

  • 动机:对于过长的函数将其拆分放在独立的函数中。如果每个函数的力度都很小,那么函数被复用的机会就更大。其次,这会使高层函数读起来就像一系列注释。而且,如果函数都是细粒度,那么函数的覆写也会更容易些。
  • 做法:

    1. 创造一个新函数,根据这个函数的意图来对它命名
    2. 将提炼出的代码从原函数复制到新建的目标函数中。
    3. 检查提炼出的代码是否引用了作用域限于原函数的变量。
    4. 检查是否有仅用于被提炼代码段的临时变量,如果有,在目标函数中将它们声明为临时变量。
    5. 检查被提炼代码段,看看是否存在被改变的变量值。
    6. 将被提炼代码段中需要读取的局部变量,当做参数传给目标函数。
    7. 处理完所有局部变量后,编译。
    8. 在原函数中将提炼的代码段换成对目标函数的调用。
    9. 编译测试。
void printOwing(double amount){
        printBanner();
        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);
}

内联函数(Inline Method)

  • 动机:与上一种手法刚好相反,如果遇到某些函数,其内部代码和函数名称同样清晰易读。也可能你重构了该函数,使其内容和名称变得同样清晰。如果是这样,你就应该去掉这个函数,直接使用其中的代码。间接性可能带来帮助,但非必要的间接性总是让人不舒服。
  • 做法:

    1. 检查函数,确定它不具有多态性。
    2. 找出这个函数的所有被调用点。
    3. 将这个函数的所有被调用点都替换为函数本体。
    4. 编译测试。
    5. 删除该函数的定义。
int getRating(){
    return (moreThanFiveLateDeliveries()) ? 2 : 1;
}

boolean moreThanFiveLateDeliveries() {
    return _numberOfLateDeliveries > 5;
}

​ ↓↓

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

内联临时变量(Inline Temp)

  • 动机:如果这个临时变量妨碍了其他的重构手法,你就应该将其内联化。
  • 做法:

    1. 检查给临时变量赋值的语句,确保等号右边的表达式没有副作用。
    2. 如果这个临时变量并未被声明final,那就将他声明为final,然后编译。(这可以检查该临时变量是否真的只被复制一次)
    3. 找到该临时变量的所有引用点,将它们替换为“为临时变量赋值”的表达式。
    4. 每次修改后,编译测试。
    5. 修改完所有引用点之后,删除该临时变量的声明和赋值语句。
    6. 编译测试。
double basePrice = anOrder.basePrice()
return (basePrice > 1000);

​ ↓↓

return (anOrder.basePrice() > 1000);

以查询取代临时变量(Replace Temp with Query)

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

    1. 找出只被赋值一次的临时变量。(如果某个临时变量被多次赋值,考虑使用Split Temporary Variable分割为多个变量)
    2. 将该临时变量声明为final。
    3. 编译。(为了确保被声明final的临时变量只被赋值一次)
    4. 将“对该临时变量赋值”的语句等号右侧提炼到另一个独立函数中。(确保提炼出来的函数无任何副作用)
    5. 编译测试。
    6. 在该临时变量身上实施Inline Temp内联临时变量。
double getValue(){
    double basePrice = quantity * itemPrice;
    if (basePrice > 1000)
        return basePrice * 0.95;
    else 
        return basePrice * 0.98;
}

​ ↓↓

double getValue(){
    if (basePrice > 1000)
        return basePrice * 0.95;
    else
        return basePrice * 0.98;
}

double price(){
    return quantity * itemPrice;
}

引入解释性变量(Introduce Explaining Variable)

  • 动机:表达式可能会非常复杂且难以阅读。在条件逻辑中,这种重构手法就特别有价值,你可以用这项重构将每个条件自居提炼出来,以一个良好命名的临时变量来解释对应条件子句的意义。另一种情况是,在较长的算法中,可以运用临时变量来解释每一步运算的意义。
  • 做法:

    1. 声明一个final临时变量,将待分解的复杂表达式中的一部分动作的运算结果赋值给它。
    2. 将表达式中的“运算结果”这一部分,替换为上述临时变量。
    3. 编译测试。
    4. 重复上述步骤,处理表达式的其他部分。
if ((platform.toUpperCase().indexOf("MAC") > -1) && 
    (browser.toUpperCase().indexOf("IE") > -1) && 
    wasInitialized() && 
    resize > 0){
    //...
}

​ ↓↓

final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIE = browser.toUpperCase().indexOf("IE") > -1;
final boolean wasResized = resize > 0;

if (isMacOs && isIE && wasResized && wasInitialized()){
    //...
}

分解临时变量(Split Temporary Variable)

  • 动机:临时变量有各种不同用途,其中某些用途会导致临时变量被多次赋值。

    • 循环变量:循环变量会随着循环的每次运行而改变,比如for(int i = 0; i < 10; i++)中的i
    • 结果收集变量:负责将“通过整个函数的运算”而构成的某个值收集起来。
    • 临时变量保存一段冗长代码的运算结果,这种变量应只被赋值一次,如果多次赋值会导致代码阅读者看糊涂了。
  • 做法:

    1. 在待分解临时变量的声明及其第一次被赋值处,修改其名称。(如果循环中的赋值语句是[i=i+某表达式]的形式,就意味这是个结果收集变量,那么就不要分解它。结果收集变量的作用通常是累加,字符串接合,写入流或者向集合添加元素。)
    2. 将新的临时变量声明为final。
    3. 以该临时变量的第二次赋值动作为界,修改此前对该临时变量的所有引用点,让它们引用新的临时变量。
    4. 在第二次赋值出,重新声明原先那个临时变量。
    5. 编译测试。
    6. 逐次重复上面的步骤,每次都在声明处对临时变量改名,并修改下次赋值之前的引用点。
double temp = 2 * (height + width);
System.out.println(temp);
temp = height * width;
System.out.println(temp);

​ ↓↓

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

移除对参数的赋值(Remove Assignments to Parameters)

  • 动机:“对参数赋值”是指对传递进来的参数进行操作,这会降低代码的清晰度,而且混用了按值传递和按引用传递这两种参数传递方式。在按值传递的情况下,对参数的任何修改,都不会对调用端造成任何影响。那些用过按引用传递方式的人可能会在这一点上犯糊涂。另一个会让人糊涂的地方是函数本体内。如果你只以参数表示“被传递进来的东西”,那么代码会清晰得多,因为这种语法在所有语言中都表现出相同语义。
  • 做法:

    1. 建立一个临时变量,把待处理的参数值赋予它。
    2. 以“对参数的赋值”为界,将其后所有对此参数的引用点,全部替换为“对此临时变量的引用”。
    3. 修改赋值语句,使其改为对新建的临时变量赋值。
    4. 编译测试。
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;
    //...
}

以函数对象取代函数(Replace Method with Method Object)

  • 动机:局部变量的存在会增加函数分解难度。如果一个函数之中局部变量泛滥成灾,那么要分解这个函数是非常困难的。Replace Temp with Query可以帮助你减轻这一负担,但是有的时候会发现根本难以拆解一个需要拆解的函数,这是就可以使用Replace Method with Method Object,将所有局部变量都变成函数对象的字段。然后就可以对这个新对象使用Extract Method创造新函数,从而拆解复杂函数。
  • 做法:

    1. 建立一个新类,根据待处理函数的用途,为这个类命名。
    2. 在新类中建立一个final字段,用以保存原先大型函数所在的对象。我们将这个字段称为“源对象”。同时,针对原函数的每个临时变量和每个参数,在新类中建立一个对应的字段保存。
    3. 在新类中建立一个构造函数,接受源对象及原函数的所有参数作为参数。
    4. 在新类中建立一个compute()函数。
    5. 将原函数的代码复制到compute()函数中。如果需要调用源对象的任何函数,请通过源对象字段调用。
    6. 编译。
    7. 将旧函数的函数本体替换为这样一条语句:“创建上诉新类的一个新对象,然后调用其中的compute()函数”。
class Order {
    double price(){
        double aPrice;
        double bPrice;
        double cPrice;
        // long computation
    }
}

​ ↓↓

class Order {
    double price(double aPrice, double bPrice, double cPrice){
        return new PriceCalculator(this, aPrice, bPrice, cPrice).compute();
    }
}

class PriceCalculator {
    double aPrice;
    double bPrice;
    double cPrice;
    Order order;

    PriceCalculator(Order order, double aPrice, double bPrice, double cPrice) {
        this.order = order;
        this.aPrice = aPrice;
        this.bPrice = bPrice;
        this.cPrice = cPrice;
    }

    int compute(){
        //long computation
    }
}

替换算法(Substitute Algorithm)

  • 动机:在对算法进行重构时,你有时会发现更简单清晰的处理方式,重构可以把复杂的东西分解成简单的小块,但有时需要壮士断腕,删掉整个算法,用简单的方法取代。比如使用程序库,其中某些功能或者特性与算法代码重复,这是就可以改变它。在使用Substitute Algorithm前需要尽可能了解原先的函数。替换一个巨大且复杂的算法是非常困难的,只有将其分解成简单的小型函数,才能很有把握的进行算法替换工作。
  • 做法:

    1. 准备好另一个替换用的算法,让它通过编译测试。
    2. 针对现有测试,执行新算法。如果结果与原本的结果相同,重构结束。
    3. 如果测试结果存在差异,在测试和调试过程中,以旧算法为参照标准。