动机(Motivation)
Extract Method是我们最常用的重构手法之一.当我们看见一个过长的函数或一段需要注释才能让人理解用途的代码,我们就会将这段代码放进一个独立函数中.
有数个原因造成我们喜欢简短而有良好命名的函数.首先,如果每个函数的粒度都很小(finely grained),那么函数之间彼此复用的机会就更大;其次,这会使高层函数代码读起来就像一系列注释;再者,如果函数都是细粒度,那么函数的覆写(override)也会更容易些.
的确,如果你习惯看人型函数,恐怕需要一段时间才能适应这种新风格.而且只有当你能给小型函数很好地命名时,它们才能真正起到作用,所以你需要在函数名称下点功夫.人们有时会问,一个函数多长合适?在我看来,长度不是问题,关键在于函数名称和函数本体之间的语义距离(scmantic distance),如果提炼动作(extracting)可以强化代码的清晰度,那就去做,就算函数名称比提炼出来的代码还长也无所谓.
作法(Mechanics)
1、创造一个新函数,根据这个函数的意图来给它命名(以它做什么来命名,而不是以它怎么做命名)
即使你相聚要提炼(extract)的代码非常简单,例如只是一条消息或一个函数的调用,只要新函数的名称能够以更好方式昭示代码意图,你也应该提炼它,但如果你想不出一个更有意议的名称,就别动。
2、将提炼出的代码从源函数(source)拷贝到新建的目标函数(target)中。
3、仔细检查提炼出的代码,看看其中是否引用了作用域(scope),只限于源函数的变量(包括局部变量和源函数参数)。
4、检查是否有了公用于被提炼代码的临时变量(teemporary variables)如果有,在目标函数中将它声明为临时变量。
5、检查被提炼代码,看看是否有任何局部变量(local-scope variables)的值被它改变。如果一个临时变量值被修改了,看看是否可以将被提炼码处理为一个查询(query),并将结果赋值给相关变量。如果很难这样做,或如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动地提炼出来,你可能需要先使用Split Temporary Variable。然后再尝试提炼,也可以使用Replace Temp with Query将临时变量消灭掉。
6、将被提炼代码中需要读取的局部变量,当作参数传给目标函数。
7、处理完所有局部变量之后,进行编译。
8、在源函数中,将被提炼代码替换为(对目标函数的调用)
如果你将任何临时变量移到目标函数中,请检查它们原本的声名式是否在被提炼代码的外围,如果是,现在你可以删除这些声明式了。
范例(Examples)
1、无局部变量(No Local Variables)
03 |
public void PrintUserInfo() |
05 |
Console.WriteLine( "******************************" ); |
06 |
Console.WriteLine( "************用户信息**********" ); |
07 |
Console.WriteLine( "******************************" ); |
08 |
string Name = "spring yang" ; |
11 |
Console.WriteLine( string .Format( "name is {0},age is {1},sex is {2}" , Name, Age, Sex)); |
改为:
03 |
public void PrintUserInfo() |
06 |
string Name = "spring yang" ; |
09 |
Console.WriteLine( string .Format( "name is {0},age is {1},sex is {2}" , Name, Age, Sex)); |
12 |
public void PrintStart() |
14 |
Console.WriteLine( "******************************" ); |
15 |
Console.WriteLine( "************用户信息**********" ); |
16 |
Console.WriteLine( "******************************" ); |
2、有局部变量(Using Local Variables)
果真这么简单,这个重构手法的困难点在哪里?是的,就是在局部变量,包括传进源函数的参数和源函数所声明的临时变量。局部变量的作用域仅限于源函数。所以当我使用Extract Method时,必须花费额外功夫去处理这此变量。某些时候它们甚至可能妨碍我,使我根本无法进行这项重构。
局部变量最简单的情况是:被提炼代码只是读取这些变量的值,并不修改它们。这种情况下我可以简单地将它们当作参数传给目标函数。所发我们面对如下函数:
03 |
public void PrintUserInfo() |
05 |
string Name = "spring yang" ; |
08 |
Console.WriteLine( string .Format( "name is {0},age is {1},sex is {2}" , Name, Age, Sex)); |
09 |
Console.WriteLine( string .Format( "{0} both year is {1}." , Name, DateTime.Now.AddYears(- int .Parse(Age)))); |
改为:
03 |
public void PrintUserInfo() |
05 |
string Name = "spring yang" ; |
08 |
PrintInfo(Name, Age, Sex); |
11 |
public void PrintInfo( string Name, string Age, string Sex) |
13 |
Console.WriteLine( string .Format( "name is {0},age is {1},sex is {2}" , Name, Age, Sex)); |
14 |
Console.WriteLine( string .Format( "{0} both year is {1}." , Name, DateTime.Now.AddYears(- int .Parse(Age)))); |
如果局部变量是个对象,而被提炼代码调用了会对该对象造成修改的函数,也可以如法炮制。你同样只需将这个对象作为参数传递给目标函数即可。只有在被提炼代码真的对一个局部变量赋值的情况下,你才必须采取其它措施。
3、对局部变量现赋值(Reassigning)
如果被提炼代码对局部变量赋值,问题就变得复杂了。这里我们只讨论临时变量的问题。如果你发现源函数的参数被赋值,应该马上使用Remove Assignments to Parameters。
03 |
public void PrintSum() |
06 |
for ( int i = 0; i <= 100; i++) |
08 |
Console.WriteLine( "From 1 add to 100 result is {0}." , Result); |
改为:
03 |
public void PrintSum() |
11 |
for ( int i = 0; i <= 100; i++) |
16 |
public void PrintInfo( double result) |
18 |
Console.WriteLine( "From 1 add to 100 result is {0}." , result); |
总结
把能够独立的代码块给它独立出来,使代码的粒度变小,可重用性提高。