重构之--重新组织函数的几种方法

Posted 沪深狙击手

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重构之--重新组织函数的几种方法相关的知识,希望对你有一定的参考价值。

本篇目录

1 Extract Method(提炼函数)

2 InLine Method(内联函数)

3 Inline Temp(内联临时变量)

4 Replace Temp with Query(以查询取代临时变量)

5 Introduce Explaining Variable(引入解释性变量)

6 Spilt Temporary Variable (分解临时变量)

7 Remove AssignMents to Parameters(移除对参数的赋值)

8 Replace Method with Method Object(以函数对象取代函数)

9 Substitue Algorithm(替换算法)

 

重构手法中,很大一部分都是在对函数进行整理,很多问题也都来自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(引入解释性变量)

解释:你有一个复杂的表达式。将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。

如:原函数

if (Platform.ToUpperCass().indexOf("MAC") > -1 && (Browser.ToUpperCass().indexOf("Ie") > -1) && WasInitalized() )

{

                //do something

}

重构后函数

const bool imMacOs = Platform.ToUpperCass().indexOf("MAC") > -1;

const bool isIeBrowser = Browser.ToUpperCass().indexOf("Ie") > -1;

const bool wasInitalized = WasInitalized();

if (imMacOs && isIeBrowser && wasInitalized)

{

         //do something

}

动机:表达式有可能非常复杂而难以阅读。这种情况下,临时变量可以帮助你将表达式分解为比较容易管理的形式。

在条件逻辑中,Introduce Explaining Variable (引入解释性变量)是一个很常见的手法,但是最好尽量使用 Extract Method (提炼函数)来解释一段代码的意义。毕竟临时变量只在他所处的那个函数才有意义,局限性较大。函数则可以在对象的这个生命中都有用,并且可被其他对象使用。但有时候,当局部变量使Extract Method (提炼函数)难以进行时,就可以使用Introduce Explaining Variable (引入解释性变量).

做法:1、声明const临时变量,将待分解的复杂表达式中的一部分动作的运算结果赋值给他。

           2、将表达式中的“运算结果”这一部分,替换为上述临时变量。如果被替换的这一部分在代码中重复出现,你可以一次一个,逐次替换。

           3、编译、测试

           4、重复上述过程,处理表达式的其他部分。

注意:引入解释性变量是通常很常见,但最好采用Extract Method方法

6 Spilt Temporary Variable (分解临时变量)

定义:你有一个复杂的表达式。将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。

如:原函数

double temp = 2 + (_height + _width);

Console.WriteLine(temp);

temp = _height * _width;

Console.WriteLine(temp);

重构后(观察发现temp被赋值两次)

const double perimeter = 2 + (_height + _width);

Console.WriteLine(perimeter);

const double area = _height * _width;

Console.WriteLine(area);    

动机:临时变量有各种不同用途,其中某些用途会很自然的导致临时变量被多次赋值。“循环变量”和“结果收集变量”就是典型的例子:循环变量会随循环的每次运行而改变;   结果收集变量负责将“通过这个函数的运算”而构成的某个值收集起来。

           除了这2种情况,还有很多临时变量保存一段冗长代码的运算结果,以便稍后使用。这种临时变量应该只被赋值一次。如果它们被赋值超过一次,就意味着它们在函数中承担了一个以上的职责。如果临时变量承担多个责任,它就应该被替换为多个临时变量,每个变量只承担一个责任。同一个临时变量承担2件不同的事情,会令代码阅读者糊涂。

做法:1、在待分解临时变量的声明及其第一次被赋值处,修改其名称。如果是结果收集变量就不要分解它。

          2、将新的临时变量声明为const。

          3、以该临时变量的第二次赋值动作为边界,修改此前对该临时变量的所有引用点,让它们引用新的临时变量。

          4、在第二次赋值处,重新声明原先的那个临时变量。

          5、编译、测试。

          6、逐次重复上述过程。每次都在声明处对临时变量改名,并修改下次赋值之前的引用点。

注意:在函数内尽量不给一个变量赋值多次,可以把它当做一个尽可能遵守的规范

7 Remove AssignMents to Parameters(移除对参数的赋值)

定义:你的代码对一个参数进行赋值动作,以一个临时的变量取代该参数的位置

举重构例子前先简单说一下java的值传递

例子一(基本类型的值传递)

public static void triple(int arg) {
         arg = arg - 3;
         System.out.println("arg in triple:" + arg);
     }
 
     public static void main(String[] args) {
         // TODO Auto-generated method stub
         int x = 5;
         triple(x);
         System.out.println("x after triple:" + x);
     }

输出结果(观察发现triple函数对参数进行了赋值,其实在java中尽量不这么做,当然有些语言是出参的,不在这个范围内)

arg in triple:2
x after triple:5

 

例二(值传递传递的是对象,不改变引用,只是改变了对象属性值)

public static String printWithDate(Calendar arg) {
        return arg.get(Calendar.YEAR) + "-" + (arg.get(Calendar.MONTH) + 1) + "-"
                + arg.get(Calendar.DAY_OF_MONTH);
    }

    public static void nextDateUpdate(Calendar arg) {
        arg.add(Calendar.DAY_OF_MONTH, 1);
        System.out.println("arg in nextDay:" + printWithDate(arg));
    }
    
    public static void nextDateReplace(Calendar arg) {
        arg = Calendar.getInstance();
        
        arg.add(Calendar.DAY_OF_MONTH, 1);
        System.out.println("arg in nextDay:" + printWithDate(arg));
    }

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Calendar c1 = Calendar.getInstance();
        nextDateUpdate(c1);
        System.out.println("c1 after nextDay:" + printWithDate(c1));
        
        Calendar c2 = Calendar.getInstance();
        nextDateReplace(c2);
        System.out.println("c2 after nextDay:" + printWithDate(c2));
    }

输出

arg in nextDay:2012-7-28
c1 after nextDay:2012-7-28
arg in nextDay:2012-7-28
c2 after nextDay:2012-7-27

重构举例

原函数

private int discount(int inputVal, int quantity, int yearToDate) {
        if (inputVal > 50) {
            inputVal -= 2;
        }

        if (quantity > 100) {
            inputVal -= 1;
        }
        
        if(yearToDate > 10000) {
            inputVal -= 4;
        }
        
        return inputVal;
    }

重构后

private int discount(int inputVal, int quantity, int yearToDate) {
        int result = inputVal;
        
        if (inputVal > 50) {
            result -= 2;
        }

        if (quantity > 100) {
            result -= 1;
        }
        
        if(yearToDate > 10000) {
            result -= 4;
        }
        
        return result;
    }

动机:
首先,我要确定大家都清楚"对参数赋值"这个说法的意思,如果你把一个名为foo的对象作为参数传给某个函数,那么"对参数赋值"意味改变foo,使它引用( 指向)另一个对象.如果你在被入对象(参数)身上进行什么操作,那不叫对参数赋值,没有什么问题,我也总是这样干.
例如:
void Change(Ojbect foo){
   foo.modifySomeWay();   //  that\'s  ok
   foo=  anotherOjbect;  //这种写法,就是对参数赋值,最好不要这么写.要移去.
}

做法:
1 建立一个临时的变量,将要处理的参数赋值给它。 
2 以对参数赋值的操作为界,将其后所有的对此参数的引用点,全部替换为对临时变量的引用。 
3 修改赋值语句,使其改为对新建之临时变量赋值。 
4 编译,测试。 
注意:如果代码的语义是传址,请在调用端检查调用后是否还使用了这个参数。也要检查有多少个传址参数被赋值后又被使用。请尽量用return方式返回一个值。如果返回的值不止一个,看看能不能使用对象,或者干脆为每一个返回值设计对象的函数。

8 Replace Method with Method Object(以函数对象取代函数)

解释:当一个函数中有大量临时变量或者参数影响我们Extract Method,则可以采用以函数对象取代函数的方法进行重构

原始函数(观察代码发现三个参数在函数中都进行计算,如果重构新函数的话就得把这几个参数继续传递,因此可以采取以函数对象取代函数的方法进行重构)

class Account...
    int gamma(int inputVal, int quantity, int yearToDate) {
       int importantValue1 = (inputVal * quantity) + delta();
       int importantValue2 = (inputVal * yearToDate) + 100;
       if((yearToDate - importantValue1) > 100)
          importantValue2 -= 20;
       int importantValue3 = importantValue2 * 7;
       //   and so on.
       return importantValue3 -2 * importantValue1;
    }

重构后(需要新建一个class来存储原函数中对象和所有变量)

class Gamma{
    private final Account _account;
    private int inputVal;
    private int quantity;
    private int yearToDate;
    private int importantValue1;
    private int importantValue2;
    private int importantValue3;
//加入一个构造函数:
Gamma (Account source, int inputValArg, int quantityArg, int yearToDateArg) {
    _account = source;
    inputVal = inputValArg;
    quantity = quantityArg;
    yearToDate = yearToDateArg;
}
 
//现在可以把原来的函数搬到compute(),发现参数已经不需要了
int compute() {
       int importantValue1 = (inputVal * quantity) + _account.delta();
       int importantValue2 = (inputVal * yearToDate) + 100;
       if((yearToDate - importantValue1) > 100)
          importantValue2 -= 20;
       int importantValue3 = importantValue2 * 7;
       return importantValue3 -2 * importantValue1;
 }

}

旧函数变为

int gamma(int inputVal, int quantity, int yearToDate) {
    return new Gamma(this, inputVal, quantity, yearToDate).compute();
}

你现在可以随心所欲的对compute()函数进行函数提取了

 动机:局部变量的存在会增加函数分解的难度。如果一个函数之中局部变量泛滥,那么想分解这个函数是非常困难的。Replace Temp with Query (以查询取代临时变量)可以帮助你减轻这一负担,但有时候你会发现根本无法拆解一个需要拆解的函数。这种情况下,应该使用函数对象。

 做法:1、建立一个新类,根据待处理函数的用途,为这个类命名。

            2、在新类中建立一个const字段,用以保存原来大型函数所在的对象。我们将这个字段称为“源对象”。同时,针对原函数的每个临时变量和每个参数,在新类中建立一个对应的字段保存之。

            3、在新类中建立一个构造函数,接收源对象及原函数的所有参数。

            4、在新类中建立一个compute()函数。

            5、将原函数的代码复制到compute()函数中。如果需要调用源对象的任何函数,请通过源对象字段调用。

          6、编译。

            7、将旧函数的函数本体替换为这样一条语句“创建上述新类的一个新对象,而后调用其中的compute()函数”。

注意:由于所有局部变量现在都成了字段,所以你可以任意分解这个大型函数,不必传递任何参数。

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 "";
}

注:使用这种重构手法之前,尽可能的拆分原函数,只有先分解为了多个小函数,替换算法才容易下手

 

以上是关于重构之--重新组织函数的几种方法的主要内容,如果未能解决你的问题,请参考以下文章

重构改善既有代码设计--重构手法 之重新组织你的函数总结

重构手法之重新组织数据

重构手法之重新组织函数

重构手法之重新组织数据

重构改善既有代码的设计--第6章--重新组织函数

PHP 杂谈《重构-改善既有代码的设计》之一 重新组织你的函数