# 第6章:重新组织函数

loading

很多问题都源于 Long Methods。对付过长函数,一项重要的重构手法就是 Extract Method,它把一段代码从原先函数中提取出来,放进一个单独函数中。Inline Method 正好相反,将一个函数调用动作替换为该函数本体。如果在多次提炼之后,意识到提炼所得的某些函数并没有做任何实质事情,或如果需要回溯回复原先函数,就需要 Inline Method

Extract Method 最大的困难就是处理局部变量,而临时变量则是其中一个主要的困难源头。处理一个函数时,我最喜欢运用 Replace Temp with Query 去掉所有可去掉的临时变量。如果很多地方使用了某个临时变量,我就会先运用 Split Temporary Variable 将它变得比较容易替换。

有时候临时变量太混乱,这时候就需要使用 Replace Method with Method Object,代价是引入一个新类。

参数带来的问题比临时变量稍微少一些,前天是你不在函数内赋值给它们。如果你已经这样做了,就得使用 Remove Assignments to Parameters

如果发现算法可以改进,可以使用 Substitue Algorithm 引入更清晰的算法。

# 一、Extract Method(提炼函数)

# 1、概要

原本的方法:

    void printOwing(double amount) {
        printBanner();

        // print details
        System.out.println("name: " + _name);
        System.out.println("amount: " + amount);
    }
1
2
3
4
5
6
7

应当将后面的代码放进一个独立函数中,并让函数名称解释该函数的用途:

    void printOwing(double amount) {
        printBanner();
        printDetails(amount);
    }
    
    void printDetails(double amount) {
        System.out.println("name: " + _name);
        System.out.println("amount: " + amount);
    }
1
2
3
4
5
6
7
8
9

# 2、动机

首先,如果每个函数的粒度都很小,那么函数被复用的机会就更大;其次,这会使高层函数读起来就像一系列注释;再次,如果函数都是细粒度,那么函数的复写也会更容易。

函数的长度不是问题,关键在于函数名称和函数本体之间的语意距离。如果提炼可以强化代码的清晰度,那么就去做,就算函数名比提炼出来的代码还长也无所谓。

# 3、做法

  • 创造一个新函数,根据这个函数的意图来对它明明(以它“做什么”来命名,而不是以它“怎样做”命名)。
  • 将提炼出的代码从原函数复制到新建的目标函数中
  • 仔细检查提炼出的代码,看看其中是否引用了“作用域咸鱼源函数”的变量(包括局部变量和源函数参数)。
  • 检查是否有“仅用于被提炼代码段”的临时变量。如果有,在目标函数中将它们声明为临时变量。
  • 检查被踢连代码段,看看是否有任何局部变量的值被它改变。如果一个临时变量值被修改了,看看是否可以将被提炼代码段处理为一个查询,并将结果赋值给相关变量。如果很难这样做,或如果被修改的变量不止一个,就不能仅仅将这段代码原封不动的提炼出来。可能需要先使用 Split Temporary Variable,然后再尝试提炼。也可以使用 Replace Temp with Query 将临时变量消灭掉。
  • 将被提炼代码段中需要读取的局部变量,当做参数传给目标函数。
  • 处理完所有的局部变量之后,进行编译。
  • 在源函数中,将被提炼代码段替换为目标函数的调用。
  • 编译,测试。

# 4、范例

# (1) 无局部变量

原本的函数:

public class Example1 {

    private static Vector<Order> _orders = new Vector<>();
    private String _name = "Example1";

    static {
        _orders.add(new Order(14.5));
    }

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

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

将打印横幅这部分提炼出一个方法:

public class Example1 {

    private static Vector<Order> _orders = new Vector<>();
    private String _name = "Example1";

    static {
        _orders.add(new Order(14.5));
    }

    public 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("**************************");        
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# (2) 有局部变量

上述提炼之后的代码,可以将“打印详细信息”这一部分提炼为一个带一个参数的函数:

public class Example1 {

    private static Vector<Order> _orders = new Vector<>();
    private String _name = "Example1";

    static {
        _orders.add(new Order(14.5));
    }

    public 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 printBanner() {
        // print banner
        System.out.println("**************************");
        System.out.println("***** Customer Owes ******");
        System.out.println("**************************");
    }
    
    void printDetails(double outstanding) {
        // print details
        System.out.println("name:" + _name);
        System.out.println("amount" + outstanding);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

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

# (3) 对局部变量再赋值

如果被提炼的代码段对局部变量复制,那么应该使用 Remove Assignments to Parameters

被赋值的临时变量也分两种情况。较简单的情况是:这个变量只在被提炼代码段中使用。果真如此,可以将这个临时变量的声明移到被提炼代码段段中,然后一起提炼出去。另一种情况是:被提炼码之外的代码也使用了这个变量。这又分为两种情况: 如果这个变量在被提炼码之后未再被使用,你只需直接在目标函数中修改它就可以了;如果被提炼码之后的代码还使用了这个变量,你就需要让目标函数返回该变量改变后的值。

接着上面的代码,接下来把计算部分也提炼出来:

public class Example1 {

   private static Vector<Order> _orders = new Vector<>();
   private String _name = "Example1";

   static {
       _orders.add(new Order(14.5));
   }

   public void printOwing() {
       printBanner();

       double outstanding = getOutStanding();

       printDetails(outstanding);
   }

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

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

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

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

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

    double getOutStanding() {
       Enumeration e = _orders.elements();
       double result = 0.0;
       // calculate outstanding
       while (e.hasMoreElements()) {
           Order each = (Order) e.nextElement();
           result += each.getAmount();
       }
       return result;
   }
1
2
3
4
5
6
7
8
9
10

如果代码中还对提炼出来的函数中的变量做了其他处理,就必须将它的值作为参数传递给目标函数。例如最初代码可能是这样子的:

public class Example2 {
   
   private static Vector<Order> _orders = new Vector<>();
   private String _name = "Example2";
   private double previousAmount;

   static {
       _orders.add(new Order(14.5));
   }

   public void printOwing() {
       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 printBanner() {
       // print banner
       System.out.println("**************************");
       System.out.println("***** Customer Owes ******");
       System.out.println("**************************");
   }

   void printDetails(double outstanding) {
       // print details
       System.out.println("name:" + _name);
       System.out.println("amount" + outstanding);
   }
   
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

提炼后的代码:

public class Example2 {
   
   private static Vector<Order> _orders = new Vector<>();
   private String _name = "Example2";
   private double previousAmount;

   static {
       _orders.add(new Order(14.5));
   }

   public void printOwing() {
       double outstanding = previousAmount * 1.2;

       printBanner();

       outstanding = getOutstanding(outstanding);

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

   void printDetails(double outstanding) {
       // print details
       System.out.println("name:" + _name);
       System.out.println("amount" + outstanding);
   }
   
   double getOutstanding(double initialValue) {
       double result = initialValue;
       Enumeration e = _orders.elements();
       // calculate outstanding
       while (e.hasMoreElements()) {
           Order each = (Order) e.nextElement();
           result += each.getAmount();
       }
       return result;
   }
   
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

编译测试后,将 outstanding 的初始化过程整理一下:

   public void printOwing() {
       printBanner();

       double outstanding = getOutstanding(previousAmount * 1.2);

       printDetails(outstanding);
   }
1
2
3
4
5
6
7

如果需要返回的变量不止一个,又该怎么办呢?

有数种选择。最好的选择通常是:挑选另一块代码来提炼。安排多个函数,用以返回多个值。如果使用的语言支持“出参数”(output parameters),你可以使用它们带回多个回传值。但还是尽可能选择单一返回值。

临时变量往往为数众多,甚至会使提炼工作举步维艰。这种情况下,我会尝试先运用 Replace Temp with Query 减少临时变量。如果即使这么做了提炼依旧困难重重,我就会动用 Replace Method with Method Object,这个重构手法不在乎代码中有多少临时变量,也不在乎你如何使用它们。

# 二、Inline Method(内联函数)

# 1、概要

一个函数的本体与名称同样清楚易懂。

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

改动前:

public class InlineMethod {

    private int _numberOfLateDeliveries;

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

    boolean moreThanFiveLateDeliveries() {
        return _numberOfLateDeliveries > 5;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13

改动后:

public class InlineMethod {

    private int _numberOfLateDeliveries;

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

}
1
2
3
4
5
6
7
8
9

# 2、动机

某些函数其内部代码和函数名称同样清晰易读。也可能重构了某个函数,使得其内容和其名称变得同样清晰。果真如此,就应该去掉这个函数,直接使用其中的代码。

另一种需要使用 Inline Method 的情况是:你手上有一群组织不甚合理的函数。你可以将它们都内联到一个大型函数中,再从中提炼出组织合理的小型函数。实施 Replace Method with Method Object 之前先这么做,往往可以获得不错的效果。你可以把你所要的函数(有着你要的行为)的所有调用对象的函数内容都内联到函数对象中。比起既要移动一个函数,又要移动它所调用的其他所有函数,将大型函数作为单一整体来移动会比较简单。

如果别人使用了太多间接层,使得系统中的所有函数都似乎只是对另一个函数的简单委托,造成我在这些委托动作之间晕头转向,那么我通常都会使用 Inline Method。当然,间接层有其价值,但不是所有间接层都有价值。试着使用 Inline Method,找出那些有用的间接层,同时将那些无用的间接层去除。

# 3、做法

  • 检查函数,确定它不具多态性。 如果子类继承了这个函数,就不要将此函数内联化,因为子类无法覆写一个根本不存在的函数。
  • 找出这个函数的所有被调用点。
  • 将这个函数的所有被调用点都替换为函数本体(代码)。
  • 编译,测试。
  • 删除该函数的定义。

# 三、Inline Temp(内联临时变量)

# 1、概要

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

将所有对该变量的引用动作,替换为对它赋值的那个表达式本身。

改动前:

public class InlineTemp {

    private Order anOrder = new Order(900);

    public boolean inLineTemp() {
        double basePrice = anOrder.basePrice();
        return (basePrice > 1000);
    }
}
1
2
3
4
5
6
7
8
9

改动后:

public class InlineTemp {

    private Order anOrder = new Order(900);

    public boolean inLineTemp() {
        return (anOrder.basePrice() > 1000);
    }
}
1
2
3
4
5
6
7
8

# 2、动机

Inline Temp 多半是作为 Replace Temp with Query 的一部分来使用,所以真正的动机出现在后者那儿。惟一单独使用 Inline Temp 的情况是:你发现某个临时变量被赋予某个函数调用的返回值。一般来说,这样的临时变量不会有任何危害,你可以放心地把它留在那儿。但如果这个临时变量妨碍了其他的重构手法——例如 Extract Method,你就应该将它内联化。

# 3、做法

  • 检查给临时变量赋值的语句,确保等号右边的表达式没有副作用。
  • 如果这个临时变量并未声明为 final,那就将它声明为 final,然后编译。这可以检查该临时变量是否真的只被赋值一次。
  • 找到该临时变量的所有引用点,将它们替换为“为临时变量赋值”的表达式。
  • 每次修改后,编译并测试。
  • 修改完所有引用点之后,删除该临时变量的声明式和赋值语句。
  • 编译,测试。

上次更新: 2020-08-21 09:02:51(10 小时前)