重構:改善既有代碼的設計

第一個案例:算法

重構的第一步:爲即將改變的代碼創建一組可靠的測試環境。數據庫

public class Movie {
    public static final int CHILDRENS = 2;
    public static final int REGULAR = 0;
    public static final int NEW_RELEASE = 1;
    private String _title;
    private int _priceCode;

    public Movie(String title, int priceCode) {
        _title = title;
        _priceCode = priceCode;
    }

    public String getTitle() {
        return _title;
    }

    public void setTitle(String _title) {
        this._title = _title;
    }

    public int getPriceCode() {
        return _priceCode;
    }

    public void setPriceCode(int _priceCode) {
        this._priceCode = _priceCode;
    }
}
public class Rental {
    private Movie _movie;
    private int _daysRented;

    public Rental(Movie _movie, int _daysRented) {
        this._movie = _movie;
        this._daysRented = _daysRented;
    }

    public Movie getMovie() {
        return _movie;
    }

    public int getDaysRented() {
        return _daysRented;
    }
}

 

public class Cunstomer {
    private String _name;
    private Vector _rentals = new Vector();

    public Cunstomer(String _name) {
        this._name = _name;
    }

    public void addRental(Rental arg) {
        _rentals.addElement(arg);
    }

    public String getName() {
        return _name;
    }

    public String statement() {
        double totalAmount = 0;
        int frequentRenterPoints = 0;
        Enumeration rentals = _rentals.elements();
        String result = "Rental Record for " + getName() + "\n";
        while (rentals.hasMoreElements()) {
            double thisAmount = 0;
            Rental each = (Rental) rentals.nextElement();
            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;

            }
            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;
    }
}

如今須要重構的就是這個statement:編程

重構這個statement分爲如下幾步:數組

第一步:switch語句性能優化

    

我用idea重構功能,他並自動生成的方法,thisAmount也是做爲入參傳進來的,看起來好像沒多大差,至少看到這我是沒看出來差異或者特殊的用意的。數據結構

第二步:搬移「金額計算」的代碼ide

        觀察amountFor()時,發現這個函數使用了Rental類的信息,沒有使用來自Customer類的信息。絕大多數狀況下,函數應該放在它所使用的數據的所屬對象內,因此amountFor()應該移動到Rental類去。而後找出舊函數的引用點並修改成引用新函數(全局搜索一下看都有哪些引用)。去掉舊函數。而後進行測試。函數

有時候會保留舊函數,而後讓它調用新函數。好比說舊函數是public的函數,而又不想修改其餘類的接口,這就是一種頗有效的方法。性能

移動以後,函數會有微調,到Rental類的方法,不須要參數了。單元測試

第三步:改完以後,thisAmount就變得多餘了,由於他接受的是each.getCharge()的結果,而且不會發生變化,因此能夠去掉thisAmount這個局部變量。

第四步:提煉「常客積分計算」代碼

首先,積分計算應該放在Rental類。而後,再看一下局部變量。each,能夠被看成參數傳入新函數;另外一個臨時變量是frequentRenterPoints,它在被使用前有初始化值,但提煉出來的函數並無讀取該值,因此咱們不須要把它看成參數傳進去,只須要把新函數的返回值累加上去就好了。

第五步:去除臨時變量。

臨時變量多是個問題,他們使函數變得冗長而複雜。這個例子有2個臨時變量totalAmount和frequentRentalPoints,他們都是用來從Rental對象中得到某個總量,能夠利用查詢函數來取代totalAmount和frequentRentalPoints這2個臨時變量。這樣類中任何函數均可以調用這個查詢函數了。

第六步:

假如,須要修改影片分類規則,費用計算方式和常客積分計算方式有待肯定。這個時候盲目重構,確定是不合適的。

這個時候能夠運用多態取代與價格相關的條件邏輯。

首先,是switch,最好不要再另外一個對象的屬性基礎上運用switch語句,若是不得不使用,也應該在對象本身的數據上使用,而不是在別人的數據上使用。

也就是getCharge()應該從Rental類移動到Movie類中。這個時候想讓方法可使用,必須把租期長度做爲參數傳遞進去。租期長度來自Rental對象。計算費用時須要2項數據:租期長度和影片類型。選擇把租期長度傳給Movie對象而不是將影片類型傳給Rental對象,是由於影片類型可能會發生變化,因此選擇在Movie對象內計算費用。

第7步:繼承

加入這一層間接性,能夠在Price對象內進行子類化動做,能夠在任何須要時刻修改價格。

爲了引入State模式,首先運用Replace Type Code with State/Strategy,將與類型相關的行爲搬移到State模式內。而後運用Move Method講switch語句移動到Price類,最後運用Replace Conditional with Palymorphism去掉switch語句。

運用Replace Type Code with State/Strategy:

第一步驟,針對類型代碼使用SelfEncapsulate Field,確保任什麼時候候都經過取值函數和設置函數來訪問類型代碼。多數訪問操做來自其餘類,他們已經在使用取值函數,但構造函數仍然直接訪問價格代碼,能夠在Movie構造函數中,用一個設置函數來代替直接訪問價格,而後編譯測試,確保沒有破壞任何東西。新建一個Price類,並在其中提供類型相關的行爲,即在Price類中加入一個抽象函數,並在全部子類中加上對應的具體函數。

第二步:修改Movie類中的「價格代號」訪問函數(取值函數/設置函數),讓他們使用新類。在Movie類內保存一個Price對象,而再也不保存一個_priceCode變量,修改訪問函數。而後從新編譯測試。

Move Method:對getCharge()實施Move Method。

搬移以後,運用Replace Conditional with Palymorphism:一次取出一個case分支,在相應的類創建一個覆蓋函數。這個函數覆蓋了父類中的case分支,父類中的代碼先不動,在取出下一個case分支,一次處理,並編譯測試(注意確保執行的是子類的)。處理完全部的case分支以後,把P.getCharge()聲明爲abstract。再運用一樣的手法處理getFrequentRenterPoints().可是這個方法不須要把超類函數聲明爲abstract,只須要爲新片類型增長一個覆蓋函數,並在超類留下一個已定義的函數,使它成爲一種默認行爲。

引入State模式,能夠作到,若是要修改任何與價格有關的行爲,或是添加新的訂價標準,或是加入其它取決於價格的行爲,程序的修改會容易不少。

通過以上重構以後,代碼變成了以下的樣子:

public abstract class Price {
    abstract int getPriceCode();

    abstract double getCharge(int daysRented);

    public int getFrequentRenterPoints(int daysRented) {
        return 1;
    }

}
public class RegularPrice extends Price {
    @Override
    int getPriceCode() {
        return Movie.REGULAR;
    }
   public double getCharge(int daysRented) {
        double thisAmount = 2;
        if (daysRented > 2)
            thisAmount += (daysRented - 2) * 1.5;
        return thisAmount;
    }
}
public class ChildrensPrice extends Price {
    @Override
    int getPriceCode() {
        return Movie.CHILDRENS;
    }

    public double getCharge(int daysRented) {
        double thisAmount = 1.5;
        if (daysRented > 3)
            thisAmount += (daysRented - 3) * 1.5;
        return thisAmount;
    }
}
public class NewReleasePrice extends Price {
    @Override
    int getPriceCode() {
        return Movie.NEW_RELEASE;
    }

    public double getCharge(int daysRented) {
        return daysRented * 3;

    }

    public int getFrequentRenterPoints(int daysRented) {
        return daysRented > 1 ? 2 : 1;
    }
}
public class Rental {
    private Movie _movie;
    private int _daysRented;

    public Rental(Movie _movie, int _daysRented) {
        this._movie = _movie;
        this._daysRented = _daysRented;
    }

    public Movie getMovie() {
        return _movie;
    }

    public int getDaysRented() {
        return _daysRented;
    }

    public double getCharge() {
        return _movie.getCharge(_daysRented);
    }

    public int getFrequentRenterPoints() {
        return _movie.getFrequentRenterPoints(_daysRented);
    }

}
public class Movie {
    public static final int CHILDRENS = 2;
    public static final int REGULAR = 0;
    public static final int NEW_RELEASE = 1;
    private String _title;
    private Price _price;

    public Movie(String title, int priceCode) {
        _title = title;
        setPriceCode(priceCode);
    }

    public String getTitle() {
        return _title;
    }

    public void setTitle(String _title) {
        this._title = _title;
    }

    public int getPriceCode() {
        return _price.getPriceCode();
    }

    public void setPriceCode(int args) {
        switch (args) {
            case REGULAR:
                _price = new RegularPrice();
                break;
            case CHILDRENS:
                _price = new ChildrensPrice();
                break;
            case NEW_RELEASE:
                _price = new NewReleasePrice();
                break;
                default:
                    throw new IllegalArgumentException("Incorrent Price Code");
        }
    }

    public double getCharge(int daysRented) {
        return _price.getCharge(daysRented);

    }

    public int getFrequentRenterPoints(int daysRented) {
        return _price.getFrequentRenterPoints(daysRented);
    }
}
public class Cunstomer {
    private String _name;
    private Vector _rentals = new Vector();

    public Cunstomer(String _name) {
        this._name = _name;
    }

    public void addRental(Rental arg) {
        _rentals.addElement(arg);
    }

    public String getName() {
        return _name;
    }

    public String statement() {
        Enumeration rentals = _rentals.elements();
        String result = "Rental Record for " + getName() + "\n";
        while (rentals.hasMoreElements()) {
            Rental each = (Rental) rentals.nextElement();
            result += "\t" + each.getMovie().getTitle() + "\t" + String.valueOf(amountFor(each)) + "\n";

        }
        result += "Amount owed is" + String.valueOf(getTotalCharge()) + "\n";
        result += "You earned " + String.valueOf(getTotalFrequentRenterPoints()) + " frequent renter points";
        return result;
    }

    private int getTotalFrequentRenterPoints() {
        int frequentRenterPoints = 0;
        Enumeration rentals = _rentals.elements();
        while (rentals.hasMoreElements()) {
            Rental each = (Rental) rentals.nextElement();
            frequentRenterPoints++;
            frequentRenterPoints = each.getFrequentRenterPoints();

        }
        return frequentRenterPoints;
    }

    private double getTotalCharge() {
        double totalAmount = 0;
        Enumeration rentals = _rentals.elements();
        while (rentals.hasMoreElements()) {
            Rental each = (Rental) rentals.nextElement();
            totalAmount += amountFor(each);
        }
        return totalAmount;
    }

    private double amountFor(Rental rental) {
        return rental.getCharge();
    }
}

能夠試試把最開始的代碼考到idea中,而後看看若是是你,你會怎麼優化,而後按提示優化下,再跟答案比一下,最後對比一下本身的答案,仍是挺有意思的,這個就是第一章的內容。

 

第二章:重構原則

重構的目的是使軟件更容易被理解和修改。重構不會改變軟件可觀察的行爲,重構以後軟件功能一如以往。

與之造成對比的是性能優化。和重構同樣,性能優化一般不會改變組建的行爲(除了執行速度),以後改變內部結構。可是二者出發點不一樣:性能優化每每使代碼較爲難理解,但爲了獲得所須要的性能不得不那麼作。

爲什麼重構:

1.重構改進軟件設計,好比,消除重複代碼,肯定全部事物和行爲在代碼中只表述一次,方面將來代碼的修改。

2.重構使軟件更容易理解:在重構上花一點點時間,就可讓代碼更好地表達本身的用途,即「準確說出我所要的」。並且,還能夠利用重構來幫助本身協助理解不熟悉的代碼。真正動手修改代碼,讓它更好地反映出個人理解,而後從新執行,看它是否正常運行,檢驗本身的理解是否正確。隨着代碼的簡潔,就能夠看到之前看不到的設計層面的東西。

3.重構幫助找bug:

4.重構提升編程速度

 

什麼時候重構:

重構不是一件應該特別撥出事件作的事情,重構應該隨時進行,不該該爲了重構而重構。之因此重構,是由於想作別的事情,而重構能夠幫助把那些事作好。

三次法則:第一次作某件事時只管去作;第二次作相似的事會產生反感,但不管如何仍是能夠去作;第三次在作相似的事,就應該重構。事不過三,三則重構。

1.添加功能時重構

2.修補錯誤時重構:調試過程當中運用重構,讓代碼更具備可讀性,由於代碼還不夠清晰,沒有讓本身一眼看出bug。

3.複審代碼時重構:開始重構前能夠先閱讀代碼,獲得必定程度的理解,並提出一些建議。一旦想到一些點子,考慮是否能夠經過重構當即輕鬆地實現它們。多作幾回重構,代碼會變得更清楚,提出更多恰當的建議,能夠得到更高層次的認識。重構還能夠幫助代碼複審工做獲得更具體的認識,不只得到建議,並且其中許多建議可以理解實現,從實現中獲得成就感。

 

間接層和重構:

間接層的價值:容許邏輯共享;分開解釋意圖和實現;隔離變化;封裝條件邏輯

找出一個缺少「間接層利益」之處,在不修改現有行爲的前提下,爲它加入一個間接層,得到一個更有價值的程序,提升程序質量,讓本身再明天受益。

找出不值得的間接層,將它拿掉。這種間接層常以中介函數形式出現,它多是個組件,你原本指望在不一樣地方共享它或者讓它表現出多態性,最終卻只在一處用到。

 

重構的難題:

1.數據庫:重構常常出問題的一個領域就是數據庫。第一,絕大多數商用程序都和背後的數據庫結構緊密耦合在一塊兒;第二,數據遷移。就算將系統分層,將數據庫結構和對象模型間依賴降至最低,但數據庫結構改變仍是讓你不得不遷移全部數據,這是漫長而煩瑣的工做。

在非對象數據庫中,解決這個問題的辦法之一就是:在對象模型和數據庫模型之間插入一個分隔層,這就能夠隔離兩個模型各自的變化。升級某一模型是,只需升級上述的分離層便可。這樣的分割層會增長系統複雜度,但能夠帶來很大的靈活度。若是同時擁有多個數據庫,或若是數據庫模型較爲複雜使你難以控制,那麼就是不進行重構,這分隔層也是很重要的。

對開發者而言,對象數據庫既有幫助也有妨礙。自行完成遷移時,必須留神類中的數據結構變化,能夠放心把類的行爲轉移過去,可是轉移字段時必須格外當心,數據還沒有被轉移前就得先運用訪問函數形成「數據已經轉移」的假象。一旦肯定知道數據應該放在何處,就能夠一次性將數據遷移過去。這是惟一須要修改的只有訪問函數,能夠下降錯誤風險。

2.修改接口:如何面對那些必須修改「已發佈接口」的重構手法?儘可能讓舊接口調用新接口。但這樣會使接口變得複雜,還有另外一個選擇,能不發佈接口的時候,儘可能不要發佈接口。

 

什麼時候不應重構:現行代碼不能正常運行,盡是錯誤,重構不如重寫來的簡單。重構以前,代碼必須在大部分狀況下正常運行。項目接近最後期限,避免重構。

重構與性能:除了對性能有嚴格要求的實時系統,其餘任何狀況下「編寫快速軟件」的祕密就是,首先寫出可調的軟件,而後調整它以求得到足夠速度。

 

第三章:代碼的壞味道

1.重複代碼

2.過長函數

3.過大的類

4.過長參數列

5.發散式變化

6.霰(xian,第四聲)彈式修改

7.依戀情節

函數對某個類的興趣高過對本身所處類的興趣,這種孺慕之情一般就是數據。把這個函數移到另外一個地方,使用Move Method,有時候函數中只有一部分受這種依戀之苦,就應該使用Extract Method提煉到獨立的函數,在使用move method帶它去他該去的地方。若是一個函數用到幾個類的功能,判斷哪一個類擁有最多被此函數使用的數據,把該函數移到那個類,若是先以Extract Method將這個函數分解爲數個較小函數並分別置放於不一樣地點,上述步驟就更容易完成了。將老是一塊兒變化的東西放在一起。若是例外出現,就搬移那些行爲,保持變化只在一地發生。Stragegy和Visitor使得能夠輕鬆修改函數行爲,但多一層間接性的代價。

8.數據泥團

9.基本類型偏執

10.switch語句

11.平行繼承體系

12.冗餘類

13.誇誇其談將來性

14.使人迷惑的暫時字段

15.過分耦合的消息鏈。

16.中間人

17.狎暱關係

18.殊途同歸的類

19.不完美的庫類

20.純粹的數據類

21.被拒絕的遺贈

22.過多的註釋

第4章:構建測試體系

自動化的單元測試:test suit

每當收到bug報告,請先寫一個單元測試來暴露bug

繼續添加更多測試:觀察類該作的全部事情,而後針對任何一項功能的任何一種可能失敗狀況,進行測試。

測試的一項重要技巧就是「尋找邊界條件」。「尋找邊界條件」也包括尋找特殊的,可能致使測試失敗的狀況。

當事情被認爲應該會出錯,要記得檢查是否拋出了預期的異常。

把測試集中在可能出錯的地方。「花合理時間抓出大多數bug」要好過「窮盡一輩子抓出全部bug」。

構建一個良好的bug檢測器並常常運行它,對任何開發工做都將大有裨益,而且是重構的前提。

第5章:重構列表

第6章:從新組織函數

1.提煉函數

有局部變量時:

局部變量最簡單的狀況是:被提煉代碼段只是讀取這些變量的值,並不修改它們。這種狀況下我能夠簡單地將它們看成參數傳給目標函數。

若是局部變量是個對象,而被提取代碼調用了對該對象形成修改的函數,也能夠如法炮製,只須要將這個對象做爲參數傳遞給目標函數便可。只有在被提煉代碼真的對一個局部變量賦值的狀況下,才必須採起其餘措施。

2.內聯函數

3.內聯臨時變量

有一個臨時變量,只被簡單表達式賦值一次,而它妨礙了其餘重構手法。就須要內聯化。

4.以查詢取代臨時變量

5.引入解釋性變量

將複雜表達式(或其中一部分)的結果放進一個臨時變量,以此變量名稱來解釋表達式用途。

6.分解臨時變量

程序中某個臨時變量被賦值超過一次,它既不是循環變量,也不被用於收集計算結果,針對每次賦值,創造一個獨立、對應的臨時變量。

7.移除對參數的賦值

(這個我不是很能理解和吸取)

8.以函數對象取代函數

有一個大型函數,其中對局部變量的使用使你沒法採用Extract Method。將這個函數放進一個單獨對象中,如此一來局部變量就成了對象內的字段。而後能夠在同一個對象中將這個大型函數分解爲多個小型函數。

它帶來的好處是:能夠輕鬆地對compute()函數採起Extract Method,沒必要擔憂參數傳遞問題。

9.替換算法

第七章:在對象之間搬移特性

一、搬移函數:

2.搬椅字段

3.提煉類

4.將類內聯化

5.隱藏委託關係

6.移除中間人

7.引入外加函數

8.引入本地擴展

這個根本沒看懂。

第八章:

1.自封裝字段

2.以對象取代數據值

3.將值對象改成引用對象

4.將引用對象改成值引用

5.以對象代替數組

6.複製被監視的數據

7.將單向關聯改成雙向關聯

8.將雙向關聯改成單向關聯

7和8都沒怎麼懂

9.以字面常量取代魔法數

10.封裝字段

11.封裝集合

12.以數據類取代記錄

13.以類取代類型碼

 

(未完待續)

相關文章
相關標籤/搜索