12
2014
06

JavaScript代码重构系列-重新组织你的函数

JavaScript代码重构系列中,最重要的要算這節了: 重新组织你的函数

  1. 提炼函数

  2. 将函数内联化

  3. 用查询取代临时变量

  4. 以临时变量取代高消耗的查询

  5. 将临时变量内联化

  6. 引入解释性变量

  7. 剖解临时变量

  8. 移除对参数的赋值动作

  9. 以函数对象取代函数

  10. 替换你的算法



提炼函数

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

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

當你有一個很長的函數,這也是一個非常直接的重構信號,但真正能讓你用【提煉函數】方法重構代碼時候,必須發現有一段代碼可以被組織在一起,也就是說這段代碼能夠看作一個獨立的工作模塊。

/*
 * 原函數
 */
composingMethodsDemo.printOwing = function (order) {
    //print header
    document.writeln('===================');
    document.writeln('== print amount ===');
    document.writeln('===================');
    var _order = order,
        outstanding = 0;
    for (var i = 0, len = _order.records.length; i < len; i++) {
        outstanding = outstanding + _order.records[i].expenditure;
    }
    //print detail
    document.writeln('name: ' + _order.name);
    document.writeln('amount: ' + outstanding);
};
/*
 * 重構後函數
 */
composingMethodsDemo.newPrintOwing = function (order) {
    var outstanding = this.getOutstanding(order);
    this.extPrintHeader();
    this.extPrintDetail(order.name, outstanding);
};
// 无局部变量,直接提出
composingMethodsDemo.extPrintHeader = function () {
    document.writeln('===================');
    document.writeln('== print amount ===');
    document.writeln('===================');
};
// 有局部变量, 以參數傳遞形式輸入
composingMethodsDemo.extPrintDetail = function (name, outstanding) {
    document.writeln('name: ' + name);
    document.writeln('amount: ' + outstanding);
};
// 对局部变量再赋值
composingMethodsDemo.getOutstanding = function (order) {
    var outstanding = 0;
    for (var i = 0, len = order.records.length; i < len; i++) {
        outstanding = outstanding + order.records[i].expenditure;
    }
    return outstanding;
};

将函数内联化

信號:一个函数,它本体应该与其名称同样清楚易懂。

操作:在函数调用点插入函数本体,然后移除该函数。

這條重構方法跟【提煉函數】是相對立的,如果你發現你【提煉函數】的函數它的內容跟它名字一樣清晰易懂,那麼還是把它內聯回去,讓它看上去更直接。

也有可能【提煉函數】重構完後,長函數(主函數)中添加了一些代碼,這時候被提取出來的函數寫到長函數中更直接,那我們也需要先內聯回去,然後再看它和其他代碼一起組織成新函數是否能再利用【提煉函數】。

/*
 * 原函數
 */
composingMethodsDemo.getRating = function (numberOfLateDeliveries) {
    return (this.moreThanFiveLateDeliveries(numberOfLateDeliveries)) ? 2 : 1;
};
composingMethodsDemo.moreThanFiveLateDeliveries = function (numberOfLateDeliveries) {
    return numberOfLateDeliveries > 5;
};
/*
 * 重構後
 */
composingMethodsDemo.getRating = function (numberOfLateDeliveries) {
    return (numberOfLateDeliveries > 5) ? 2 : 1;
};

用查询取代临时变量

信號:你的程序以一个临时变量保存某一表达式的运算结果。

操作:将这个表达式提炼到一个独立函数中。将这个临时变量的所有被引用点替换为对新函数的调用。新函数可被其他函数使用。

臨時變量一般在函數內部實用,這樣就促使你不得不讓你的函數變得更長,這樣才能使你的代碼訪問到你的臨時變量。這時候,如果將臨時變量換成一個查詢式,那麼代碼就更清晰,更簡潔了,但這個需要將高消耗的查詢除外。

// 用查询取代临时变量
var composingMethodsDemo = {
    _quantity: 30,
    _itemPrice: 12
}
/*
 * 原函數
 */
composingMethodsDemo.getTotalPrice = function () {
    var basePrice = this._quantity * this._itemPrice;
    if (basePrice > 1000) {
        return basePrice * 0.95;
    } else {
        return basePrice * 0.98;
    }
};
/*
 * 重構後
 */
composingMethodsDemo.getTotalPrice = function () {
    if (this.getBasePrice() > 1000) {
        return this.getBasePrice() * 0.95;
    } else {
        return this.getBasePrice() * 0.98;
    }
};
composingMethodsDemo.getBasePrice = function () {
    return this._quantity * this._itemPrice;
};

用临时变量取代高消耗的查询

信號:多次調用的查詢式是一個高消耗的函數

操作:用臨時變量儲存該查詢,將該查詢式調用處用臨時變量代替

該方法與【用查询取代临时变量】是對立的,但出發點不同,本方法出發點最重要的為了提升代碼性能,一些高消耗的方法如果在函數體中多次調用,我們需要用臨時變量緩存起來。最常見的是DOM查詢式,高循環運算的函數查詢式。

// 用临时变量取代高消耗的查询
composingMethodsDemo.getTotalPrice = function () {
    if (this.getBasePrice() > 1000) {
        return this.getBasePrice() * 0.95;
    } else {
        return this.getBasePrice() * 0.98;
    }
};
composingMethodsDemo.getBasePrice = function () {
    return parseFloat(document.getElementById('quantity').value) * parseFloat(document.getElementById('itemPrice').value);
};
/*
 * 重構後
 */
composingMethodsDemo.getTotalPrice = function () {
    var basePrice = this.getBasePrice();
    if (basePrice > 1000) {
        return basePrice * 0.95;
    } else {
        return basePrice * 0.98;
    }
};
composingMethodsDemo.getBasePrice = function () {
    return parseFloat(document.getElementById('quantity').value) * parseFloat(document.getElementById('itemPrice').value);
};

将临时变量内联化

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

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

本方法多半是作为【用查询取代临时变量】的一部分来使用,所以真正的动机出现在后者那儿。惟一单独使用本方法的情况是:你发现某个临时变量被赋予某个函数调用的返回值。一般来说,这样的临时变量不会有任何危害,你可以放心地把它留在那儿。但如果这个临时变量妨碍了其他的重构方法(例如【提取函數】),你就应该将它內聯化。

// 将临时变量内联化
var composingMethodsDemo = {
    _quantity: 30,
    _itemPrice: 12
};
/*
 * 原函數
 */
composingMethodsDemo.isOverMaxPrice = function () {
    var basePrice = this.getBasePrice();
    return basePrice > 1000;
};
composingMethodsDemo.getBasePrice = function () {
    return this._quantity * this._itemPrice;
};
/*
 * 重構後
 */
composingMethodsDemo.isOverMaxPrice = function () {
    return this.getBasePrice() > 1000;
};
composingMethodsDemo.getBasePrice = function () {
    return this._quantity * this._itemPrice;
};

引入解释性变量

信號:你有一个复杂的表达式。

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

【引入解釋性變量】能使方法體看上去更加易懂。特別是當表達式特別複雜的情況下,更能體現該方法的優勢。該方法非常有用,但很多時候我們都實用【提取函數】的方法來重構,畢竟臨時變量只對當前函數體有效,所以只有當局部變量令【提取函數】難以進行,我們才實用【引入解釋性變量】方法來處理重構。

// 引入解释性变量
/*
 * 原函數
 */
composingMethodsDemo.getMacIEVersion = function (platform, browser) {
    if ((platform.toUpperCase().indexOf("MAC") > -1) &&
        (browser.toUpperCase().indexOf("IE") > -1)) {
        return browser.version;
    }
    return null;
};
/*
 * 重構後
 */
composingMethodsDemo.getMacIEVersion = function (platform, browser) {
    var isMac = platform.toUpperCase().indexOf("MAC") > -1,
        isIE = browser.toUpperCase().indexOf("IE") > -1;
    if (isMac && isIE) {
        return browser.version;
    }
    return null;
};

剖解临时变量

信號:你的程序有某个临时变量被赋值超过一次,它既不是循环变量,也不是一个集用临时变量(collecting temporary variable)。

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

除了循環變量和集用臨時變量這兩種臨時變量被多次賦值的情況,其他被多次賦值的臨時變量都在敲醒我們重構的警鐘。每個臨時變量應該只承擔一個單獨的責任,例如【用临时变量取代高消耗的查询】中生成的臨時變量,如果賦值多次,就應該重構以多個臨時代替。

// 剖解临时变量
composingMethodsDemo.getActivedUsers = function (users) {
    var names = [], // 集用临时变量
        count = users.length;
    for (var i = 0; i < count; i++) { // i就是循环变量
        if (users[i].actived) {
            names.push(users[i].name);
        }
    }
    return names;
};
/*
 * 原函數
 */
composingMethodsDemo.printRectangleInfo = function (height, width) {
    var temp = 2 * (height + width);
    document.writeln(temp);
    temp = height * width;
    document.writeln(temp);
};
/*
 * 重構後
 */
composingMethodsDemo.printRectangleInfo = function (height, width) {
    var perimeter = 2 * (height + width),
        area = height * width;
    document.writeln(perimeter);
    document.writeln(area);
};

移除对参数的赋值动作

信號:你的代码对一个参数进行赋值动作。

操作:以一个临时变量取代该参数的位置,以函數返回值在原函數改變對象。

在JavaScript中非常特別的一個地方,但一個對象作為參數傳遞時,傳遞的往往是一個原始的引用,甚至都不是副本。這樣出現的問題就是,參數被修改後會引起原函數中的對象會相應被修改,引起很多混雜不清晰的現象,甚至會是錯誤。所以如果參數是一個值類型,可以以一个临时变量取代该参数的位置。如果參數為一個引用類型,這時就需要避免這種情況出現。

// 移除对参数的赋值动作
/*
 * 原函數(值傳遞)
 */
composingMethodsDemo.printAreaSize = function (height, width, padding) {
    height = height + 2 * padding;
    width = width + 2 * padding;
    document.writeln(height * width);
};
/*
 * 重構後(值傳遞)
 */
composingMethodsDemo.printAreaSize = function (height, width, padding) {
    var outHeight = height + 2 * padding;
    outWidth = width + 2 * padding;
    document.writeln(outHeight * outWidth);
};
/*
 * 原函數(引用傳遞)
 */
composingMethodsDemo.printRectangleInfo = function (height, width) {
    var rectang = {
        height: 12,
        width: 15,
        padding: 5
    };
    this.printArea(rectang);        // 這裡正確
    this.printPerimeter(rectang);   // 由於rectang引用在printArea中被修改,這裏的輸出結構就錯了
};
composingMethodsDemo.printArea = function (rectang) {
    //這裡需要避免修改,如需更改參數對象,請在原函數進行修改
    rectang.height = rectang.height + 2 * rectang.padding;
    rectang.width = rectang.width + 2 * rectang.padding;
    document.writeln(rectang.height * rectang.width);
};
composingMethodsDemo.printPerimeter = function (rectang) {
    rectang.height = rectang.height + 2 * rectang.padding;
    rectang.width = rectang.width + 2 * rectang.padding;
    document.writeln(2 * (rectang.height + rectang.width));
};
/*
 * 重構後(引用傳遞)
 */
composingMethodsDemo.printRectangleInfo = function (height, width) {
    var rectang = {
        height: 12,
        width: 15,
        padding: 5
    };
    this.printArea(rectang);        // 這裡正確, 但rectang被更改 
    this.printPerimeter(rectang);   // 輸出錯誤,rectang再次被更改
};
composingMethodsDemo.printArea = function (rectang) {
    var height = rectang.height + 2 * rectang.padding,
    width = rectang.width + 2 * rectang.padding;
    document.writeln(height * width);
};
composingMethodsDemo.printPerimeter = function (rectang) {
    var height = rectang.height + 2 * rectang.padding,
    width = rectang.width + 2 * rectang.padding;
    document.writeln(2 * (height + width));
};

以函数对象取代函数

信號:你有一个大型函数,其中对局部变量的使用,使你无法釆用【提取函數】。

操作:将这个函数放进一个单独对象中,如此一来局部变量就成了对象内的值域(field) 然后你可以在同一个对象中将这个大型函数分解为数个小型函数。 

如果一個大型函數,功能比較單一,且局部變量很多,這時候我們沒辦法採用單純的【提取函數】方法來重構,這個時候我們就可以把大型函數轉換成一個函數對象來處理,然後將函數體分解成小函數。


替换你的算法

信號:把某个算法替换为另一个更清晰的算法。

操作:将函数本体(method body)替换为另一个算法。

算法過於複雜,或則循環過於深入都是影響代碼閱讀的因素,這裡我們需要了解循环复杂度。可以用【提取函數】或替換算法來解決這類複雜問題。


« 上一篇下一篇 »

评论列表:

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。