Sung in Blog

                     一些技術文章 & 一些生活雜碎

          本文用eclipse的自動重構功能對一個程序實例進行重構,目的是探索Eclipse自動重構可以在多大程度上輔助重構這個過程。程序實例使用《Refactoring:Improving the Design of Existing Code》一書中的例子。
            
            Eclipse的自動重構功能能夠很好地支持各種程序元素的重命名,并自動更新相關的引用。Eclipse能夠支持方法、字段在類之間移動,并自動更新引用。Eclipse較好地支持內聯字段、函數的更新替換。Eclipse較好地支持抽取方法、變量等程序元素。
            
            重構的過程是一個不斷嘗試和探索的過程。Eclipse的重構支持撤銷和重做,并且能夠預覽重構結果,這些是很實用的功能。
            
            Eclipse的重命名、抽取方法、移動、內聯功能、更改方法特征符等代碼結構級別的重構方法,是比較成熟同時也值得使用的功能。至于設計結構上的重構,eclipse還不能很好地支持。但是作者相信,自動重構的理念應該是"工具輔助下的重構工作",人仍然承擔大部分重構工作。
            
            一、預備工作
            
            本文使用《Refactoring:Improving the Design of Existing Code》一書第一章的例子。重構前的代碼及每一步重構后的代碼見附件。讀者最好配合《Refactoring:Improving the Design of Existing Code》一書閱讀本文。
            
            Eclipse使用如下版本:
             
            同時安裝了中文語言包。
            
            二、重構第一步:分解并重組statement()
            
            目的:
            
            1、 把statement()函數中的swich語句提煉到獨立的函數amountFor()中。
            
            2、 修改amountFor()參數命名
            
            重構方法:
            
            Extract Method
            Rename Method
            
            方法:
            
            1、選中swich語句的代碼塊,在右鍵菜單中選擇"重構/抽取方法",出現參數對話框。Eclipse自動分析代碼塊中的局部變量,找到了兩個局部變量:each和thisAmount。其中,each只是在代碼塊中被讀取,但thisAmount會在代碼塊中被修改。按照重構Extract Method總結出來的規則,應該把each當作抽取函數的參數、thisAmount當作抽取函數的返回值。然而Eclipse并不做區分,直接把這兩個變量當作抽取新方法的參數,如圖。
             
            我們的目的是把在抽取函數中不會被修改的each作為參數;會被修改的thisAmount作為返回值。解決的辦法是,把 double thisAmount = 0; 這行代碼移到switch語句的上面,變成這樣:
            
            double thisAmount = 0;
            switch(each.getMovie().getPriceCode()){
            case Movie.REGULAR:
            thisAmount += 2;
            if(each.getDaysRented()>2)
            thisAmount += (each.getDaysRented()-2)*1.5;
            break;
            
            case Movie.NEW_RELEASE:
            thisAmount += each.getDaysRented()*3;
            break;
            
            case Movie.CHILDRENS:
            thisAmount += 1.5;
            if(each.getDaysRented()>3)
            thisAmount += (each.getDaysRented()-3)*1.5;
            break;
            }
            
            選中這段代碼,在右鍵菜單中選擇"重構/抽取方法",eclipse這次變得聰明點了,如圖。
             
            選擇"預覽"按鈕預先查看重構后的結果,符合我們最初的目的。
             
            選擇"確定"按鈕,重構后的代碼片斷如下:
            
            public String statement() {
            double totalAmount = 0;
            int frequentRenterPoints = 0;
            Enumeration rentals = _rentals.elements();
            String result = "Rental Record for " + getName() + "\n";
            
            while(rentals.hasMoreElements()){
            Rental each = (Rental)rentals.nextElement();
            
            double thisAmount = amountFor(each);
            
            frequentRenterPoints ++;
            if((each.getMovie().getPriceCode())==Movie.NEW_RELEASE &&each.getDaysRented()>1)
            frequentRenterPoints ++;
            
            result += "\t" + each.getMovie().getTitle() + "\t" +String.valueOf(thisAmount) + "\n";
            totalAmount += thisAmount;
            }
            
            result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
            result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
            
            return result;
            }
            
            /**
            * @param each
            * @return
            */
            private double amountFor(Rental each) {
            double thisAmount = 0;
            switch(each.getMovie().getPriceCode()){
            case Movie.REGULAR:
            thisAmount += 2;
            if(each.getDaysRented()>2)
            thisAmount += (each.getDaysRented()-2)*1.5;
            break;
            
            case Movie.NEW_RELEASE:
            thisAmount += each.getDaysRented()*3;
            break;
            
            case Movie.CHILDRENS:
            thisAmount += 1.5;
            if(each.getDaysRented()>3)
            thisAmount += (each.getDaysRented()-3)*1.5;
            break;
            }
            return thisAmount;
            }
            
            2、選中amountFor()的參數each,在右鍵菜單中選擇"重構/重命名",在對話框中輸入新的名稱:aRental,選擇確定,amountFor()中所有each的引用全部被替換成新的名稱。用同樣的辦法修改amountFor()中的局部變量thisAmount為result。重構后的amountFor()代碼如下:
            
            /**
            * @param aRental
            * @return
            */
            private double amountFor(Rental aRental) {
            double result = 0;
            switch(aRental.getMovie().getPriceCode()){
            case Movie.REGULAR:
            result += 2;
            if(aRental.getDaysRented()>2)
            result += (aRental.getDaysRented()-2)*1.5;
            break;
            
            case Movie.NEW_RELEASE:
            result += aRental.getDaysRented()*3;
            break;
            
            case Movie.CHILDRENS:
            result += 1.5;
            if(aRental.getDaysRented()>3)
            result += (aRental.getDaysRented()-3)*1.5;
            break;
            }
            return result;
            }
            
            三、重構第二步:搬移"金額計算"代碼
            
            目的:
            
            1、 將函數amountFor()轉移到Rental類中,并更名為getCharge()。
            
            2、 更新并替換所有對amountFor()的引用。
            
            重構方法:
            
            Move Method
            Change Method signatrue
            Inline Method
            Inline Temp
            
            方法:
            
            1、選中函數amountFor()的定義,在右鍵菜單中選擇"重構/移動",顯示參數設置對話框。把新方法名改成getCharge。按下"確定"按鈕,Customer Class中的amountFor()函數被移動到Rental Class中,并更名為:getCharge()。
             
            同時eclipse自動在Customer的amountFor()函數中添加一行對新函數的"委托"代碼:
            
            private double amountFor(Rental aRental) {
            return aRental.getCharge();
            }
            
            這行代碼會產生編譯錯誤,原因是amountFor()的private型被傳遞到了新的方法中:
            
            /**
            * @param this
            * @return
            */
            private double getCharge() {
            ……
            }
            
            2、繼續重構!選中getCharge()方法,在右鍵菜單中選擇"重構/更改方法特征符",彈出參數選擇對話框,把訪問修飾符從private改成public。Eclipse的編譯錯誤提示自動消失。
             
            3、回到Customer類,把所有對amountFor()引用的地方替換成直接對getCharge()的引用。選中Customer類的函數amountFor(Rental aRental),在右鍵菜單中選擇"重構/內聯",出現參數選擇對話框。
             
            選擇"確認"按鈕,引用amountFor()的地方被替換成對getCharge()的引用。
            
            public String statement() {
            ……
            double thisAmount = each.getCharge();
            ……
            }
            
            4、除去臨時變量thisAmount。
            
            選中變量thisAmount,在右鍵菜單中選擇"重構/內聯",重構預覽窗口如下,可見達到了重構的目的。按下"確認"按鈕重構代碼。
             
            statement()代碼:
            
            public String statement() {
            double totalAmount = 0; // 總消費金額
            int frequentRenterPoints = 0; // 常客積點
            Enumeration rentals = _rentals.elements();
            String result = "Rental Record for " + getName() + "\n";
            
            while(rentals.hasMoreElements()){
            Rental each = (Rental)rentals.nextElement(); //取得一筆租借記錄
            
            // add frequent renter points(累加 常客積點)
            frequentRenterPoints ++;
            // add bouns for a two day new release rental
            if((each.getMovie().getPriceCode())==Movie.NEW_RELEASE && each.getDaysRented()>1)
            frequentRenterPoints ++;
            
            // show figures for this rental(顯示此筆租借數據)
            result += "\t" + each.getMovie().getTitle() + "\t" +
            String.valueOf(each.getCharge()) + "\n";
            totalAmount += each.getCharge();
            }
            
            // add footer lines(結尾打印)
            result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
            result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
            
            return result;
            }
            
            四、重構第三步:提煉"常客積點計算"代碼
            
            目的:提取"常客積點計算"代碼并放在Rental類中,"常客積點計算"代碼如下。
            
            public String statement() {
            ……
            // add frequent renter points
            frequentRenterPoints ++;
            // add bouns for a two day new release rental
            if((each.getMovie().getPriceCode())==Movie.NEW_RELEASE && each.getDaysRented()>1)
            frequentRenterPoints ++;
            ……
            }
            
            重構后的代碼如下:
            
            frequentRenterPoints += each.getFrequentRenterPoints();
            
            重構方法:
            
            Extract Method
            Move Method
            Change Method signatrue
            Inline Method
            
            方法:
            
            1、 首先,抽取代碼到獨立的函數中。
            
            用"抽取方法"重構代碼,函數名:getFrequentRenterPoints。很遺憾,eclipse的不能生成諸如:frequentRenterPoints += getFrequentRenterPoints(Rental aRental); 的代碼。原因是執行自增操作的局部變量frequentRenterPoints要出現在等式右邊,因此抽取函數getFrequentRenterPoints()一定要把frequentRenterPoints作為參數。手工修改函數和對函數的引用,重構后的代碼如下:
            
            public String statement() {
            ……
            while(rentals.hasMoreElements()){
            ……
            frequentRenterPoints += getFrequentRenterPoints(each);
            ……
            }
            ……
            }
            
            /**
            * @param each
            * @return
            */
            private int getFrequentRenterPoints(Rental each) {
            if((each.getMovie().getPriceCode())==Movie.NEW_RELEASE && each.getDaysRented()>1)
            return 2;
            else
            return 1;
            }
            
            2、 把getFrequentRenterPoints()移動到Rental類中。
            
            3、 對getFrequentRenterPoints()"更改方法特征符"為public。
            
            4、 對Customer的函數getFrequentRenterPoints()執行內聯操作,重構目標完成。
            
            五、重構第四步:去除臨時變量(totalAmount和frequentRenterPoints)
            
            目的:去除臨時變量(totalAmount和frequentRenterPoints)
            
            方法:
            
            1、 分析totalAmount和frequentRenterPoints的定義和引用結構如下:
            
            // 聲明和定義
            double totalAmount = 0;
            int frequentRenterPoints = 0;
            ……
            // 在循環中修改
            while(rentals.hasMoreElements()){
            ……
            frequentRenterPoints += each.getFrequentRenterPoints();
            ……
            totalAmount += each.getCharge();
            ……
            }
            ……
            // 在循環外使用
            result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
            result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points";
            ……
            
            上述兩個變量在循環體外面定義和使用,在循環中被修改,運用Replace Temp with Query方法去除這兩個臨時變量是一項稍微復雜的重構。很遺憾,eclipse目前不支持這樣的重構。
            
            2、手工修改代碼。
            
            六、重構第五步:運用多態取代與價格相關的條件邏輯
            
            目的:
            
            1、 把Rental類中的函數getCharge()移動到Movie類中。
            
            2、 把Rental類中的函數getFrequentRenterPoints()移動到Movie類中。
            
            重構方法:
            
            Move Method
            Inline Method
            
            方法:
            
            1、 選中Rental類中的函數getCharge(),右鍵菜單選中"重構/移動",eclipse提示找不到接收者,不能移動。原因在于這行語句:
            
            switch(getMovie().getPriceCode()){//取得影片出租價格
            
            選中getMovie(),右鍵菜單選中"重構/內聯",確定后代碼成為:
            
            switch(_movie.getPriceCode()){ //取得影片出租價格
            
            選中getCharge(),執行"重構/移動"后,函數被移動到Movie類中。然而這只是部分達成了重構目的,我們發現,移動后的代碼把Rental作為參數傳給了getCharge(),手工修改一下,代碼變成:
            
            class Movie ……
            /**
            * @param this
            * @return
            */
            public double getCharge(int _daysRented) {
            double result = 0;
            switch(getPriceCode()){ //取得影片出租價格
            case Movie.REGULAR: // 普通片
            result += 2;
            if(_daysRented>2)
            result += (_daysRented-2)*1.5;
            break;
            
            case Movie.NEW_RELEASE: // 新片
            result += _daysRented*3;
            break;
            
            case Movie.CHILDRENS: // 兒童片
            result += 1.5;
            if(_daysRented>3)
            result += (_daysRented-3)*1.5;
            break;
            }
            return result;
            }
            
            class Rental……
            /**
            * @param this
            * @return
            */
            public double getCharge() {
            return _movie.getCharge(_daysRented);
            }
            
            2、用同樣的步驟處理getFrequentRenterPoints(),重構后的代碼:
            
            class Movie ……
            /**
            * @param frequentRenterPoints
            * @param this
            * @return
            */
            public int getFrequentRenterPoints(int daysRented) {
            if((getPriceCode())==Movie.NEW_RELEASE && daysRented>1)
            return 2;
            else
            return 1;
            }
            class Rental……
            /**
            * @param frequentRenterPoints
            * @param this
            * @return
            */
            public int getFrequentRenterPoints(int daysRented) {
            if((getPriceCode())==Movie.NEW_RELEASE && daysRented>1)
            return 2;
            else
            return 1;
            }
            
            七、重構第六步:終于……我們來到繼承
            
            目的:對switch語句引入state模式。
            
            方法:
            
            很遺憾,不得不在這里提前結束eclipse的自動重構之旅。Eclipse幾乎不能做結構上的重構。也許Martin Fowler在書中呼喚的自動重構工具止于"工具輔助下的重構工作"這一理念。藝術是人類的專利,編程藝術的夢想將持續下去。
            
            感興趣的讀者可以查看手工重構的最后一步代碼。將重構進行到底!
            
            附錄:eclipse支持的重構方法(摘自eclipse中文幫助)
            
           

          posted on 2005-11-02 15:42 Sung 閱讀(755) 評論(0)  編輯  收藏 所屬分類: Eclipse
          主站蜘蛛池模板: 开封市| 城固县| 霍林郭勒市| 崇阳县| 修文县| 温州市| 山阴县| 得荣县| 武安市| 孝义市| 无为县| 海宁市| 开鲁县| 合江县| 霞浦县| 闻喜县| 昌宁县| 舞阳县| 普洱| 兴隆县| 大荔县| 桂平市| 化隆| 南召县| 清远市| 彰化市| 洞头县| 六安市| 十堰市| 蒲城县| 翼城县| 安溪县| 滕州市| 卢氏县| 凤冈县| 柘城县| 平邑县| 延寿县| 苏尼特左旗| 太仆寺旗| 房产|