#
范例(Examples)
下面是Account class的部分代碼:
class Account...
private AccountType _type;
private double _interestRate;
double interestForAmount_days(double amount, int days) {
return _interestRate * amount * days / 365;
}
我想把表示利率的_interestRate搬移到AccountType class去。目前已有數個函數引用了它,interestForAmount_days()就是其一。下一步我要在AccountType中建立_interestRate field以及相應的訪問函數:
class AccountType...
private double _interestRate;
void setInterestRate(double arg) {
_interestRate = arg;
}
double getInterestRate() {
return _interestRate;
}
這時候我可以編譯新的 AccountType class。
現在,我需要讓Account class中訪問_interestRate
field的函數轉而使用AccountType對象,然后刪除Account class中的_interestRate
field。我必須刪除source field,才能保證其訪問函數的確改變了操作對象,因為編譯器會幫我指出未正確獲得修改的函數。
private double _interestRate;
double interestForAmount_days(double amount, int days) {
return _type.getInterestRate() * amount * days / 365;
}
作法(Mechanics)
- 如果field的屬性是public,首先使用Encapsulate Field(206)將它封裝起來。
- ==》如果你有可能移動那些頻繁訪問該field的函數,或如果有許多函數訪問某個field,先使用Self Encapsulate Field(171)也許會有幫助。
- 編譯,測試。
- 在target class中建立與source field相同的field,并同時建立相應的設值/取值(setting/getting)函數。
- 編譯target class。
- 決定如何在source object中引用target object。
- ==》一個現成的field或method可以助你得到target
object。如果沒有,就看能否輕易建立這樣一個函數。如果還不行,就得在source class中新建一個field來存放target
object。這可能是個永久性修改,但你也可以暫不公開它,因為后續重構可能會把這個新建field除掉。
- 刪除source field。
- 將所有[對source field的引用]替換為[對target適當函數的調用]。
- ==》如果是[讀取]該變量,就把[對source field的引用]替換為[對target取值函數(getter)的調用];如果是[賦值]該變量,就把[對source field的引用]替換成[對設值函數(setter)的調用]。
- ==》如果source field不是private,就必須在source class的所有subclasses中查找source field的引用點,并進行相應替換。
- 編譯,測試。
動機(Motivation)
如果我發現,對于一個field(值域),在其所駐class之外的另一個class中有更多函數使用了它,我就會考慮搬移這個field。上述所謂[使
用]可能通過設值/取值(setting/getting)函數間接進行。我也可能移動該field的用戶(某函數),這取決于是否需要保持接口不受變
化。如果這些函數看上去很合適待在原地,我就選擇搬移field。
使用Extract Class(149)時,我也可能需要搬移field。此時我會先搬移field,然后再搬移函數。
你的程序中,某個field(值域)被其所駐class之外的另一個class更多地用到。
在target class建立一個new field,修改source field的所有用戶,令它們改用new field。

范例(Examples)
我用一個表示[帳戶]的account class來說明這項重構:
class Account...
double overdraftCharge() { //透支金計費,它和其他class的關系似乎比較密切。
if(_type.isPremium()) {
double result = 10;
if(_daysOverdrawn > 7)
result += (_daysOverdrawn -7) * 0.85;
return result;
}
else return _daysOverdrawn * 1.75;
}
double bankCharge() {
double result = 4.5;
if(_daysOverdrawn > 0) result += overdraftCh你arge();
return result;
}
private AccountType _type;
private int _daysOverdrawn;
假設有數種新帳戶,每一種都有自己的[透支金計費規則]。所以我希望將overdraftCharge()搬移到AccountType class去。
第一步要做的是:觀察被overdraftCharge()使用的每一特性(features),考慮是否值得將它們與overdraftCharge()一起移動。此例之中我需要讓_daysOverdrawn值域留在Account class,因為其值會隨不同種類的帳戶而變化。然后,我將overdraftCharge()函數碼拷貝到AccountType中,并做相應調整。
class AccountType...
double overdraftCharge(int daysOverdrawn) {
if(isPremium()) {
double result = 10;
if(daysOverdrawn >7)
result += (daysOverdrawn - 7) * 0.85;
return result;
}
else return daysOverdrawn * 1.75;
}
在這個例子中,[調整]的意思是:(1)對于[使用AccountType特性]的語句,去掉──type;(2)想辦法得到依舊需要的Account
class特性。當我需要使用source class特性,我有四種選擇:(1)將這個特性也移到target
class;(2)建立或使用一個從target class到source的引用(指涉)關系;(3)將source
object當作參數傳給target method;(4)如果所需特性是個變量,將它當作參數傳給target method。
本例中我將_daysOverdrawn變量作為參數傳給target method(上述(4))。
調整target method使之通過編譯,而后我就可以將source method的函數本體替換為一個簡單的委托動作(delegation),然后編譯并測試:
class Account...
double overdraftCharge() {
return _type.overdraftCharge(_daysOverdrawn);
}
我可以保留代碼如今的樣子,也可以刪除source method。如果決定刪除,就得找出source method的所有調用者,并將這些調用重新定向,改調用Account的bankCharge():
bankCharge():
class Account...
double bankCharge() {
double result = 4.5;
if(_daysOverdrawn > 0)
result += _type.overdraftCharge(_daysOverdrawn);
return result;
}
所有調用點都修改完畢后,我就可以刪除source
method在Account中的聲明了。我可以在每次刪除之后編譯并測試,也可以一次性批量完成。如果被搬移的函數不是private,我還需要檢查其
他classes是否使用了這個函數。在強型(strongly typed)語言中,刪除source
method聲明式后,編譯器幫我發現任何遺漏。
此例之中被移函數只取用(指涉)一個值域,所以我只需將這個值域作為參數傳給target method就行了。如果被移函數調用了Account中的另一個函數,我就不能這么簡單地處理。這種情況下我必須將source object傳遞給target method:
class AccountType...
double overdraftCharge(Account account) {
if(isPremium()) {
double result = 10;
if(account.getDaysOverdrawn() >7)
result += (account.getdaysOverdrawn() - 7) * 0.85;
return result;
}
else return daysOverdrawn * 1.75;
}
如果我需要source class的多個特性,那么我也會將source object傳遞給target method。不過如果target
method需要太多source class特性,就得進一步重構。通常這種情況下我會分解target
method,并將其中一部分移回source class。
作法(Mechanics)
- 檢查source class定義之source method所使用的一切特性(features),考慮它們是否也該被搬移。
- ==》如果某個特性只被你打算搬移的那個函數用到,你應該將它一并搬移。如果另有其他函數使用了這個特性,你可以考慮將使用該特性的所有函數全部一并搬移。有時侯搬移一組函數比逐一搬移簡單些。
- 檢查source class的subclass和superclass,看看是否有該函數的其他聲明。
- ==》如果出現其他聲明,你或許無法進行搬移,除非target class也同樣表現出多態性(polymorphism)。
- 在target class中聲明這個函數。
- ==》你可以為此函數選擇一個新名稱 -- 對target class更有意義的名稱。
- 將source method的代碼拷貝到target method中。調整后者,使其能在新家中正常運行。
- ==》如果target method使用了source特性,你得決定如何從target method引用source
object。如果target class中沒有相應的引用機制,就把source object
reference當作參數,轉給新建立的target method。
- ==》如果source method包含異常處理式(exception handler),你得判斷邏輯上應該由哪個class來處理這一異常。如果應該由source class來負責,就把異常處理式留在原地。
- 編譯target class。
- 決定如何從source正確引用target object。
- ==》可能會有一個現成的值域或函數幫助你取得target
object。如果沒有,就看能否輕松建立一個這樣的函數。如果還是不行,你得在source class中新建一個新值域來保存target
object。這可能是一個永久性修改,但你也可以讓它保持暫時的地位,因為后繼的其他重構項目可能會把這個新建值域去掉。
- 修改source method,使之成為一個delegating method(純委托函數)。
- 編譯,測試。
- 決定[刪除source method]或將它當作一個delegating method保留下來。
- ==》如果你經常要在source object中引用target method,那么將source method作為delegating method保留下來會比較簡單。
- 如果你移除source method,請將source class中對source method的所有引用動作,替換為[對target method的引用動作]。
- ==》你可以每修改一個引用點就編譯并測試一次。也可以通過一次[查找/替換]改掉所有引用點,這通常簡單一些。
- 編譯,測試。
動機(Motivation)
[函數搬移]是重構理論的支柱。如果一個class有太多行為,或如果一個class與另一個class有太多合作而形成高度耦合(highly
coupled),我就會搬移函數。通過這種手段,我可以使系統中的classes更簡單,這些classes最終也將更干凈利落地實現系統交付的任務。
常常我會瀏覽class的所有函數,從中尋找這樣的函數:使用另一個對象的次數比使用自己所駐對象的次數還多。一旦我移動了一些值域,就該做這樣的檢查。
一旦發現[有可能被我搬移]的函數,我就會觀察調用它的那一端、它調用的那一端,以及繼承體系中它的任何一個重定義函數。然后,我會根據[這個函數與哪個
對象的交流比較多],決定其移動路徑。
這往往不是一個容易做出的決定。如果不能肯定是否應該移動一個函數,我就會繼續觀察其他函數。移動其他函數往往會讓這項決定變得容易一些。有時候,即使你
移動了其他函數,還是很難對眼下這個函數做出決定。其實這也沒什么大不了的。如果真的很難做出決定,那么或許[移動這個函數與否]并不那么重要。所以,我
會憑本能去做,反正以后總是可以修改的。
你的程序中,有個函數與其所駐class之外的另一個class進行更多交流:調用后者,或被后者調用。
在該函數最常引用(指涉)的class中建立一個有著類似行為的新函數。將舊函數變成一個單純的委托函數(delegating method),或者將舊函數完全移除。
作法(Mechanics)
- 準備好你的另一個(替換用)算法,讓它通過編譯。
- 針對現有測試,執行上述的新算法。如果結果于原本結果相同,重構結束。
- 如果測試結果不同于原先,在測試和調試過程中,以舊算法為比較參照標準。
- ==》對于每個test case(測試用例),分別以新舊兩種算法執行,并觀察兩者結果是否相同。這可以幫助你看到哪一個test case出現麻煩,以及出現了怎么的麻煩。
動機(Motivation)
有時侯你會想要修改原先的算法,讓它去做一件與原先動作略有差異的事。這時候你也可以先把原先的算法替換為一個較易修改的算法,這樣后續的修改會輕松許多。
使用這項重構手法之前,請先確定自己已經盡可能分解了原先函數。替換一個巨大而復雜的算法是非常困難的,只有先將它分解為較簡單的小型函數,然后你才能很有把握地進行算法替換工作。
|