關於 Replace Temp With Query

這個I disagree系列裏面我準備把所有在工作中技術上的爭執記錄下來。也有立此存照的意思。也許再過幾年,回頭一看,會自己bs自己一把呢。

今天要記錄的,是一個關於martin的refactoring那本書裏提到的"Replace Temp With Query"的重構技術。

事情是這樣的。在和同事pair的時候,對他頻繁使用的這個重構不太同意。搞得同事很不爽。很不好意思的是,我並沒有讀過這本書,所以對這個重構模式事先是一無所知。這就更加讓同事不爽。

當他無奈地指出“我用的是Martin推薦的”的時候,我當時還真有點不敢相信。於是我馬上把書拿了過來,讀了一遍Replace Temp With Query。

讀過之後,我的感覺是,只能部分同意Martin,而對同事對這個重構的使用仍然是無法苟同。

先說對Martin的保留意見:
馬丁說這個技術對把大的函數切割成小函數很有作用。這個我是同意的。有時候,當使用eclipse的"extract method"的時候,IDE會給這個新函數提示出七八個參數。這個時候,如果某些參數可以用query代替,自然會減少參數的個數。
不能同意的有:
1。馬丁說在作extract method之前,要儘量多地做replace temp with query。而我認爲,有那麼兩三個參數沒什麼不好。用參數傳遞比用query來隱含地傳遞信息有靈活性和清晰性上的好處。通過參數傳遞相比於通過query傳遞,有點像dependency injection vs. service locator。
所以,我認爲只有在發現某個temp真的影響了extract method,纔去做replace temp比較好。
一個類裏面如果到處充斥着各種各樣的query函數,在我看來也是味道不好聞。

2。馬丁對這個重構的侷限性和危險性只是一筆帶過。實際上,我感覺這個重構的適用範圍極小。在一個臨時變量的值會變化的時候,或者後面會發生副作用的時候,當然不能直接用這個重構,這點martin也說了。但是,martin舉的例子都是query返回一個原始類型。實際使用中更多的是一個query需要返回一個對象。
而java作爲一個引用不透明的語言,任何對引用類型局部變量的replace with query動作,理論上都不是安全的。

舉個例子:
[code]
interface Profile {
Account getPrimaryAccount();
}
interface Account {
...
Contribution getPreTaxContribution();
}
interface Contribution {
Balance getBalance();
}

void f(Profile profile, Service service) {
Account acct = profile.getPrimaryAccount();
Contribution contrib = acct.getPreTaxContribution();
Balance balance = contrib.getBalance();
...
if(contrib.isMandatory()){...}
...
service.setContribution(..., contrib);
...
if(balance.isGood()) { ...}
...
service.transferBalance(..., balance);
...
}
[/code]

我們能夠簡簡單單地把contrib和balance這兩個temp變成下面的query麼?
[code]
Contribution contribution(Profile profile) {
return profile.getPrimaryAccount().getPreTaxContribution();
}
Balance balance(Profile profile) {
return contribution(profile).getBalance();
}
void f(Profile profile, Service service) {
...
if(contribution(profile).isMandatory()){...}
...
service.setContribution(..., contribution(profile));
...
if(balance(profile).isGood()) { ...}
...
service.transferBalance(..., balance(profile));
...
}

[/code]

Contribution, Profile, Account這些東西都是接口。而在接口的javadoc上沒有明確表示getContribution(),getAccount(), getBalance()這些方法都必然一直返回一個對象的引用的時候,我是不敢這麼做的。

要知道,我們要重構的是一個架構很凌亂的系統裏面的幾十個大函數(一百行以上)中的一個,這個系統凌亂到沒有一個人清楚知道總體到底是怎麼回事,跟蹤查找一個bug可能要按F3或者"Reference - Hierarchy"二十多次。
更討厭的是,要重構的函數沒有很好的單元測試。(當然,要是有良好的單元測試,也不會寫成這個德性了)

對這種系統,任何不能從理論上證明等價的重構都是危險的。也許,這麼重構了之後,不會馬上發現問題,但是,它會在我幼小的心靈裏面留下的“我那個重構沒有問題吧???”的陰影的。


google了一下"Replace Temp With Query",發現馬丁有這麼一個補充聲明:

[quote]
Paul Haahr pointed out that you can't do this refactoring if the code in between the the assignment to the temp and the use of the temp changes the value of the expression that calculates the temp. In these cases the code is using the temp to snapshot the value of the temp when it's assigned. The name of the temp should convey this fact (and you should change the name if it doesn't).

He also pointed out that it is easy to forget that creating a reference object is a side effect, while creating a value object isn't.
[/quote]
這段話雖然語焉不詳,但是它還是呼應了我對這個重構的保留:"create a reference object"也是一個side effect。而麻煩的是,對一個接口裏面的getSomething(),我基本無法知道這裏面有沒有一個"create a reference object"。(我的同事對這點不是很同意我的,他會說,我們目前有的兩個實現都沒有create a reference object。而我在面對一個接口的時候,更傾向於不去看現有的實現類裏面到底如何實現的,我只在乎接口的spec,除非spec說這裏不允許create a new reference object,我是寧可不做任何假設的。做個proxy之類,把返回值封裝一下再返回的這種技術對我來說不是很不可思議的。)


下面再說我對同事的對這個重構的使用方法的不同意見。
1。同事基本上就是上來就能replace的就replace。有一個函數居然最後被重構成:
[code]
private Account[] accts;
Account[] filteredAccounts(String type){
ArrayList ret = new ArrayList();
for(...) {
if(type.equals(accts[i].getType())
ret.add(accts[i]);
}
return ret.toArray(new Account[ret.size()]);
}
void f(String type) {
for(int i=0; i<filteredAccounts(type).length; i++){
if(filteredAccounts(type)[i].getBalance()<0) {
filteredAccounts(type)[i].setValid(false);
}
}
}
[/code]
哎,就算我裝作看不見循環裏面重複的子循環,或者捏着鼻子念着“不要過早優化”的咒語,這代碼閱讀起來,調試起來,也不如下面這個簡單明瞭吧?局部變量真的這麼可怕?
[code]
void f(String type) {
Account[] found = filteredAccounts(type);
for(int i=0; i<found.length; i++){
if(found[i].getBalance()<0) {
found[i].setValid(false);
}
}
}
[/code]
2。在我表達了我對效率和副作用的擔心之後,同事很富有團隊精神地聲明瞭幾個局部變量來避免這個問題
[code]
private Account acct;
private Contribution contrib;
private Balance balance;
private void cleanStates(){
acct = null;
contrib = null;
balance = null;
}
Account account(Profile profile){
if(acct==null) acct = profile.getAccount();
return acct;
}
Contribution contribution(Profile profile) {
if(contrib==null) contrib = account(profile).getPreTaxContribution();
return contrib;
}
Balance balance(Profile profile) {
if(balance==null) balance = contribution(profile).getBalance();
return balance;
}
void f(Profile profile, Service service) {
cleanStates();
...
}
[/code]

我很不好意思地跟同事說,相比於前一個,我更不喜歡這個方案。兩點問題:
1。副作用。我很討厭引入可變的對象狀態。它帶來更大的bug機率,還有同步問題。
2。複雜性。代碼比原來更多,更復雜了。本來是局部變量的現在變成了全局變量。而我記得從開始學寫程序開始,都是局部變量優先於全局變量的。


其實,歸根結底,我想跟同事說的是:
replace with query還是小心使用爲上。我們先看看有沒有必要把這些東西變成query好不好?如果這幾個temp真的影響了重構,再研究怎麼處置不行麼?我在自己的代碼重構中似乎還真是很少發現需要使用replace with query的。

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