那有什么天生如此,只是我们天天坚持。

本篇文章主要讲解 《重构---改善既有代码的设计》 这本书中的 第六章重新组织函数中 的知识点,

将现有的函数重新分解是 进行大型优雅重构的第一步!

内联函数

问题:一个函数的本体与名称同样清楚易懂

解决:在函数调用点插入函数本体,然后移除该函数

//重构前  
public int getRating(){  
    return (moreThanSixLateDeliveries()) ? 2 : 1;  
}  
  
boolean moreThanSixLateDeliveries(){  
    return _numberOfLateDeliveries > 6;  
}  
//重构后  
public int getRating(){  
    return (_numberOfLateDeliveries > 6) ? 2 : 1;  
} 

动机

重构过程中经常会以简短的函数来表现动作意图,这样就使得代码更清晰易读。
但有时你会遇到某些函数,其内部代码和函数名称同样清晰易读。可能你重构了该函数,使得其内容和其名称变得同样清晰。果真如此,你就应该去掉这个函数,直接使用其中的代码。间接性可能会带来一些帮助,但是没有必要的间接性总是让人感觉不舒服。

还有一种情况是:你手上有一群组织不甚合理的函数。
你可以将它们都内联到一个大型函数中,再从中提炼出组织合理的小型函数。比起既要移动一个函数、又要移动它所调用的其它所有函数,将整个大型函数作为整体来移动会比较的简单。

如果你发现代码中使用了太多的间接层,使得系统中的所有函数都似乎只是对另一个函数的简单委托,造成对被些委托动作弄的晕头转向,这时通常也会使用内联函数。

做法

(1)检查函数,确定其不具有多态性。(如果子类继承了这个函数,就不要将此函数内联,因为子类无法复写一个根本不存在的函数)。
(2)找出这个函数的所有被调用点。
(3)将这个函数的所有被调用点都替换为函数本体。
(4)编译,测试。
(5)一切正常后,删除该函数的定义。

内联函数看起来似乎很简单。但情况往往那并非如此。对于递归调用、内联至另一个对象中而该对象并无提供访问函数......每一种情况都会很复杂。不介绍复杂情形是因为:如果你遇到了这样复杂的情形,那么就不该运用这种重构手法。

内联临时变量

问题:你有一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其它的重构手法

解决:将所有对该变量的引用动作,替换为对它赋值的那个表达式自身。

//重构前  
double basePrice = singleOrder.basePrice();  
return (basePrice > 1000)  
//重构后  
return (singleOrder.basePrice() > 1000)  

动机

内联临时变量多数情况是作为“以查询取代临时变量”的一部分来进行使用的,而真正的动机是出现在“以查询取代临时变量”中。
唯一单独使用内联临时变量的情况是:你发现某个临时变量被赋予某个函数调用的返回值。一般来说,这样的临时变量是不会造成任何危害的,也可以放心地放在那儿。
但是,如果这个临时变量妨碍了其它的重构手法(例如提炼函数),你就应该将其内联化。

做法

(1)检查给临时变量赋值的语句,确保等号右边的表达式没有副作用。
(2)如果这个临时变量并未被声明为final,那就将它声明为final,然后编译。(这可以检查该临时变量是否真的只被赋值一次)
(3)找到该临时变量所有引用点,将它们替换为“为临时变量赋值”的表达式
(4)每次修改后,编译并测试。
(5)修改完后所有引用点后,删除该临时变量的声明和赋值语句。
(6)编译,测试

引入解释性变量

问题:你有一个复杂的表达式

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

//重构前  
if((platform.toUpperCase().indexOf("MAC") > -1) &&  
    (browser.toUpperCase().indexOf("IE") > -1) &&  
    wasInitialized() && resize > 0)  
{  
    //do something  
}  
//重构后  
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;  
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;  
final boolean wasResize = resize > 0;  
  
if(isMacOs && isIEBrowser && wasInitialized() && wasResize){  
    //do something  
}  

动机

在某些情况下,表达式可能非常的复杂以至于难以阅读。这样,临时变量可以帮助你将表达式分解为比较容易管理的形式。

条件逻辑中,引入解释性变量就显得比较有价值:你可以用这项重构将每个子句提炼出来,以一个良好命名的临时变量来解释对应条件子句的意义。另一种可能的情况是,对于那些比较长的算法,可以运用临时变量来解释每一步运算的意义

本文的重构手法是比较常见的手法之一,但是对其的使用又不是那么的多。因为一般情况下,我们都可以使用提炼函数来解释一段代码的意义。毕竟临时变量只有在它所处的那个函数中才有意义,局限性较大,函数则可以在对象的整个生命周期中都有用,并且可被其它对象使用。但是,当局部变量使用提炼函数难以进行时,就可以尝试使用引入解释性变量。

做法

(1)声明一个final型的临时变量,将待分解之复杂表达式中的一部分动作的运算结果赋值给它。
(2)将表达式中的“运算结果”这一部分,替换为上述的临时变量。(如果被替换的这一部分在代码中重复出现,可以每次一个,逐一进行替换)
(3)编译,测试。
(4)重复上述过程,处理其它类似部分。

示例


//重构前  
double price(){  
    // 价格 = basePrice - quantity discount + shipping  
    return _quantity * _itemPrice -  
            Math.max(0, _quantity - 800) * _itemPrice * 0.15 +  
            Math.min(_quantity * _itemPrice * 0.25, 100);  
}  

这段代码还是比较简单,不过现在要让其更加容易理解一些。
首先发现底价(basePrice)等于数量(quantity)乘以单价(item price)。于是可以把这一部分的计算结果放进一个临时变量中,同时将Math.min()函数中参数进行同样替换。

double price(){  
    // 价格 = basePrice - quantity discount + shipping  
    final double basePrice = _quantity * _itemPrice;  
    return basePrice -  
            Math.max(0, _quantity - 800) * _itemPrice * 0.15 +  
            Math.min(basePrice * 0.25, 100);  
}

然后,将批发折扣(quantity discount)的计算提炼出来,并将运算结果赋予临时变量。


double price(){  
    // 价格 = basePrice - quantity discount + shipping  
    final double basePrice = _quantity * _itemPrice;  
    final double quantityDiscount = Math.max(0, _quantity - 800) * _itemPrice * 0.15;  
    return basePrice -quantityDiscount+  
            Math.min(basePrice * 0.25, 100);  
} 

最后,再把搬运费(shipping)计算提炼出来,并将运算结果赋予临时变量。

//重构后  
double price(){  
    // 价格 = basePrice - quantity discount + shipping  
    final double basePrice = _quantity * _itemPrice;  
    final double quantityDiscount = Math.max(0, _quantity - 800) * _itemPrice * 0.15;  
    final double shipping = Math.min(basePrice * 0.25, 100);  
    return basePrice - quantityDiscount + shipping;  
}  

运用提炼函数处理

对于上述代码,通常不以临时变量来解释其动作意图,而是更喜欢使用提炼函数。

//重构前  
double price(){  
    // 价格 = basePrice - quantity discount + shipping  
    return _quantity * _itemPrice -  
            Math.max(0, _quantity - 800) * _itemPrice * 0.15 +  
            Math.min(_quantity * _itemPrice * 0.25, 100);  
}  

现在把底价计算提炼到一个独立的函数中。

double price(){  
    // 价格 = basePrice - quantity discount + shipping  
    return basePrice() -  
            Math.max(0, _quantity - 800) * _itemPrice * 0.15 +  
            Math.min(basePrice() * 0.25, 100);  
}  
  
private double basePrice(){  
    return _quantity * _itemPrice;  
} 

继续进行提炼,每次提炼一个新的函数。最后得到代码如下。

//重构后  
double price(){  
    // 价格 = basePrice - quantity discount + shipping  
    return basePrice() - quantityDiscount() + shipping();  
}  
  
private double basePrice(){  
    return _quantity * _itemPrice;  
}  
  
private double shipping(){  
    return Math.min(basePrice() * 0.25, 100);  
}  
  
private double quantityDiscount(){  
    return Math.max(0, _quantity - 800) * _itemPrice * 0.15;  
}  

分解临时变量

问题:你的程序有某个临时变量被赋值超过一次,它既不是循环变量,也不被用于收集计算结果

解决:针对每次赋值,创造一个独立、对应的临时变量。

//重构前  
double temp = 2 * (_height + _width);  
System.out.println(temp);  
temp = _height + _width;  
System.out.println(temp); 
//重构后  
final double perimeter = 2 * (_height + _width);  
System.out.println(perimeter);  
final double area = _height + _width;  
System.out.println(area);  

动机

在某些情况下,临时变量用于保存一段冗长代码的运算结果,以便稍后使用。
这种临时变量应该只被赋值一次。
如果它被赋值超过一次,就意味着它们在函数中承担了一个以上的责任。
如果临时变量承担多个责任,它就应该被替换(分解)为多个临时变量,使得每一个变量只承担一个责任
同一个临时变量承担两件不同的事情,会让代码阅读者糊涂。

做法

(1)在待分解临时变量的声明及第一次被赋值处,修改其名称。
(2)将新的临时变量声明为final。
(3)以该临时变量的第二次赋值动作为界,修改此前对该临时变量的所有引用点,让它们引用新的临时变量。
(4)在第二次赋值处,重新声明原先那个临时变量。
(5)编译,测试。
(6)逐次重复上述过程。每次都在声明处对临时变量改名,并修改下次赋值之前的引用点。

示例

我们从一个简单计算开始:我们需要计算一个苏格兰布丁运动的距离。在起点处,静止的布丁会受到一个初始力的作用而开始运动。一段时间后,第二个力作用于布丁,让它再次加速。根据牛顿第二定律,计算布丁运动距离:

 牛顿第二定律 
内容:物体的加速度与所受合外力成正比,跟物体的质量成反比。
表达式:F=ma。   
  物理意义:反映物体运动的加速度大小、方向与所受合外力的关系,且这种关系是瞬时的。

double getDistance(int time){  
    double result;  
    double acc = _primaryForce / _mass;  
    int primaryTime = Math.min(time, _delay);  
    result= 0.5 * acc * primaryTime * primaryTime;  
    int secondaryTime = time - _delay;  
    if(secondaryTime > 0){  
        double primaryVel = acc *_delay;  
        acc = (_primaryForce + _secondaryForce) / _mass;  
        result += primaryVel * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime;  
    }  
    return result;  
}  

代码看起来好像有点丑陋。观察例子中的acc变量是如何被赋值两次。
acc变量有两个责任,一是保存第一个力产生的加速度;二是保存两个力共同产生的加速度。这就是需要分解的东西。

首先,在函数开始修改处修改这个临时变量的名称,并将新的临时变量声明为final。然后,把第二次赋值之前对acc变量的所有引用点,全部改用心的临时变量。最后,在第二次赋值处重新声明acc变量。

double getDistance(int time){  
    double result;  
    final double primaryAcc = _primaryForce / _mass;  
    int primaryTime = Math.min(time, _delay);  
    result= 0.5 * primaryAcc * primaryTime * primaryTime;  
    int secondaryTime = time - _delay;  
    if(secondaryTime > 0){  
        double primaryVel = primaryAcc *_delay;  
        double acc = (_primaryForce + _secondaryForce) / _mass;  
        result += primaryVel * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime;  
    }  
    return result;  
}  

新的临时变量指出,它只承担原先acc变量的第一个责任。
将它声明为final,确保它只被赋值一次
然后,在原先acc变量第二次被赋值处重新声明acc。
现在,重新编译并测试,一切都没有问题。
然后继续处理acc临时变量的第二次赋值

double getDistance(int time){  
    double result;  
    final double primaryAcc = _primaryForce / _mass;  
    int primaryTime = Math.min(time, _delay);  
    result= 0.5 * primaryAcc * primaryTime * primaryTime;  
    int secondaryTime = time - _delay;  
    if(secondaryTime > 0){  
        double primaryVel = primaryAcc *_delay;  
        final double secondaryAcc = (_primaryForce + _secondaryForce) / _mass;  
        result += primaryVel * secondaryTime + 0.5 * secondaryAcc * secondaryTime * secondaryTime;  
    }  
    return result;  
}  

以查询取代临时变量 手法进行重构

//“以查询取代临时变量”手法进行重构  
double getDistance(int time){  
    double result= 0.5 * getPrimaryAcc() * getPrimaryTime(time) * getPrimaryTime(time);  
    if(getSecondaryTime(time) > 0){  
        result += getSeconddistance();  
    }  
    return result;  
}  
  
private double getPrimaryAcc(){  
    return _primaryForce / _mass;  
}  
  
private double getSecondaryAcc(){  
    return (_primaryForce + _secondaryForce) / _mass;  
}  
  
private int getPrimaryTime(int time){  
    return Math.min(time, _delay);  
}  
  
private int getSecondaryTime(int time){  
    return time - _delay;  
}  
  
private double getSeconddistance(){  
    return getPrimaryAcc() *_delay * getSecondaryTime(time)   
            + 0.5 * getSecondaryAcc() * getSecondaryTime(time) * getSecondaryTime(time);  
}  

移除对参数的赋值

问题:代码对一个参数进行赋值
解决:以一个临时变量取代该参数的位置。

//重构前  
int dicount(int inputVal, int quantity, int yearToDate){  
    if(inputVal > 50) inputVal-=10;  
} 
//重构后  
int dicount(final int inputVal, int quantity, int yearToDate){  
    int result = inputVal;  
    if(result > 50) result-=10;  
}  

动机

我想你很清楚“对参数赋值”这个说话的意思。
如果把一个名称为fool的对象作为参数传递给某个函数,那么“对参数赋值”意味改变fool,使它引用另一个对象
但是,如果在“被传入对象”身上进行什么操作,那没问题,我们经常会这么做。
这里只针对“fool被改变而指向另一个对象”这种情况来讨论:

void test(Object fool){  
    fool.changedBySomeWay(); //that's ok  
    fool=anotherObject; //trouble will appear  
}  

我们之所不这样做,是因为它降低了代码的清晰度,而且混用了按值传递和按引用传递这两种参数传递方式。

JAVA只采用按值进行传递

在按值传递的情况下,对参数的任何修改,都不会对调用端造成任何影响
如果你只以参数表示“被传递进来的东西”,那么代码会清晰的多,因为这种用法在所有语言中都表现出相同的语义。

在JAVA中,一般不要对参数赋值:如果你看到手上的代码已经这么做了,就应该使用本文的方法。

做法

(1)建立一个临时变量,把待处理的参数值赋赋予它。
(2)以“对参数的赋值”为界,将其后所有对此参数的引用点,全部替换为“对此临时变量的引用”。
(3)修改赋值语句,使其改为对新建之临时变量赋值。
(4)编译,测试。
(如果代码的语义是按照引用传递的,需在调用端检查调用后是否还使用了这个参数。也要检查有多少个按引用传递的参数被赋值后又被使用。应该尽量以return方式返回一个值。如果返回值有多个,可考虑将需返回的一大堆数据变为对象,或者为每个返回值设定一个独立的函数)

示例

int dicount(int inputVal, int quantity, int yearToDate){  
    if(inputVal > 50) inputVal-=5;  
    if(quantity > 100) quantity-=10;  
    if(yearToDate > 1000) yearToDate-=100;  
    return inputVal;  
}  

以临时变量取代对参数的赋值动作,得到下列代码:

int dicount(int inputVal, int quantity, int yearToDate){  
    int result  = inputVal;  
    if(result > 50) result-=5;  
    if(quantity > 100) quantity-=10;  
    if(yearToDate > 1000) yearToDate-=100;  
    return result;  
}  

可以为参数加上final关键词,强制其遵循“不对参数赋值”这一惯例:

int dicount(final int inputVal, final int quantity, final int yearToDate){  
    int result  = inputVal;  
    if(result > 50) result-=5;  
    if(quantity > 100) quantity-=10;  
    if(yearToDate > 1000) yearToDate-=100;  
    return result;  
}

JAVA的按值传递

我们应该都知道,JAVA使用按值传递的函数调用方式,这常常也会使大家迷惑。在所有地点,JAVA都会遵循严格按值传递:

//JAVA按值的传递  
class Params{  
    public static void main(String[] args) {  
        int x = 10;  
        triple(x);  
        System.err.println("x after triple:" + x);  
    }  
      
    private static void triple(int arg){  
        arg = arg * 3;  
        System.err.println("arg in triple:" +arg );  
    }  
}  
//输出  
//arg in triple:30   
//x after triple:10  

上面代码是使用基本数据类型进参数传递,还不至于让人糊涂。但如果参数中传递的是对象,就可能把人弄糊涂。如果在程序中以Date对象表示日期,下列程序所示:

//以对象为参数  
class Params{  
    public static void main(String[] args) {  
        Date d1 = new Date(2015,1,1);  
        nextDateUpdate(d1);  
        System.err.println("d1 after nextday:" + d1);  
          
        Date d2 = new Date(2015,1,1);  
        nextDateReplace(d2);  
        System.err.println("d2 after nextday:" + d2);//61380864000000  
          
    }  
  
    private static void nextDateUpdate(Date d) {  
        d.setDate(d.getDate()+1);  
        System.err.println("arg in nextday d1 : "+d);  
    }  
  
    private static void nextDateReplace(Date d) {  
        d = new Date(d.getYear(),d.getMonth(),d.getDate()+1);  
        d=null;  
        System.err.println("arg in nextday d2: "+d);  
    }  
}  
  
//输出  
/* 
 arg in nextday d1 : Tue Feb 02 00:00:00 CST 3915 
 d1 after nextday:   Tue Feb 02 00:00:00 CST 3915 
 arg in nextday d2:  Tue Feb 02 00:00:00 CST 3915 
 d2 after nextday:   Mon Feb 01 00:00:00 CST 3915 
 */  

从本质上说,对象的引用是按值传递的。因为可以修改参数对象的内部状态,但对参数对象重新赋值是没有意义的。

以函数对象取代函数

问题:你有一个大型函数,其中对局部变量的使用使你无法采用“提炼函数”这种重构手法

解决:将这个函数放进一个单独对象中,这样,局部变量就成了对象的字段,然后就可以在同一个对象中将这个大型函数分解为多个小型函数。

//重构前  
class Order....  
    double price(){  
        double basePrice;  
        double secondaryPrice;  
        double thirdaryPrice;  
        //compute()  
        ......  
}
//重构后  
class Order...  
    double price(){  
        return new PriceCalculator(this).compute();  
}  
  
class PriceCalculator{  
    double basePrice;  
    double secondaryPrice;  
    double thirdaryPrice;  
      
    double compute(){  
        //...  
    }  
}

动机

在前面的文章中一直在强调小型函数的优美动人。
只要将相对独立的代码从大函数提炼出来,就可以大大提高代码的可读性。

但是局部变量的存在会增加函数分解的难度
如果一个函数中的局部变量泛滥成灾,那么想分解这个函数是非常困难的。
“以查询替换临时变量”手法可以帮助减轻负担,但有时候还是会发现根本无法拆解一个需要拆解的函数。

这种情况就应该考虑使用函数对象来解决。本文的重构方法会将所有的局部变量都变成函数对象的字段。然后就可以使用“提炼函数”创造新的函数,从而将原来的大型函数拆解变小。

做法

(1)建立一个新的类,根据待处理函数的用途为其命名。
(2)在新类中建立一个final字段,用以保存原先大型函数所在对象。针对原函数的每个临时变量和每个参数,在新类中建立一个对应的字段保留之。
(3)在新类中建立一个构造函数,接收原对象以原函数的所有参数作为其参数。
(4)在新类中建立一个compute()函数。
(5)将原函数的代码复制到compute()函数中。如果需要调用源对象的任何函数,通过原对象字段调用。
(6)编译,测试。
由于所有局部变量现在都成了字段,所以你可以任意分解这个大型函数,不必传递任何参数。

示例

class Account{  
        int gamm(int value, int quantity, int year2Date){  
            int importValue1 = (value * quantity) + delta();  
            int importValue2 = (value * year2Date) + 200;  
            if(year2Date - importValue1 >200)  
                importValue2-=50;  
            int importValue3 = importValue2 * 8;  
            //......  
              
            return importValue3 - 2 * importValue1;  
        }  
        //.....  
    }  

为了把这个函数变为函数对象,首先需要声明一个新类。在新类中提供final对象保存原对象,对于函数的每个参数和每个临时变量,也以一个字段逐一保留。

class Gamm{  
    private final Account _account;  
    private int value;  
    private int quantity;  
    private int year2Date;  
    private int importValue1;  
    private int importValue2;  
    private int importValue3;  

接下来,加入一个构造函数。

Gamm(Account source, int inputVal, int quantity, int year2Date){  
    this._account = source;  
    this.value = inputVal;  
    this.quantity = quantity;  
    this.year2Date = year2Date;  
}  

现在可以把原本函数搬到compute()中了。函数中任何调用Accout类的地方,都必改用_account字段。

int compute(){  
    importValue1 = (value * quantity) + _account.delta();  
    importValue2 = (value * year2Date) + 200;  
    if(year2Date - importValue1 >200)  
        importValue2-=50;  
    importValue3 = importValue2 * 8;  
    //......  
      
    return importValue3 - 2 * importValue1;  
}  

然后,修改旧函数,让它将工作委托给刚完成的这个函数对象。

int gamm(int value, int quantity, int year2Date){  
    return new Gamm(this,value,quantity,year2Date).compute();  
} 

以上就是本文重构方法的基本原则。其所带来的好处是:现在可以轻松地对compute()函数采取“提炼函数”,而不必担心参数传递的问题。

//运用提炼函数 不必担心参数问题  
int compute(){  
    importValue1 = (value * quantity) + _account.delta();  
    importValue2 = (value * year2Date) + 200;  
    importantThing();  
    importValue3 = importValue2 * 8;  
    //......  
      
    return importValue3 - 2 * importValue1;  
}  
  
private void importantThing() {  
    if(year2Date - importValue1 >200)  
        importValue2-=50;  
}  

主要介绍了重构手法——以函数对象取代函数。
我们都不喜欢临时变量巨多的方法,那只会让我们迷惑。
对于局部变量很多的函数,有必要运用本文的重构方法进行处理,将其转化为函数对象,那样就把临时变量转为函数对象的字段,继而可以进行其它重构方法。


石志远
572 声望62 粉丝

引用和评论

0 条评论