标签:john 今天 extract 逻辑 测试 没有 ace 算法 anti
重构手法中,很大一部分都是在对函数进行整理,很多问题也都来自Long Methods(过长的函数),下边就介绍一下关于重新组织函数的几种常用手法
1 Extract Method(提炼函数)
解释:一个函数中有部分代码可以被提取出来单独抽成一个函数,并起一个能表达函数用途的函数名,这就是提炼函数(一个大函数可以提出很多小函数)
如:原函数
void PrintOwing(double amount) { PrintBanner(); //print details Console.WriteLine("name:"+_name); Console.WriteLine("amount:"+_amount); }
重构提炼后的函数
void PrintOwing(double amount) { PrintBanner(); //print details PrintDetails(); } private void PrintDetails() { Console.WriteLine("name:" + _name); Console.WriteLine("amount:" + _amount); }
动机:
Extract Method (提炼函数)是最常用的重构手法之一。当看见一个过长的函数或者一段需要注释才能让人理解用途的代码,就应该将这段代码放进一个独立函数中。
简短而命名良好的函数的好处:首先,如果每个函数的粒度都很小,那么函数被复用的机会就更大;其次,这会使高层函数读起来就想一系列注释;再次,如果函数都是细粒度,那么函数的覆写也会更容易些。
一个函数多长才算合适?长度不是问题,关键在于函数名称和函数本体之间的语义距离。如果提炼可以强化代码的清晰度,那就去做,就算函数名称必提炼出来的代码还长也无所谓。
做法:
1、创造一个新函数,根据这个函数的意图对它命名(以它“做什么“命名,而不是以它“怎样做”命名)。
即使你想要提炼的代码非常简单,例如只是一条消息或一个函数调用,只要新函数的名称能够以更好方式昭示代码意图,你也应该提炼它。但如果你想不出一个更有意义的名称,就别动。
2、将提炼出的代码从源函数复制到新建的明白函数中。
3、仔细检查提炼出的代码,看看其中是否引用了“作用域限于源函数”的变量(包括局部变量和源函数参数)。
4、检查是否有“仅用于被提炼代码段”的临时变量。如果有,在目标函数中将它们声明为临时变量。
5、检查被提炼代码段,看看是否有任何局部变量的值被它改变。如果一个临时变量值被修改了,看看是否可以将被提炼代码处理为一个查询,并将结果赋值给修改变量。如果很难这样做,或如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动提炼出来。你可能需要先使用 Split Temporary Variable (分解临时变量),然后再尝试提炼。也可以使用 Replace Temp with Query (以查询取代临时变量)将临时变量消灭掉。
6、将被提炼代码段中需要读取的局部变量,当做参数传给目标函数。
7、处理完所有局部变量后,进行编译。
8、在源函数中,将被提炼代码段替换给对目标函数的调用。
如果你将如何临时变量移到目标函数中,请检查它们原本的声明式是否在被提炼代码段的外围。如果是,现在可以删除这些声明式了。
9、编译,测试。
注意:不要小看这个微小的变化,当一个函数很长的时候你就会发现他的威力,我们的项目肯定都比这个复杂,有时候你会发现有很多临时变量在干扰我们,没关系,请接着往下看4、6、8都是为了解决这个问题
2 InLine Method(内联函数)
解释:一个函数的本体与名称同样清楚易懂。也就是说某个小函数的代码体一看就知道什么意思,已经没有必要作为一个单独的函数,可以在调用函数的地方直接用代码体,然后移除该函数
如:原函数
int GetRating() { return MoreThanfiveLateDeliverise() ? 2 : 1; } bool MoreThanfiveLateDeliverise() { return _numberOfLateLiveries > 5; }
用内联函数方法重构后
int GetRating() { return _numberOfLateLiveries > 5 ? 2 : 1; }
动机: 有时候你会遇到某些函数,其内部代码和函数名称同样清晰易读。也可能你重构了改函数,使得其内容和其名称变得同样清晰。果真如此,你应该去掉这个函数,直接使用其中的代码。间接性可能带来帮助,但非必要的间接性总是让人不舒服。
另一种需要使用Inline Method (内联函数)的情况是:你手上有一群不甚合理的函数。你可以将它们都内联到一个大型函数中,再从中提炼出合理的小函数。实施Replace Method with Method Object (以函数对象取代函数)之前这么做,往往可以获得不错的效果。你可以把所要的函数的所有调用对象的函数内容都内联到函数对象中。比起既要移动一个函数,又要移动它所调用的其他所有函数,将整个大型函数作为整体来移动比较简单。
如果别人使用了太多间接层,使得系统中所有函数都似乎只是对另一个函数的简单委托,造成在这些委托动作之间晕头转向,那么就使用 Inline Method (内联函数)。当然,间接层有其价值,但不是所有间接层都有价值。试着使用内联手法,可以找出那些有用的间接层,同时将那些无用的间接层去除。
做法:1、检查函数,确定它不具多态性。如果子类继承了这个函数,就不要将此函数内联,因为子类午饭覆写一个根本不存在的函数。
2、找出这个函数的所有被调用点。
3、将这个函数的所有被调用点都替换为函数本体。
4、编译、测试。
5、删除该函数定义。
注意:Inline Method (内联函数)似乎很简单。但情况往往并非如此。对于递归调用、多返回点、内联至另一个对象中而该对象并无提供访问函数……每种情况都可以写上好几页如果遇到这些情况,那么就不应该使用这个手法。
3 Inline Temp(内联临时变量)
解释:你有一个临时变量,只被一个简单的表达式赋值一次,而它妨碍了其他重构手法。其实就是当你看到一个变量被赋值后只用一次且觉着多余,那么就替换掉变量直接用表达式
如:原函数(看到这种代码不觉着basePrice多余?)
double basePrice = anOrder.basePrice(); return (basePrice > 1000);
用Inline Temp重构后
return (anOrder.basePrice() > 1000);
动机:
你发现某个临时变量被赋予某个函数的返回值,并且这个变量影响到了你用其他方法重构,则替换掉他!
做法:
1 检查临时变量赋值语句,确保等号右边的表达式没有副作用
2 如果这个临时变量未声明为final,则将其声明为final,这样可以确保该变量确实只被赋值了一次(因为final 变量赋值多次编译报错)
3 找到所有临时变量引用点,将他们替换为赋值表达式
4 每次修改完后编译测试
5 修改引用点后删除变量声明和赋值语句
6 编译测试
注意:无
4 Replace Temp with Query(以查询取代临时变量)
解释:你的程序以一个临时变量保存某一个表达式的运算效果。将这个表达式提炼到一个独立函数中。将这个临时变量的所有引用点替换为对新函数的调用。此后,新函数就可以被其他函数调用。
如:原函数
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; } private int BasePrice() { return _quantity* _itemPrice; }
动机:临时变量的问题在于:它们是暂时的,而且只能在所属函数内使用。由于临时变量只是在所属函数内可见,所以它们会驱使你写出更长的函数,因为只有这样你才能访问到需要的临时变量。如果把临时变量替换为一个查询,那么同一个类中的所有函数都可以获得这份信息。这将带给你极大帮助,使你能够为这个类编写更清晰地代码。
Replace Temp with Query (以查询取代临时变量)往往是你运用Extract Method (提炼函数)之前必不可少的一个步骤。局部变量会使代码难以被提炼,所以你应该尽可能把它们替换为查询式。
这个重构手法较为简单的情况是:临时变量只被赋值一次,或者赋值给临时变量的表达式不受其他条件影响。其他情况比较棘手,但也可能发生。你可能需要先运用Split Temporary Variable (分解临时变量)或Separate Query form Modifier (将查询函数和修改函数分离)使情况变得简单一些,然后再替换临时变量。如果你想替换的临时变量是用来收集结果的)例如循环中的累加值),就需要将某些程序逻辑(例如循环)复制到查询函数去。
做法:1、找出只被赋值一次的临时变量。如果某个临时变量被赋值超过一次,考虑使用Split Temporary Variable (分解临时变量)将它们分解成多个变量。
2、将该变量声明为const。
3、编译。这可确保临时变量的确只被赋值一次。
4、将“对该临时变量赋值”之语句的等号右侧部分提炼到一个独立函数中。首先将函数声明为private。日后你可能会发现有更多的类需要使用它。那是放松对它的保护也很容易。确保提炼出来的函数无任何副作用,也就是说该函数并不修改任何对象内容。如果它有副作用,就对它进行Separate Query form Modifier (将查询函数和修改函数分离).
5、编译,测试。
6、在该变量身上实施 Inline Temp (内联临时变量)。
我们常常使用临时变量保存循环中的累加信息。在这种情况下,这个循环都可以被提炼为一个独立函数,这也使原本的函数可以少掉几行扰人的循环逻辑。有时候,你可能会在一个循环中累加好几个值。这种情况下你应该针对每个累加值重复一遍循环,这样就可以将所有临时变量都替换为查询。当然,循环应该很简单,复制这些代码才不会带来危险。
注意: 运用此手法,你可能会担心性能问题。和其他问题一样,我们现在不管它,因为它十有八九根本不会造成任何影响。若是性能真的出了问题,你也可以在优化时期解决它。代码组织良好,你往往能发现更有效的优化方案。如果没用进行重构,好的优化方案就可能与你失之交臂。如果性能实在太糟糕,要把临时变量放回去也很容易。(其实刚开始开发和重构时大可不必过多考虑性能问题,不然会寸步难行,代码结构清楚了性能调优也会很方便,反正我是今天才明白)
5 Introduce Explaining Variable(引入解释性变量)
6 Spilt Temporary Variable (分解临时变量)
7 Remove AssignMents to Parameters(移除对参数的赋值)
8 Replace Method with Method Object(以函数对象取代函数)
9 Substitue Algorithm(替换算法)
解释:想要把某个算法替换成另一个更加清晰的算法,将函数本体替换成另一个算法
原始函数(使用的if-else逻辑)
1 String findPerson(String[] person) 2 { 3 for (int i = 0; i < person.length(); ++i) 4 { 5 if(person[i].equals("Don")) 6 return "Don"; 7 else if (person[i].equals("John")) 8 return "John"; 9 else if (person[i].equals("Kent")) 10 return "Kent"; 11 } 12 return ""; 13 }
替换算法后的代码(替换掉了多个if-else判断,是不是简洁许多)
String findPerson(String[] person) { StringList perList = Arrays.asList(new String[] {"Don" , "John" , "Kent"}); for (int i = 0; int i < person.length(); i++) { if (perList.contains(person[i])) return person[i]; } return ""; }
注:使用这种重构手法之前,尽可能的拆分原函数,只有先分解为了多个小函数,替换算法才容易下手
标签:john 今天 extract 逻辑 测试 没有 ace 算法 anti
原文地址:http://www.cnblogs.com/cs-forget/p/7679118.html