# 第6章:重新组织函数
很多问题都源于 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);
}
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);
}
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);
}
}
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("**************************");
}
}
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);
}
}
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;
}
}
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;
}
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);
}
}
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;
}
}
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);
}
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;
}
}
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;
}
}
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);
}
}
2
3
4
5
6
7
8
9
改动后:
public class InlineTemp {
private Order anOrder = new Order(900);
public boolean inLineTemp() {
return (anOrder.basePrice() > 1000);
}
}
2
3
4
5
6
7
8
# 2、动机
Inline Temp
多半是作为 Replace Temp with Query
的一部分来使用,所以真正的动机出现在后者那儿。惟一单独使用 Inline Temp
的情况是:你发现某个临时变量被赋予某个函数调用的返回值。一般来说,这样的临时变量不会有任何危害,你可以放心地把它留在那儿。但如果这个临时变量妨碍了其他的重构手法——例如 Extract Method
,你就应该将它内联化。
# 3、做法
- 检查给临时变量赋值的语句,确保等号右边的表达式没有副作用。
- 如果这个临时变量并未声明为
final
,那就将它声明为final
,然后编译。这可以检查该临时变量是否真的只被赋值一次。 - 找到该临时变量的所有引用点,将它们替换为“为临时变量赋值”的表达式。
- 每次修改后,编译并测试。
- 修改完所有引用点之后,删除该临时变量的声明式和赋值语句。
- 编译,测试。
← 第5章:重构列表