重構(一)

重構

通過調整程序結構,在不改變功能的前提下,提高其可讀性,降低修改成本。

Why:

你寫的代碼計算機能讀懂,不代表人能讀懂。

  • 1 提高代碼維護性:項目快速迭代,功能驅動程序設計。隨着功能添加,程序逐漸失去自己的結構,可讀性大幅下降。新人可以迅速熟悉項目,減少開發公司成本。
  • 2 提高代碼健壯性:在重構的過程,可以重新梳理代碼結構,順帶可以發現一些位置的bug。
  • 3 提高開發速度:有過開源項目經驗的同學,在二次開發時候都有過這樣的體驗,可以很方便替換某些子功能。
  • 4 提高程序性能: 毋庸置疑,拋開冗餘的代碼,合理的結構勢必會提高性能。而且在良好的結構下,我們可以根據性能測試結果去優化某一個步驟,某一個模塊。

這裏再多說一句,項目經理往往是不希望碼農重構代碼,需求永遠是無窮無盡的,但往往 7+1 > 8。

When:

Don Roberts 認爲‘事不過三’ 是重構的時機。

  • 1 當添加新功能時候,發現當前代碼結構使得添加過程變得麻煩時,那就先重構,在添加。
  • 2 修改bug時候,當拿到QA報告說此處存在bug,而你不能根據代碼很快找出哪裏出錯時,可以考慮重構此處,使得代碼邏輯更加清晰。
  • 3 代碼Review時,公司裏在提交代碼時基本都會做double check以確保代碼不存在粗淺的bug。自己代碼往往自己看起來很清晰,可能在別人眼裏就變成一坨,所以根據別人意見反饋,重構結構。
  • 4 在重構時,如果發現無從下手,那可以考慮重寫項目了。

Which:

  • 1 重複代碼,設法將重複代碼合二爲一肯定是有利於優化代碼結構的。對於同一個類中的,可以封裝一個私有函數。在不同類中,如果有共同繼承,則可以在父類中封裝一個函數,如果沒有共同繼承,可以考慮增加一個xxUtil.class提供公共服務。這些方法在開源項目中,隨處可見。

這是一個 service 的例子,主要是通過調用數據庫接口執行某類操作,再調用數據庫之前,需要對用戶進行身份驗證,我們可以這樣寫。

    public void insertItem(String userName, String userPwd, String item){
        boolean success = false;

        /**
         *  Need to verify before execute operation
         */
        User user = UserUtil.getUserByName(userName);

        if(user != null){
            if(user.getPwd() == userPwd){
                success = true;
            }
        }

        if(success){
            System.out.println("Have access to insert to database");
        }
    }

    public void deleteItem(String userName, String userPwd, String item){
        boolean success = false;

        /**
         *  Need to verify before execute operation
         */
        User user = UserUtil.getUserByName(userName);

        if(user != null){
            if(user.getPwd() == userPwd){
                success = true;
            }
        }

        if(success){
            System.out.println("Have access to delete from database");
        }
    }

我們分析一下上面兩個方法,顯然它們的主要功能是 insert/delete, 而不是verify。而且它們兩個verify部分的代碼重複。假如有一天,你需要更改驗證方法,比如通過 token 的方式,那麼你需要更改每一個方法中這部分代碼。因此,我們應該講該部分代碼提取出來,並給它起一個名字。

    public void insertItem(String userName, String userPwd, String item){

        if( verifyByUserPwd(userName, userPwd )){
            System.out.println("Have access to insert to database");
        }
    }

    public void deleteItem(String userName, String userPwd, String item){

        if( verifyByUserPwd(userName, userPwd) ){
            System.out.println("Have access to delete from database");
        }
    }

    private boolean verifyByUserPwd( String userName, String userPwd ){
        boolean rtval = false;
        User user = UserUtil.getUserByName(userName);

        if(user != null){
            if(user.getPwd() == userPwd){
                rtval = true;
            }
        }
        return rtval;
    }

一個好的函數名字可以代碼一段蹩腳的註釋。

  • 2 長函數,首先要定義多長算長。這個在各個語言中都是不一樣的,python和Java行數肯定就不一樣。個人建議是將具備獨立邏輯的功能封裝到一個private函數。現代OO語言進程內函數調用基本已經沒有開銷。如果每一個封裝的函數都有一個易懂的名字,閱讀代碼的人根本就不需要查看具體函數的功能,就可以明白主流程。
    public void tooLong(){
        List<Integer> dataList = new ArrayList<Integer>();
        List<Integer> evenList = new ArrayList<Integer>();

        /**
         *  Init a integer list with 100 elements.
         */
        Random r = new Random();
        for( int i = 0; i < 100; i++){
            dataList.add(r.nextInt());
        }

        /**
         *  Throw away which belong to odd.
         */
        for(int i : dataList ){
            if( i % 2 == 0){
                evenList.add(i);
            }
        }

        /**
         *  Print satisfied numbers.
         */
        for(int i : evenList ){
            System.out.println(i);
        }
    }

重構後:

    public void noLongerTooLong(){
        List<Integer> dataList = new ArrayList<Integer>();
        List<Integer> evenList = new ArrayList<Integer>();

        /**
         *  Init a integer list with 100 elements.
         */
        dataList = genRandomIntegerList(100);

        /**
         *  Throw away which belong to odd.
         */
        evenList = getEvenIntegerList(dataList);

        /**
         *  Print satisfied numbers.
         */
        printList(evenList);
    }

    private List<Integer> genRandomIntegerList(int num){
        List<Integer> dataList = new ArrayList<Integer>();

        Random r = new Random();
        for( int i = 0; i < 100; i++){
            dataList.add(r.nextInt());
        }
        return dataList;
    }

    private List<Integer> getEvenIntegerList(List<Integer> dataList){
        List<Integer> evenList = new ArrayList<Integer>();

        for(int i : dataList ){
            if( i % 2 == 0){
                evenList.add(i);
            }
        }
        return evenList;
    }

    private void printList(List<Integer> dataList){
        for(int i : dataList ){
            System.out.println(i);
        }
    }
  • 3 過長的類,一個類如果過長,很可能意味着大量邏輯耦合,比如說部分功能完全可以抽象出一個新的類去實現。這個就完全考驗設計模式的(GOF)能力了。使每一個類都只完成自己的功能,會使得日後擴展更加靈活。
  • 4 函數參數過長,絕大多數情況下函數參數都是函數需要的,那麼怎麼減少數量呢。之前在做openstack項目時候,經常看到一個函數幾十個參數。這些參數很多還是一個json格式字符串或者一個list。如果展開來看,可能有上百個參數,顯然這樣的函數,沒人願意維護的,也沒人願意調用把。處理方法呢,就是儘量將同類的變量封裝到對象,list,甚至json中。函數可以通過稍後的解析獲取它要的變量。
  • 5 功能集中,考慮一個類在不同的情景下需要修改不同的類函數,這個時候,就要考慮拆分類,使得某一種情景的修改只發生在該類之中,而不影響其他類。
  • 6 功能發散,當你要增加某個功能,需要在很多地方修改代碼時候,可以考慮將這些要修改的地方抽出來行一個新的類。
  • 7 當發現一個類中的函數調用了半打別的類中的數據時,可以考慮把這個函數移動到調用的函數中。
  • 8 當發現兩個類中很用到很多相同的變量時候,可以考慮爲它們創建一個類。
  • 9 去除Switch,switch是一種對枚舉情況的硬編碼,但每次增加一種新的情景時,都需要找到switch的地方,添加代碼。善於使用多態和Null Object。
  • 10 平行繼承,如果發現爲了一個類增加一個子類時,需要爲另一個類添加子類,這時候應該讓一個繼承體系的實例去引用另一個繼承體系實例。
  • 11 不要爲未來還沒有添加的功能,預留參數,方法。
  • 12 類中某些變量僅爲特殊情景下使用時,可以爲這個變量和其他相關函數創建一個新類。
  • 13 如果 A->B->C->D->E,函數調用成鏈式,對於這種情況,儘量增添一個函數使得A->E。
  • 14 如果發現兩個不同名的函數做同一件事,可以嘗試移除其中之一。
  • 15 代碼寫的清晰,可能就不需要過多的註釋。對於大塊註釋的地方,考慮抽出來封裝成一個函數,起一個能表達意思的名字要比註釋簡潔的多。

How:

Kent Beck認爲沒有不能加一個間接層解決不了的計算機問題。

爲了重構順利,請添加足夠的測試用例,測試用例的好處是當你移動部分代碼後,可以很快回歸你是否更改了當前的功能。重構的基本原則是不能引入新的bug。測試用例的好處是你不用盯着console去看你的結果是否正確,只需要console告訴你是否都ok。在某些情況下甚至可以先寫測試代碼,再添加實際功能。良好的測試用例是保證重構成功的基礎。

具體方法參見 重構(二)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章