重構,第一個案例
初始設計與實現
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; privateVector 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個問題,在看了後面的內容能得到解答。