1. 程式人生 > 實用技巧 >重構,第一個案例

重構,第一個案例

初始設計與實現

1.需求:

(1)大體是設計一個影片出租店的程式,計算每一位顧客的消費金額並列印詳單。

(2)首先操作者會告訴程式,顧客租了哪些影片,租期多長,程式便根據租賃時間和影片型別算出費用。

(3)還有要知道影片分為三類:普通片,兒童片和新片。

(4)最後除了計算費用,還要為常客計算積分,積分會根據租片種類是否為新片而有不同

2.結構圖:

3.程式碼實現:

Movie類,影片:

@Data
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) { this.title = title; this.priceCode = priceCode; } }

Rental類,租賃:

@Data
public class Rental {
    private Movie movie;
    private int dayRented;

    public Rental(Movie movie, int daysRented) {
        this.movie = movie;
        this.dayRented = daysRented;
    }
}

Customer類,顧客類:

@Data
public class Customer {
    private String name;
    private
Vector rentals = new Vector(); public Customer(String name) { this.name = name; } public void addRental(Rental arg) { rentals.add(arg); } public String statement() { double totalAmount = 0; int frequentRenterPoints = 0; Enumeration rentalElement = rentals.elements(); String result = "Rental Record for " + getName() + "\n"; while (rentalElement.hasMoreElements()) { double thisAmount = 0; Rental each = (Rental) rentalElement.nextElement(); //計算總額 switch (each.getMovie().getPriceCode()) { case Movie.REGULAR: thisAmount += 2; if (each.getDayRented() > 2) { thisAmount += (each.getDayRented() - 2) * 1.5; } break; case Movie.NEW_RELEASE: thisAmount += each.getDayRented(); break; case Movie.CHILDRENS: thisAmount += 1.5; if (each.getDayRented() > 3) { thisAmount += (each.getDayRented() - 3) * 1.5; } break; default: break; } //增加積分 frequentRenterPoints++; //add bonus for a two day new release rental if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) && each.getDayRented() > 1) { frequentRenterPoints++; } //展示租賃詳情 result += "\t" + each.getMovie().getTitle() + "\n"; totalAmount += thisAmount; } result += "Amount owed is " + String.valueOf(totalAmount) + "\n"; result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points"; return result; } }

分析並重構

對上述程式碼進行分析

1.分析:

這段程式碼statement()方法很長,做了很多其他類應該完成的事,違背了單一職責原則,開放封閉原則等,靈活性和擴充套件性都比較差,也不方便複用,但是能滿足目前的需求。

2.重構的必要性:

可能你心裡想著:“不管怎麼說,它執行得很好,只要沒壞,就不要動它”。但實際上雖然它沒壞,但是它造成了傷害,它讓你的生活比較難過,因為當客戶有其他新的需求時(如客戶想改變影片分類規則,但還沒決定怎麼改,只是決定了幾套方案,一旦決定就要迅速改完),就很難完成客戶所需要的修改,所以重構是很有必要的。

小筆記:如果你發現需要為程式新增一個特性,而程式碼結構使你無法很方便地達成目的,那就先重構那個程式,是特性的新增比較容易進行,然後再新增特性。

對上述程式碼進行重構

1.重構第一步:

重構的第一步永遠相同,為即將修改的程式碼建立一組可靠的測試環境。

小筆記:重構之前,首先檢查自己是否有一套可靠的測試機制,這些測試必須有自我檢驗能力

2.首先分解重組statement()的switch判斷邏輯:

(1)先將switch判斷當一個方法提出來amountFor(Rental each),計算總額時,直接傳參,呼叫剛提出的計算總額方法即可得到thisAmount,這次改動後最好先做一次測試,避免後續改動過多增加測試難度。

thisAmount = amountFor(each);

小筆記:重構技術就是以微小的步伐修改程式,如果你發現錯誤,很容易就能發現它。

(2)改變amountFor()裡的變數名稱。

將 each 改為 aRental
將 thisAmount 改為 result

問:改名值得麼?

答:絕對值得,好的程式碼有良好的表達,和好的清晰度,改完之後記得先測一下。

小筆記:任何一個傻瓜都能寫出計算機理解的程式碼,唯有寫出人類可以理解的程式碼,才是優秀的程式設計師。

(3)觀察amountFor(Rental aRental)方法時,發現傳參時Rental型別引數,和Customer類無關,所以要調整位置,將這個方法放到Rental,方法名叫:getCharge(Rental aRental),然後將Customer類改成如下,並重新測試編譯。

Rental類:

public double getCharge() {
        double result = 0;
        //算出總額
        switch (getMovie().getPriceCode()) {
            case Movie.REGULAR:
                result += 2;
                if (getDayRented() > 2) {
                    result += (getDayRented() - 2) * 1.5;
                }
                break;
            case Movie.NEW_RELEASE:
                result += getDayRented();
                break;
            case Movie.CHILDRENS:
                result += 1.5;
                if (getDayRented() > 3) {
                    result += (getDayRented() - 3) * 1.5;
                }
                break;
            default:
                break;
        }
        return result;
    }

Customer類:

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

疑問:為什麼這裡不直接呼叫getCharge()方法,而是先呼叫amountFor(),再通過amountFor()呼叫getCharge()呢?

答:這就是下一步要做的事情,但不能直接就先呼叫新的方法。

(4)先遷移成為新方法,測試沒問題後,再刪除舊方法

將呼叫的amountFor()替換為thisAmount = each.getCharge();

(5)替換成 each.getCharge() 後發現,thisAmount 也沒了用處,因為它除了賦值沒其他作用,而且值在後面也不會有改變,於是將 thisAmount 替換成each.getCharge(),修改後及時測試。

小習慣:可以儘量取消一些臨時變數,像上面這種臨時變臉,被傳來傳去容易跟丟也沒有必要(這裡呼叫了兩次計算總額的方法,後續說明怎麼優化)

3.然後重構statement()的常客積分的計算

(1)觀察發現常客積分的計算也是隻與Rental有關,所以講計算的方法提到Rental類中。

public int getFrequentRenterPoints() {
    if ((getMovie().getPriceCode() == Movie.NEW_RELEASE) && getDayRented() > 1) {
        return 2;
    } else {
        return 1;
    }
}

將Customer類中提煉為:

//計算積分
frequentRenterPoints += each.getFrequentRenterPoints();

(2)同樣因為要算總的積分,所以也可以像計算總額一樣單獨提出來:

private int getTotalFrequentRenterPoints() {
    int result = 0;
    Enumeration rentalElement = rentals.elements();
    while (rentalElement.hasMoreElements()) {
        Rental each = (Rental)rentalElement.nextElement();
        result += each.getFrequentRenterPoints();
    }
    return result;
}

3.馬上要修改影片分類規則,但具體怎麼做還未決定,需要再進行重構

(1)思路:現在對程式進行修改,肯定是愚蠢的,應該進入積分計算和常客積分計算中,把因條件而異的程式碼替換掉,這樣才能為將來的改變鍍上一層保護膜。

(2)先改變switch語句,將getChange()方法移動到Movie裡,原因是本系統可能發生的變化是加入新影片的影響,這種變化帶有不穩定傾向,所以為儘量控制它的影響,就在Movie裡計算費用

疑問(未解決):為什麼在Movie裡計算費用就可以控制影響?

於是先將getChange()移動到Moive類中:

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;
                break;
            case Movie.CHILDRENS:
                result += 1.5;
                if (daysRented > 3) {
                    result += (daysRented - 3) * 1.5;
                }
                break;
            default:
                break;
        }
        return result;
    }

再改變Rental類裡相應程式碼:

public double getCharge() {
    return movie.getCharge(dayRented);
}

(3)以相同手法處理常客積分計算

Movie類:

public int getFrequentRenterPoints(int daysRented) {
    if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1) {
        return 2;
    } else {
        return 1;
    }
}

Rental類:

public int getFrequentRenterPoints() {
    return movie.getFrequentRenterPoints(dayRented);
}

(4)使用狀態模式來設計Movie類

這裡先了解下實現思路,之後看完重構後再細看第一章...

隨記:

1.程式碼塊越小,程式碼功能就越容易管理,程式碼的處理和移動就餘越輕鬆。

2.還不太能get到為什麼要將Rental類的邏輯遷移到Movie裡,雖然按照後面的結果,通過狀態模式來拆開Movie裡getCharge()的邏輯,在知道了後續實現的前提下我覺得將getCharge()的邏輯遷移到Movie裡是沒問題的,但要我根據文中所說因為可能新做影片類別,就直接要遷移這個方法到Movie裡,我是不能get到這個點的,直接用三種影片算價方式繼承Rental就可以吧,這樣就只用改變一個類,就算後續有新加影片,或者重新定義怎麼分片,Movie類也只是配置引數就行,不用大改。

3.還有另一點我也沒有想清楚,和第2點也是相關的,就是為什麼不能用繼承的方式,文中說:“一部影片可以在生命週期內修改自己的分類,一個物件卻不能在自己的生命週期修改所屬類”,這句話也沒有理解。

4.希望第2第3個問題,在看了後面的內容能得到解答。