1.2重構-第一章

1.2重構的第一步

         每當要進行重構的時候,第一個步驟永遠相同:即爲將修改的代碼建立一組可靠的測試環境,這些測試是必要的,因爲儘管遵循重構手法可以使我避免絕大多數引入bug的情形,但我畢竟是人,畢竟有可能犯錯,所以我需要可靠的測試。

         接1.1,由於statement()的運作結果是個字符串,所以我首先假設一些客戶,讓他們每個人各租幾部不同的影片,然後產生報表字符串,然後我就可以拿新的字符串和手上已經檢查過的參考字符串做比較,運行這些測試只需要幾秒鐘,所以你會看到我經常運行他們。

         測試過程中很重要的一部分,就是測試程序對於結果的報告方式,他們要麼說“OK”,表示所有新字符串都和參考參數一樣,要麼就列出失敗清單,顯示問題字符串的出現行號。這些測試都能夠自我檢驗。

         進行重構的時候,我們需要依賴測試,讓他告訴我們是否引入bug。好的測試是重構的根本。花時間建立一個優良的測試機制是完全值得的,因爲當你修改程序時,好測試會給你必要的安全保障。測試機制在重構領域的地位實在太重要了。

1.3 分解並重組statement()

         第一個明顯一起我們注意的就是長得離譜的statement()。每當看到這樣長長的函數,我就想把它大卸八塊。要知道,代碼塊越小,代碼的功能就愈容易管理,代碼的處理和移動也就越輕鬆。

         本章重構過程的第一個階段中,我將說明如何把長長的函數切開,並把較小塊的代碼移至更合適的類。降低代碼重複量,從而使新的(打印HTML格式詳單的)函數更容易撰寫。

       第一步找出代碼的邏輯泥潭並運用Extract Method(提煉函數)。本例一個明顯的邏輯泥團就是switch語句(計算金額功能),把它提煉到獨立函數中似乎比較好。

         和任何重構手法一樣,當我提煉一個函數時,我必須知道可能出什麼錯。如果提煉不好,就可能給程序引入bug。所以重構之前我們要先想出安全做法。可以參考重構列表中的安全步驟(後期補充)。

         首先在這段代碼裏找出函數內的局部變量和參數。我們找到了兩個,each(租賃實體對象)和thisAmount(某種影片的總金額數),前者並未被修改,後者會被修改。任何不會被修改的變量都可以當作參數傳入新的函數,至於會被修改的變量就需要格外的小心。如果只有一個變量會被修改,可以把它當作返回值。thisAmount是個臨時變量,其值在每次循環起始處被設爲0,並且在switch語句之前不會改變,所以可以直接把新函數的返回值賦給它。

         下面將展示重構前後的代碼。重構前的代碼在上,重構後的代碼在下。凡是從函數提煉出來的代碼,以及新代碼所做的任何修改,只要不明顯的都以粗體特別提醒。

 原始代碼

/**
 * 顧客實體
 * @author harry
 */
public class Customer {
	private String _name;
	private Vector<Rental> _rentals = new Vector<>();
	public Customer(String _name) {
		super();
		this._name = _name;
	}
	
	public void addRental(Rental arg){
		_rentals.addElement(arg);
	}
	public String getName() {
		return _name;
	}
	public void setName(String _name) {
		this._name = _name;
	}
	
	public String statement(){
		double totalAmount = 0;//總金額
		int frequentRenterPoints = 0;//本次總積分
		
		Enumeration<Rental> rentals = _rentals.elements();
		// 租賃備案
		String result = "Rental Record for "+getName()+"\n";
		while(rentals.hasMoreElements()){
			double thisAmount = 0;
			Rental each = rentals.nextElement();
			
			// 計算金額
			switch(each.get_movie().get_priceCode()){
				case Movie.REGULAR:
					thisAmount += 2;
					if (each.get_dayRented() > 2) {
						thisAmount += (each.get_dayRented()-2)*1.5;
					}
					break;
				case Movie.NEW_RELEASE:
					thisAmount += each.get_dayRented()*3;
					break;
				case Movie.CHILDRENS:
					thisAmount += 1.5;
					if (each.get_dayRented() > 3) {
						thisAmount += (each.get_dayRented()-3)*1.5;
					}
					break;
			}
			
			// 常規積分累加
			frequentRenterPoints++;
			// 特殊新書積分計算
			if (each.get_movie().get_priceCode() == Movie.NEW_RELEASE &&
				each.get_dayRented() > 1) {
				frequentRenterPoints++;
			}
			
			// 顯示憑條
			result += "\t"+each.get_movie().get_title()+"\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;
	}
}
重構後代碼

/**
 * 顧客實體
 * @author harry
 */
public class Customer01 {
	private String _name;
	private Vector<Rental> _rentals = new Vector<>();
	public Customer01(String _name) {
		super();
		this._name = _name;
	}
	
	public void addRental(Rental arg){
		_rentals.addElement(arg);
	}
	public String getName() {
		return _name;
	}
	public void setName(String _name) {
		this._name = _name;
	}
	
	public String statement(){
		double totalAmount = 0;//總金額
		int frequentRenterPoints = 0;//本次總積分
		
		Enumeration<Rental> rentals = _rentals.elements();
		// 租賃備案
		String result = "Rental Record for "+getName()+"\n";
		while(rentals.hasMoreElements()){
			double thisAmount = 0;
			Rental each = rentals.nextElement();
			
			// 計算金額
			thisAmount = amountFor(each);
			
			// 常規積分累加
			frequentRenterPoints++;
			// 特殊新書積分計算
			if (each.get_movie().get_priceCode() == Movie.NEW_RELEASE &&
				each.get_dayRented() > 1) {
				frequentRenterPoints++;
			}
			
			// 顯示憑條
			result += "\t"+each.get_movie().get_title()+"\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;
	}
	
	// 計算金額
	private double amountFor(Rental each){
		double thisAmount = 0;
		switch(each.get_movie().get_priceCode()){
		case Movie.REGULAR:
			thisAmount += 2;
			if (each.get_dayRented() > 2) {
				thisAmount += (each.get_dayRented()-2)*1.5;
			}
			break;
		case Movie.NEW_RELEASE:
			thisAmount += each.get_dayRented()*3;
			break;
		case Movie.CHILDRENS:
			thisAmount += 1.5;
			if (each.get_dayRented() > 3) {
				thisAmount += (each.get_dayRented()-3)*1.5;
			}
			break;
	}
		return thisAmount;
	}
}

         現在我們已經把原來的函數分爲兩塊,可以分別處理它們。但amountFor()內的某些變量名稱不太可愛,所以要修改掉,修改這些變量名是代碼清晰的關鍵。

修改變量名後的代碼:

private double amountFor(Rental aRental) {
	double result = 0;
	switch (aRental.get_movie().get_priceCode()) {
	case Movie.REGULAR:
		result += 2;
		if (aRental.get_dayRented() > 2) {
			result += (aRental.get_dayRented() - 2) * 1.5;
		}
		break;
	case Movie.NEW_RELEASE:
		result += aRental.get_dayRented() * 3;
		break;
	case Movie.CHILDRENS:
		result += 1.5;
		if (aRental.get_dayRented() > 3) {
			result += (aRental.get_dayRented() - 3) * 1.5;
		}
		break;
	}
	return result;
}

 最終UML如下圖


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