1. 程式人生 > >【Java重構系列】重構31式之搬移方法

【Java重構系列】重構31式之搬移方法

重構第二式:搬移方法 (Refactoring 2: Move Method)

       毋容置疑,搬移方法(Move Method)應該是最常用的重構手段之一,正因為太常用而且較為簡單,以至於很多人並不認為它是一種很有價值的重構,但事實並非如此,在最初的程式碼誕生之後,有些方法可能會被放在一些不合適的地方,例如,一個方法被其他類使用比在它所在的類中的使用還要頻繁或者一個方法本身就不應該放在某個類中時,我們應該考慮將它移到更合適的地方。搬移方法,顧名思義就是將方法搬移至合適的位置,如將方法搬移到更頻繁地使用它的類中。與搬移方法相似的還有一種重構手段是搬移欄位(Move Field),即搬移屬性。

       在《重構:改善既有程式碼的設計》一書中,多種壞味都需要使用搬移方法來進行重構,例如依戀情結(Feature Envy)、霰彈式修改(Shotgun Surgery)、平行繼承結構(Parallel Inheritance Hierarchies)、異曲同工的類(Alternative Classes with DifferentInterfaces)、狎暱關係(Inappropriate Intimacy)、純稚的資料類(Data Class)等,通過搬移方法(Move Method)或者搬移欄位(Move Field),可以讓某些程式碼待在更合適的位置。因此,Martin Fowler在《重構》一書中指出,“搬移方法”是重構理論的支柱(Moving methods is the bread and butter of refactoring

.),可見該重構的重要性。

       下面舉一個例子來加以說明:

【重構例項】

       在某銀行系統中包含一個銀行賬戶類BankAccount和賬戶利息類AccountInterest,重構之前的程式碼如下:

package sunny.refactoring.two.before;

class BankAccount {
	private int accountAge;
	private int creditScore;
	private AccountInterest accountInterest;
	
	public BankAccount(int accountAge, int creditScore, AccountInterest accountInterest) {
		this.accountAge = accountAge;
		this.creditScore = creditScore;
		this.accountInterest = accountInterest;
	}
	
	public int getAccountAge() {
		return this.accountAge;
	}
	
	public int getCreditScore() {
		return this.creditScore;
	}
	
	public AccountInterest getAccountInterest() {
		return this.accountInterest;
	}
	
	public double calculateInterestRate() {
		if (this.creditScore > 800) {
			return 0.02;
		}
		
		if (this.accountAge > 10) {
			return 0.03;
		}
		
		return 0.05;
	}
}

class AccountInterest {
	private BankAccount account;
	
	public AccountInterest(BankAccount account) {
		this.account = account;
	}
	
	public BankAccount getAccount() {
		return this.account;
	}
	
	public double getInterestRate() {
		return account.calculateInterestRate();
	}
	
	public boolean isIntroductoryRate() {
		return (account.calculateInterestRate() < 0.05);
	}
}

       在上述程式碼中,很明顯,AccountInterest使用calculateInterestRate()方法更為頻繁,它更希望得到該方法,因此,我們需要成人之美,微笑,將calculateInterestRate()方法從BankAccount類搬移到AccountInterest類中。

       重構之後的程式碼如下:

package sunny.refactoring.two.after;

class BankAccount {
	private int accountAge;
	private int creditScore;
	private AccountInterest accountInterest;
	
	public BankAccount(int accountAge, int creditScore, AccountInterest accountInterest) {
		this.accountAge = accountAge;
		this.creditScore = creditScore;
		this.accountInterest = accountInterest;
	}
	
	public int getAccountAge() {
		return this.accountAge;
	}
	
	public int getCreditScore() {
		return this.creditScore;
	}
	
	public AccountInterest getAccountInterest() {
		return this.accountInterest;
	}
}

class AccountInterest {
	private BankAccount account;
	
	public AccountInterest(BankAccount account) {
		this.account = account;
	}
	
	public BankAccount getAccount() {
		return this.account;
	}
	
	public double getInterestRate() {
		return calculateInterestRate();
	}
	
	public boolean isIntroductoryRate() {
		return (calculateInterestRate() < 0.05);
	}
	
	//將calculateInterestRate()方法從BankAccount類搬移到AccountInterest類
    public double calculateInterestRate() {
		if (account.getCreditScore() > 800) {
			return 0.02;
		}
		
		if (account.getAccountAge() > 10) {
			return 0.03;
		}
		
		return 0.05;
	}
}

       通過重構,BankAccount類更加符合單一職責原則,它負責儲存銀行賬戶資訊,而對賬戶的操作(例如計算利息等)方法則轉移到其他經常使用且適合它的類中,這樣讓程式碼變得更加合理,也有助降低類之間的耦合度,增強程式碼的可擴充套件性和可維護性。

重構心得

       搬移方法是一種非常實用的重構手段。在本例項中,我們是將方法搬移到被呼叫次數最多的那個類中,在實際程式碼重構過程中,還有很多其他涉及到需要搬移方法的場景。

       有一種程式碼味道叫做依戀情結(Feature Envy),指的是一個方法對某個類的興趣高過對自己所處類的興趣,例如某個方法需要訪問另一個類中大量的資料成員,此時,也非常適合使用搬移方法重構。讓方法能夠前往它的夢想王國不是件很有意義的事情嗎?如果一個方法用到了多個類的功能,那麼這個方法放在哪個類中更合適呢?常用的做法是判斷哪個類擁有最多被此方法使用的資料,然後將這個方法和那些資料放在一起。在這種情況下,搬移方法的時機不是判斷它被哪個類呼叫更多,而是判斷它更需要哪個類提供的資料,這跟上面的重構例項有些區別。

       有時候搬移方法時,還需要將只被這個(或這些)方法使用的資料和其他方法一起搬移,需要認真檢查方法中使用到的屬性(欄位)和其他方法,必要時同時執行搬移欄位重構(Move Field)。

       如果搬移後的方法需要訪問原有類中的屬性或者方法,可以將原有類的物件作為引數傳入新類,在這個過程中可能還需要修改原有類中某些屬性或方法的可見性,畢竟它們已經分家了,原有的一些私有的東西是不能再直接訪問的。

       如果在繼承結構中,需要搬移的方法宣告在抽象層中,此時要慎重使用本重構,如果太麻煩建議就不要搬移了,免得引入太多錯誤,畢竟我們要保證抽象層的相對穩定性。

       如果有需要,可以為搬移後的函式重新取一個名字,以提高程式碼的可讀性。

       如果原有類還需要用到這個已經搬移走的方法,可以通過在原有類中提供一個委託方法的形式來實現,例如method() { TargetClass tc = new TargetClass (); tc.method();},如果原有類中的多個方法(當然也不能太多,否則就沒有必要搬走了)需要使用已搬移走的方法,也可以考慮在原有類中增加一個目標類(搬移之後所在類)的物件引用,通過該引用來呼叫搬走的方法。

       當一個類的職責太多時,為了分解類的職責,也可能需要將一些職責搬移到其他類中,此時也需要執行搬移方法重構。這樣做,系統將更加滿足單一職責原則,有利於提高程式碼的可複用性和易理解性。

       雖然搬移方法是一種簡單的重構手段,但是在實際使用中很多人經常會遇到一個問題,如何確定和尋找重構時機?也就是不知道什麼時候該用搬移方法來進行重構,特別是當系統較為複雜,類和方法個數非常多時,要準確識別出重構時機並不是一件簡單的事情。疑問

       希臘馬其頓大學(University of Macedonia)學者Nikolaos Tsantalis和Alexander Chatzigeorgiou在搬移方法重構時機識別上開展了相關研究,並在軟體工程國際頂級期刊IEEE Transactions on Software Engineering(PS:該期刊是Sunny的2014年目標之一,微笑,加油!)上發表了他們的研究成果,在他們的重構時機識別過程中,使用了Jaccard距離(Jaccard distance)來計算一個待搬移的實體(方法或者屬性)和一個類的距離,distance(A, B) = 1 – (|A∩B|/|A∪B|) = 1 – similarity(A,B),將實體搬移到距離最小的類中,他們實現了一個名為JDeodorant的Eclipse外掛來實現重構時機識別的半自動化。JDeodorant介紹URL:http://java.uom.gr/~jdeodorant/;JDeodorant安裝URL:http://marketplace.eclipse.org/content/jdeodorant