重构笔记——提炼函数

来源:互联网 发布:java好就业么 编辑:程序博客网 时间:2024/05/01 11:52

本文是在学习中的总结,欢迎转载但请注明出处:http://blog.csdn.net/pistolove/article/details/42214393


        在前面的三篇文章中介绍了重构入门篇、代码的坏味道(上)、代码的坏味道(下)。本文将正式开启重构之旅。从本文开始在后续的文章中会陆续介绍92种重构手法,每一种重构手法都会对应于一种代码坏味道。在介绍的过程中,每一种重构手法也将对应一篇文章,可能有的重构手法比较简短,但是为了便于整理还是单独将其列为一篇。(PS:不管怎样,我都会坚持把这些重构手法分享给大家,我想总会有人从中受益,同时也希望对你有所帮助)

        下面让我们来学习“提炼函数”这种重构手法吧。


开门见山

        发现:你有一段代码可以被组织在一起并独立出来。

        解决:将这段代码放进一个独立函数中,并让函数名称解释该函数的用途。


重构前:

public void PrintOwing(double amount) {printBanner();// printdetailSystem.err.println("name: " + _name);System.err.println("amount: " + amount);}

重构后:

public void PrintOwing(double amount) {printBanner();printDetails(amount);}private void printDetails(double amount) {System.err.println("name: " + _name);System.err.println("amount: " + amount);}

动机

        提炼函数是最常用的重构手法之一。当我们看见一个过长的函数或者一段需要注释才能让人理解用途的代码,一般情况下,我们都应该对其进行重构将这段代码放进一个独立的函数中。

        我们都喜欢简短而命名良好的函数。原因主要有三个方面:一是如果每个函数的粒度都很小,那么函数被复用的机会就更大;二是这会让高层函数读起来就像一系列注释一样,容易理解;三是如果函数都是细粒度,那么函数的复写也会更加容易。

        可能你会问,一个函数多长才算是合适呢?对于重构来说,长度不是问题,关键在于函数名称和函数本题之间的语义距离。如果提炼可以强化代码的清晰度,那么就值得去做,就算函数名称比提炼出来的代码还长也无所谓。


做法

(1)创建一个新的函数,根据函数的意图来命名。
(2)将提炼出的代码从源函数复制到新建的目标函数中。
(3)仔细检查提炼出的代码,看看其中时候引用了“作用域限于原函数”的变量(包括局部变量和源函数参数)。
(4)检查是否有“仅用于被提炼代码段”的临时变量。如果有,在目标函数中同样将其声明为临时变量。
(5)检查被提炼代码段,看看是否有任何的局部变量的值被它改变。如果一个临时变量值被修改了,看看是否能够将被提炼代码段处理为一个查询,并将结果赋值给相关变量。如果很难做或者被修改的变量有多个,就需要使用“分解临时变量”或以查询取代临时变量“进行处理,然后再尝试提炼。
(6)将被提炼代码段中需要读取的局部变量当作参数传递给目标函数。
(7)处理完所有局部变量后,进行编译。
(8)编程成功后,在源函数中,将被提炼出的代码段替换为对目标函数的调用。
(9)测试。


示例

A:无局部变量

      在最简单的情况下,提炼函数易如反掌:
// 重构前public void PrintOwing() {Enumeration e = _orders.elements();double outstanding = 0.0;// print bannerSystem.err.println("************************************");System.err.println("****************OK******************");System.err.println("************************************");// caculate outstandingwhile (e.hasMoreElements()) {Order each = (Order) e.nextElement();outstanding += each.getAmout();}// print detailsSystem.err.println("name: " + _name);System.err.println("amout: " + outstanding);}
// 重构后public void PrintOwing() {Enumeration e = _orders.elements();double outstanding = 0.0;printBanner();// caculate outstandingwhile (e.hasMoreElements()) {Order each = (Order) e.nextElement();outstanding += each.getAmout();}// print detailsSystem.err.println("name: " + _name);System.err.println("amout: " + outstanding);}private void printBanner() {// print bannerSystem.err.println("************************************");System.err.println("****************OK******************");System.err.println("************************************");}

B:有局部变量

      这个重构手法的困难点在于有局部变量的时候,包括传进源函数的参数和源函数所声明的临时变量。因为局部变量的作用域仅仅限于源函数,所以必须花费额外功夫去处理这些变量。在一些情况下,局部变量使得无法进行这项重构。
      局部变量比较简单的情形是被提炼的代码段只是读取这些变量的值,而并不修改它们。这种情况可以将它们当作参数传递给目标函数。
// 有局部变量public void PrintOwing() {Enumeration e = _orders.elements();double outstanding = 0.0;printBanner();// caculate outstandingwhile (e.hasMoreElements()) {Order each = (Order) e.nextElement();outstanding += each.getAmout();}printDetails(outstanding);}private void printDetails(double outstanding) {System.err.println("name: " + _name);System.err.println("amout: " + outstanding);}

C:对局部变量再赋值

      如果被提炼代码段对局部变量再赋值,问题就变得复杂了。这里只讨论临时变量的情形。
          情形1:这个变量只在被提炼代码段中使用。
          这种情形直接将该变量的的声明移动到被提炼的代码中。
// 提炼计算函数public void PrintOwing() {printBanner();double outstanding = getOutstanding();printDetails(outstanding);}private double getOutstanding() {Enumeration e = _orders.elements();double outstanding = 0.0;while (e.hasMoreElements()) {Order each = (Order) e.nextElement();outstanding += each.getAmout();}return outstanding;}
          情形2:被提炼代码之外的代码也使用这个变量。
          这里有分为两种情况:
              (1)如果这个变量在被提炼代码段之后未再被使用,则只需要在目标函数中修改它即可。
              在下面的代码中,Enumeration变量e只在被提炼的代码段中使用,所以可以它整个搬到新的函数中。
private double getOutstanding() {Enumeration e = _orders.elements();double result = 0.0;while (e.hasMoreElements()) {Order each = (Order) e.nextElement();result+= each.getAmout();}return result;}
              (2)如果被提炼的代码段之后还使用了这个变量,则需要让目标函数返回该变量改变后的值。
              在下面的代码中,double变量outstanding在被提炼代码段内外都被用到,所以必须让提炼出来的新函数返回它。
// 有参数 重构前public void PrintOwing(double previousAmount) {Enumeration e = _orders.elements();double outstanding = previousAmount * 1.5;printBanner();// caculate outstandingwhile (e.hasMoreElements()) {Order each = (Order) e.nextElement();result += each.getAmout();}printDetails(outstanding);}
// 重构后public void PrintOwing(double previousAmount) {double outstanding = previousAmount * 1.5;printBanner();outstanding = getOutstanding(outstanding);printDetails(outstanding);}private double getOutstanding(double initialValue) {double result = initialValue;while (e.hasMoreElements()) {Order each = (Order) e.nextElement();result += each.getAmout();}return result;}
          编译并测试后,再对代码进行整理如下:
// 最后的调整public void PrintOwing(double previousAmount) {printBanner();outstanding = getOutstanding(previousAmount * 1.5);printDetails(outstanding);}
         本文主要介绍了重构手法——提炼函数,针对不同的情况分别作了简单的介绍。本文举得例子都比较简单,对于刚学的人比较容易接受。但是,如果临时变量太多,有时会使得提炼工作举步维艰,这时我们就需要考虑结合其它的重构手法来进行处理。待学习了若干重构手法后,我想如何运用你肯定自然明了。总之,通过对本文的学习,对你多少会有一些帮助,比如对于一段代码只做了一件事情且不受其它代码的影响,那么则可以使用本文的重构手法将其提炼为一个独立的函数等等。
          最后,希望本文对你有所帮助。有问题可以留言,谢谢。(PS:下一篇将介绍重构笔记——内联函数)



重构笔记之前文章如下:

       重构笔记——入门篇

       重构笔记——代码的坏味道(上)

       重构笔记——代码的坏味道(下)

       重构笔记——构筑测试体


8 0