重構改善既有代碼的設計 --方法篇

重構改善既有代碼的設計 --方法篇

重新組織數據

  • 目的:解決不易理解的錯綜複雜的長函數,促使代碼更清晰更易維護。

  • 解決思路:長函數變成短函數、算法結構優化

  • 常用方法:抽離函數(Extract Method),通過查詢的方式來獲取值 (Replace Temp With query), 優化算法 (Substitute Algorthm)

  • AF老項目中有很多行數過百的函數,導致維護起來很費時費力,很多函數都可以用Extract Method方法,拆成小函數,無論是後期增加邏輯還是要代碼複用都都能幫助理解。另外函數很長的原因的是場景特別多有很多出來了函數,但是這些個處理函數都寫在一個入口中,還是希望用Replace Temp With query方式來處理數據

  1. 抽離函數(Extract Method)
  • 一個函數如果需要寫很多註釋來讓人理解的時候,這個函數應該是可以被拆解成小函數。良好的函數命名可以讓人更容易理解代碼

  • 場景說明


    let infoArr = Array(10).fill().map((item, index) => {
        return {
            name: 'VFrank' + index,
            age: Math.floor(Math.random() * 10 + 20)
        }
    });

    function printInfo() {

        // 輸出一個橫幅
        console.log('**********************')
        console.log('*****Hello VFrank*****')
        console.log('**********************')

        // 獲取所有name和age字段數組統計項
        let temp = { nameArr: [], ageArr: [] };

        infoArr.forEach(item) {
            let { name, age } = item;
            temp.nameArr.push(name);
            temp.ageArr.push(age);
        }

        // 打印統計數據
        console.log(nameArr);
        console.log(ageArr);
    }

  • 上面的printInfo函數,其實每個註釋都可以是獨立的一個函數,這裏說明抽離函數中的幾個問題
  1. 第一塊註釋,那無對內部變量的引用,可以直接方法抽離爲一個小函數
  2. 第二塊註釋,屬於數據處理層可以單獨抽離出來,以返回值形式
  3. 第三快註釋,打印邏輯對內部變量有引用關係,需要以參數形式傳入

修改後如下


    let infoArr = Array(10).fill().map((item, index) => {
        return {
            name: 'VFrank' + index,
            age: Math.floor(Math.random() * 10 + 20)
        }
    });

    function printInfo() {

        printBanner();
        let result = getOutStand(); // 這裏用Replace Temp With query方法修改
        printDetail(result);

    }

    // 無引用內部變量關係直接抽離
    function printBanner() {
        console.log('**********************')
        console.log('*****Hello VFrank*****')
        console.log('**********************')
    }

    // 計算邏輯代碼抽離,
    function getOutStand() {

        let result = { nameArr: [], ageArr: [] };
        infoArr.forEach(item) {
            let { name, age } = item;
            result.nameArr.push(name);
            result.ageArr.push(age);
        }
        return result;

    }

    // 對內部變量有引用關係的通過參數傳入
    function printDetail(result) {
        console.log(result.nameArr);
        console.log(result.ageArr);
    }

  1. 通過查詢的方式來獲取值(Replace Temp With query)
  • 函數中存在很多臨時變量,這些變量只在這個函數中使用,這個導致一個函數過長而不易理解。換成一個函數可以增加函數的顆粒度,給後續維護帶來極大的幫助

  • 場景


    // 一個計算價格的函數, 基本價格 * 折扣
    getPrice() {

        let basePrice = this.quantity * this.itemPrice;
        let discountFactor = 0;
        if (basePrice > 1000) {
            discountFactor = 0.95;
        } else {
            discountFactor = 0.98;
        }

        return basePrice * discountFactor;
    }

  • 第一次修改把basePrice抽取出來
    getPrice() {

        let discountFactor = 0;
        if (this.getBasePrice() > 1000) {
            discountFactor = 0.95;
        } else {
            discountFactor = 0.98;
        }

        return this.getBasePric() * discountFactor;
    }

    getBasePric() {
        return this.quantity * this.itemPrice;
    }

  • 修改後發現discountFactor其實也可以抽出來,再次修改如下

    getPrice() {

        return this.getBasePric() * this.discountFactor();
    }

    getBasePric() {
        return this.quantity * this.itemPrice;
    }

    getDisCountFactor() {

        if (this.getBasePrice() > 1000) {return 0.95;}
        return 0.98;
    }

在對象之間搬移特性

  • 目的:解決對象因爲承擔過多的責任而變得臃腫不堪的場景(比如業務代碼Mgr經常作了Panel或者Form應該做的事情)

  • 解決思路:梳理對象的責任,移動對應的函數。(決定把責任放在哪)

  • 常用方法:搬移方法(Move Method),提煉類(ExTract Class)

搬移方法(Move Method)

  • 一個類如果與另外一個類有着高度耦合的關係,就需要搬移函數,使得每個類更簡單

  • 小原則:函數與哪個對象交流比較多,就移入這個對象

重新組織數據

  • 目的:輕鬆處理數據的重構

  • 解決思路:魔數改成常量、數組轉換成對象

  • 常用方法:搬移方法(Move Method),提煉類(ExTract Class)

簡化條件表達式

  • 目的:簡化一些條件邏輯複雜的判斷

  • 解決思路:將“分支邏輯”和“操作細節”進行分離

  • 常用方法:分解條件表達式(Decompose Conditioal),合併條件表達式(Consolidate Conditional Expression),以衛語句代替嵌套表達式(Replace Nested Conditional withGuard Clauses)

分解條件表達式(Decompose Conditioal)

  • 複雜條件邏輯是導致複雜度上升的原因之一,編寫不同的條件分支,根據不同的分支做不同的事情,但是複雜的條件判斷會讓你弄不清楚爲什麼要這樣做,可讀性大大的降低,所以需要拆解爲多個獨立的判斷函數,根據用途來命名區分,突出條件邏輯。

  • 場景


    // dealWithData(num) {
    //     if (num < this.minNum || num > this.maxNum) {
    //         // doSomeThing
    //     } else {
    //         // doSomeThing
    //     }
    // }


    function dealWithData(num) {
        if (this.isOverFlow(num)) {
            // doSomeThing
        } else {
            // doSomeThing
        }
    }

    isOverFlow(num) {
        return num < this.minNum || num > this.maxNum
    }

合併條件表達式(Consolidate Conditional Expression)

  • 有一系類的判斷條件,得到的結果都是一致的

    // function dealWithData(num) {
    //     if (num < 2) {
    //         return 0;
    //     }

    //     if (num > this.maxNum) {
    //         return 0;
    //     }

    //     if (this.isFlag(num)) {
    //         return 0;
    //     }
    // }

    // 修改後
    dealWithData(num) {
        if (this.isZero(num)) {
            return 0;
        }
    }

    isZero(num) {
        return num < 2 || num > this.maxNum || this.isFlag(num);
    }

以衛語句代替嵌套表達式(Replace Nested Conditional withGuard Clauses)

  • 函數中的條件邏輯使人難以看清正常的執行路徑的情況下,使用衛語句理清

//   dealWithData() {
//         let result;

//         if (this.isManage()) {
//             result = this.getManage();
//         } else {
//             if (this.isEmploy()) {
//                 result = this.getEmploy();
//             } else {
//                 if (this.isReired()) {
//                     result = this.getReired();
//                 } else {
//                     result = this.getDefault()
//                 }
//             }
//         }
//     }

    // 修改後
    dealWithData() {

        if (this.isManage()) {
            return this.getManage();
        }
        if (this.isEmploy()) {
            return this.getEmploy();
        }

        if (this.isReired()) {
            return this.getReired();
        }
        result = this.getDefault()
    }

簡化函數調用

  • 目的:讓接口變得更容易理解和使用

  • 解決思路:對函數的返回值、函數名、參數進行優化

  • 常用方法:函數改名(Rename Method),添加參數(Add Parameter),分離查詢函數和修改函數(Spaaeate Query form Modifier),保持對象完整(Preserve Whole Object)

函數改名(Rename Method)

  • 將複雜的處理過程分解成小函數,小函數命名不好,很難弄清楚函數式做什麼的,沒有達到預期的效果

添加參數(Add Parameter)

  • 某個函數需要從調用端的到更多的信息,爲此可以添加一個對象參數,讓該對象帶進函數所需信息。但是過多的參數要注意數據泥團的問題。

分離查詢函數和修改函數(Spaaeate Query form Modifier)


    // doSomeCode(nameArr) {
    //     for (let i = 0; i < nameArr.length; i++) {
    //         if (name === 'VFrank') {
    //             showMsg();
    //             return 'VFrank'
    //         }
    //     }
    //     return ''
    // }


    // 修改後
    doSomeCode(nameArr) {
        if (findPerson(nameArr)) {
            showMsg();
        }
    }

    findPerson(nameArr) {
        for (let i = 0; i < nameArr.length; i++) {
            if (name === 'JhoVFrankn') {
                return 'VFrank'
            }
        }
    }

處理概括關係

  • 目的:梳理繼承關係,劃分函數應該在父類還是在子類中

  • 解決思路:將函數方法在繼承體系同上下移動

  • 常用方法: 函數上移(Pull Up Method), 函數下移(Push Down Method),塑造模板函數(Form Template Method), 提煉父類,提煉子類, 提煉接口

函數上移(Pull Up Method), 函數下移(Push Down Method)

  • 有些函數在子類中實現的都是相同的功能,這些函數應該抽離到父類中,反之,如果父類函數中某個函數只是與某一個子類有關係,這個函數應該移到子類中去

構造函數本體上移

  • 在各個子類中擁有一些構造函數, 本體幾乎一樣,可以在超類中構造一個函數,並在子類中調用。

  • 這一點Ext裏面用的很多,比如Ext的field組件,在initComponent的時候addEvent(‘focus’, ‘blur’)等事件,把子類共有的操作都提到父類中來執行

塑造模板函數

  • 子類中,相應的某些函數以相同的順序來執行類似的操作,但是每個函數的操作細節上有所不同,可以把這些操作放到獨立的函數中,用同樣的函數名錶示。

  • Ext的組件的基本上都是這種模式來實現的, 每個組件都有initComponent,onRender,afterRender等函數,就是 用了這種方式,由此也有了鉤子函數,也就有了生命週期概念。

發佈了57 篇原創文章 · 獲贊 9 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章