級(jí)別: 初級(jí)
David Gallardo, 獨(dú)立軟件顧問和作家
2003 年 11 月 10 日
Eclipse 提供了一組強(qiáng)大的自動(dòng)重構(gòu)(refactoring)功能,這些功能穿插在其他功能當(dāng)中,使您能夠重命名 Java元素,移動(dòng)類和包,從具體的類中創(chuàng)建接口,將嵌套的類變成頂級(jí)類,以及從舊方法的代碼片斷中析取出新的方法。您熟悉了 Eclipse 的重構(gòu)工具之后,就掌握了一種提高生產(chǎn)率的好方法。本文綜覽Eclipse 的重構(gòu)特性,并通過例子闡明了使用這些特性的方法與原因。
為什么重構(gòu)?
重構(gòu)是指在不改變程序功能的前提下改變其結(jié)構(gòu)。重構(gòu)是一項(xiàng)功能強(qiáng)大的技術(shù),但是執(zhí)行起來需要倍加小心才行。主要的危險(xiǎn)在于可能在不經(jīng)意中引入一些錯(cuò)誤,尤其是在進(jìn)行手工重構(gòu)的時(shí)候更是如此。這種危險(xiǎn)引發(fā)了對(duì)重構(gòu)技術(shù)的普遍批評(píng):當(dāng)代碼不會(huì)崩潰的時(shí)候?yàn)槭裁匆薷乃兀?
您需要進(jìn)行代碼重構(gòu)的原因可能有以下幾個(gè):傳說中的第一個(gè)原因是:需要繼承為某個(gè)古老產(chǎn)品而開發(fā)的年代久遠(yuǎn)的代碼,或者突然碰到這些代碼。最初的開發(fā)團(tuán)隊(duì)已經(jīng)不在了。我們必須創(chuàng)建增加了新特性的新版本軟件,但是這些代碼已經(jīng)無法理解了。新的開發(fā)隊(duì)伍夜以繼日地工作,破譯代碼然后映射代碼,經(jīng)過大量的規(guī)劃與設(shè)計(jì)之后,人們將這些代碼分割成碎片。歷經(jīng)重重磨難之后,所有這些東西都按照新版本的要求歸位了。這是英雄般的重構(gòu)故事,幾乎沒有人能在經(jīng)歷了這些之后活著講述這樣的故事。
還有一種現(xiàn)實(shí)一些的情況是項(xiàng)目中加入了新的需求,需要對(duì)設(shè)計(jì)進(jìn)行修改。至于是因?yàn)樵谧畛醯囊?guī)劃過程中失察,還是由于采用了迭代式的開發(fā)過程(比如敏捷開發(fā),或者是測(cè)試驅(qū)動(dòng)的開發(fā))而在開發(fā)過程中有意引入需求,這兩者并沒有實(shí)質(zhì)性的區(qū)別。這樣的重構(gòu)的規(guī)模要小得多,其內(nèi)容一般涉及通過引入接口或者抽象類來更改類的繼承關(guān)系,以及對(duì)類進(jìn)行分割和重新組織,等等。
重構(gòu)的最后一個(gè)原因是,當(dāng)存在可用的自動(dòng)重構(gòu)工具時(shí),可以有一個(gè)用來預(yù)先生成代碼的快捷方式——就好比在您無法確定如何拼寫某個(gè)單詞的時(shí)候,可以用某種拼寫檢查工具輸入這個(gè)單詞。比如說,您可以用這種平淡無奇的重構(gòu)方法生成 getter 和 setter 方法,一旦熟悉了這樣的工具,它就可以為您節(jié)省很多的時(shí)間。
Eclipse 的重構(gòu)工具無意進(jìn)行英雄級(jí)的重構(gòu)——適合這種規(guī)模的工具幾乎沒有——但是不論是否用到敏捷開發(fā)技術(shù),Eclipse 的工具對(duì)于一般程序員修改代碼的工作都具有無法衡量的價(jià)值。畢竟任何復(fù)雜的操作只要能夠自動(dòng)進(jìn)行,就可以不那么煩悶了。只要您知道 Eclipse 實(shí)現(xiàn)了什么樣的重構(gòu)工具,并理解了它們的適用情況,您的生產(chǎn)力就會(huì)得到極大的提高。
要降低對(duì)代碼造成破壞的風(fēng)險(xiǎn),有兩種重要的方法。第一種方法是對(duì)代碼進(jìn)行一套完全徹底的單元測(cè)試:在重構(gòu)之前和之后都必須通過這樣的測(cè)試。第二種方法是使用自動(dòng)化的工具來進(jìn)行重構(gòu),比如說 Eclipse 的重構(gòu)特性。
將徹底的測(cè)試與自動(dòng)化重構(gòu)結(jié)合起來就會(huì)更加有效了,這樣重構(gòu)也就從一種神秘的藝術(shù)變成了有用的日常工具。為了增加新的功能或者改進(jìn)代碼的可維護(hù)性,我們可以在不影響原有代碼功能的基礎(chǔ)上迅速且安全地改變其結(jié)構(gòu)。這種能力會(huì)對(duì)您設(shè)計(jì)和開發(fā)代碼的方式產(chǎn)生極大的影響,即便是您沒有將其結(jié)合到正式的敏捷方法中也沒有關(guān)系。
Eclipse 中重構(gòu)的類型
Eclipse 的重構(gòu)工具可以分為三大類(下面的順序也就是這些工具在 Refactoring 菜單中出現(xiàn)的順序):
- 對(duì)代碼進(jìn)行重命名以及改變代碼的物理結(jié)構(gòu),包括對(duì)屬性、變量、類以及接口重新命名,還有移動(dòng)包和類等。
- 改變類一級(jí)的代碼邏輯結(jié)構(gòu),包括將匿名類轉(zhuǎn)變?yōu)榍短最?,將嵌套類轉(zhuǎn)變?yōu)轫敿?jí)類、根據(jù)具體的類創(chuàng)建接口,以及從一個(gè)類中將方法或者屬性移到子類或者父類中。
- 改變一個(gè)類內(nèi)部的代碼,包括將局部變量變成類的屬性、將某個(gè)方法中選中部分的代碼變成一個(gè)獨(dú)立的方法、以及為屬性生成 getter 和 setter 方法。
還有幾個(gè)重構(gòu)工具并不能完全歸入這三個(gè)種類,特別是 Change Method Signature,不過在本文中還是將這個(gè)工具歸入第三類。除了這種例外情況以外,本文下面幾節(jié)都是按照上面的順序來討論 Eclipse 重構(gòu)工具的。
物理重組與重命名
顯然,您即便沒有特別的工具,也可以在文件系統(tǒng)中重命名文件或者是移動(dòng)文件,但是如果操作對(duì)象是 Java 源代碼文件,您就需要編輯很多文件,更新其中的 import 或 package 語句。與此類似,用某種文本編輯器的搜索與替換功能也可以很容易地給類、方法和變量重新命名,但是這樣做的時(shí)候必須十分小心,因?yàn)椴煌念惪赡芫哂忻Q相似的方法或者變量;要是從頭到尾檢查項(xiàng)目中所有的文件,來保證每個(gè)東西的標(biāo)識(shí)和修改的正確性,那可真夠乏味的。
Eclipse 的 Rename 和 Move 工具能夠十分聰明地在整個(gè)項(xiàng)目中完成這樣的修改,而不需要用戶的干涉。這是因?yàn)?Eclipse 可以理解代碼的語義,從而能夠識(shí)別出對(duì)某個(gè)特定方法、變量或者類名稱的引用。簡(jiǎn)化這一任務(wù)有助于確保方法、變量和類的名稱能夠清晰地指示其用途。
我們經(jīng)??梢园l(fā)現(xiàn)代碼的名字不恰當(dāng)或者令人容易誤解,這是因?yàn)榇a與最初設(shè)計(jì)的功能有所不同。比方說,某個(gè)用來在文件中查找特定單詞的程序也許會(huì)擴(kuò)展為在 Web 頁面中通過 URL 獲取 InputStream 的操作。如果這一輸入流最初叫做 file ,那么就應(yīng)該修改它的名字,以便能反映其新增的更加一般的特性,比方說 sourceStream 。開發(fā)人員經(jīng)常無法成功地修改這些名稱,因?yàn)檫@個(gè)過程是十分混亂和乏味的。這當(dāng)然也會(huì)把下一個(gè)不得不對(duì)這些類進(jìn)行操作的開發(fā)人員弄糊涂。
要對(duì)某個(gè) Java 元素進(jìn)行重命名,只需要簡(jiǎn)單地從 Package Explorer 視圖中點(diǎn)擊這個(gè)元素,或者從Java 源代碼文件中選中這個(gè)元素,然后選擇菜單項(xiàng) Refactor > Rename。在對(duì)話框中輸入新的名稱,然后選擇是否需要 Eclipse 也改變對(duì)這個(gè)名稱的引用。實(shí)際顯示出來的確切內(nèi)容與您所選元素的類型有關(guān)。比方說,如果選擇的屬性具有 getter 和 setter 方法,那么也就可以同時(shí)更新這些方法的名稱,以反映新的屬性。圖1顯示了一個(gè)簡(jiǎn)單的例子。
圖 1. 重命名一個(gè)局部變量
就像所有的 Eclipse 重構(gòu)操作一樣,當(dāng)您指定了全部用來執(zhí)行重構(gòu)的必要信息之后,您就可以點(diǎn)擊 Preview 按鈕,然后在一個(gè)對(duì)話框中對(duì)比 Eclipse 打算進(jìn)行哪些變更,您可以分別否決或者確認(rèn)每一個(gè)受到影響的文件中的每一項(xiàng)變更。如果您對(duì)于 Eclipse 正確執(zhí)行變更的能力有信心的話,您可以只按下 OK按鈕。顯然,如果您不確定重構(gòu)到底做了什么事情,您就會(huì)想先預(yù)覽一下,但是對(duì)于 Rename 和 Move 這樣簡(jiǎn)單的重構(gòu)而言,通常沒有必要預(yù)覽。
Move 操作與 Rename 十分相似:您選擇某個(gè) Java 元素(通常是一個(gè)類),為其指定一個(gè)新位置,并定義是否需要更新引用。然后,您可以選擇 Preview檢查變更情況,或者選擇 OK 立即執(zhí)行重構(gòu),如圖2所示。
圖 2. 將類從一個(gè)包移到另一個(gè)包
在某些平臺(tái)上(特別是 Windows),您還可以在 Package Explorer 視圖中通過簡(jiǎn)單拖放的方法將類從一個(gè)包或者文件夾中移到另一個(gè)包或文件夾中。所有的引用都會(huì)自動(dòng)更新。
重新定義類的關(guān)系
Eclipse 中有大量的重構(gòu)工具,使您能夠自動(dòng)改變類的關(guān)系。這些重構(gòu)工具并沒有 Eclipse 提供的其他工具那么常用,但是很有價(jià)值,因?yàn)樗鼈兡軌驁?zhí)行非常復(fù)雜的任務(wù)。可以說,當(dāng)它們用得上的時(shí)候,就會(huì)非常有用。
提升匿名類與嵌套類
Convert Anonymous Class(轉(zhuǎn)換匿名類)和 Convert Nested Type(轉(zhuǎn)換嵌套類)這兩種重構(gòu)方法比較相似,它們都將某個(gè)類從其當(dāng)前范圍移動(dòng)到包含這個(gè)類的范圍上。
匿名類是一種語法速寫標(biāo)記,使您能夠在需要實(shí)現(xiàn)某個(gè)抽象類或者接口的地方創(chuàng)建一個(gè)類的實(shí)例,而不需要顯式提供類的名稱。比如在創(chuàng)建用戶界面中的監(jiān)聽器時(shí),就經(jīng)常用到匿名類。在清單1中,假設(shè) Bag 是在其他地方定義的一個(gè)接口,其中聲明了兩個(gè)方法, get() 和 set() 。 清單 1. Bag 類
public class BagExample
{
void processMessage(String msg)
{
Bag bag = new Bag()
{
Object o;
public Object get()
{
return o;
}
public void set(Object o)
{
this.o = o;
}
};
bag.set(msg);
MessagePipe pipe = new MessagePipe();
pipe.send(bag);
}
}
|
當(dāng)匿名類變得很大,其中的代碼難以閱讀的時(shí)候,您就應(yīng)該考慮將這個(gè)匿名類變成嚴(yán)格意義上的類;為了保持封裝性(換句話說,就是將它隱藏起來,使得不必知道它的外部類不知道它),您應(yīng)該將其變成嵌套類,而不是頂級(jí)類。您可以在這個(gè)匿名類的內(nèi)部點(diǎn)擊,然后選擇 Refactor > Convert Anonymous Class to Nested 就可以了。當(dāng)出現(xiàn)確認(rèn)對(duì)話框的時(shí)候,為這個(gè)類輸入名稱,比如 BagImpl ,然后選擇 Preview或者 OK。這樣,代碼就變成了如清單2所示的情形。 清單 2. 經(jīng)過重構(gòu)的 Bag 類
public class BagExample
{
private final class BagImpl implements Bag
{
Object o;
public Object get()
{
return o;
}
public void set(Object o)
{
this.o = o;
}
}
void processMessage(String msg)
{
Bag bag = new BagImpl();
bag.set(msg);
MessagePipe pipe = new MessagePipe();
pipe.send(bag);
}
}
|
當(dāng)您想讓其他的類使用某個(gè)嵌套類時(shí),Convert Nested Type to Top Level 就很有用了。比方說,您可以在一個(gè)類中使用值對(duì)象,就像上面的 BagImpl 類那樣。如果您后來又決定應(yīng)該在多個(gè)類之間共享這個(gè)數(shù)據(jù),那么重構(gòu)操作就能從這個(gè)嵌套類中創(chuàng)建新的類文件。您可以在源代碼文件中高亮選中類名稱(或者在 Outline 視圖中點(diǎn)擊類的名稱),然后選擇 Refactor > Convert Nested Type to Top Level,這樣就實(shí)現(xiàn)了重構(gòu)。
這種重構(gòu)要求您為裝入實(shí)例提供一個(gè)名字。重構(gòu)工具也會(huì)提供建議的名稱,比如 example ,您可以接受這個(gè)名字。這個(gè)名字的意思過一會(huì)兒就清楚了。點(diǎn)擊 OK 之后,外層類 BagExample 就會(huì)變成清單3所示的樣子。 清單 3. 經(jīng)過重構(gòu)的 Bag 類
public class BagExample
{
void processMessage(String msg)
{
Bag bag = new BagImpl(this);
bag.set(msg);
MessagePipe pipe = new MessagePipe();
pipe.send(bag);
}
}
|
請(qǐng)注意,當(dāng)一個(gè)類是嵌套類的時(shí)候,它可以訪問其外層類的成員。為了保留這種功能,重構(gòu)過程將一個(gè)裝入類 BagExample 的實(shí)例放在前面那個(gè)嵌套類中。這就是之前要求您輸入名稱的實(shí)例變量。同時(shí)也創(chuàng)建了用于設(shè)置這個(gè)實(shí)例變量的構(gòu)造函數(shù)。重構(gòu)過程創(chuàng)建的新類 BagImpl 如清單4所示。 清單 4. BagImpl 類
final class BagImpl implements Bag
{
private final BagExample example;
/**
* @paramBagExample
*/
BagImpl(BagExample example)
{
this.example = example;
// TODO Auto-generated constructor stub
}
Object o;
public Object get()
{
return o;
}
public void set(Object o)
{
this.o = o;
}
}
|
如果您的情況與這個(gè)例子相同,不需要保留對(duì) BagExample 的訪問,您也可以很安全地刪除這個(gè)實(shí)例變量與構(gòu)造函數(shù),將 BagExample 類中的代碼改成缺省的無參數(shù)構(gòu)造函數(shù)。
在類繼承關(guān)系內(nèi)移動(dòng)成員
還有兩個(gè)重構(gòu)工具,Push Down 和 Pull Up,分別實(shí)現(xiàn)將類方法或者屬性從一個(gè)類移動(dòng)到其子類或父類中。假設(shè)您有一個(gè)名為 Vehicle 的抽象類,其定義如清單5所示。 清單 5. 抽象的 Vehicle 類
public abstract class Vehicle
{
protected int passengers;
protected String motor;
public int getPassengers()
{
return passengers;
}
public void setPassengers(int i)
{
passengers = i;
}
public String getMotor()
{
return motor;
}
public void setMotor(String string)
{
motor = string;
}
}
|
您還有一個(gè) Vehicle 的子類,類名為 Automobile ,如清單6所示。 清單6. Automobile 類
public class Automobile extends Vehicle
{
private String make;
private String model;
public String getMake()
{
return make;
}
public String getModel()
{
return model;
}
public void setMake(String string)
{
make = string;
}
public void setModel(String string)
{
model = string;
}
}
|
請(qǐng)注意, Vehicle 有一個(gè)屬性是 motor 。如果您知道您將永遠(yuǎn)只處理汽車,那么這樣做就好了;但是如果您也允許出現(xiàn)劃艇之類的東西,那么您就需要將 motor 屬性從 Vehicle 類下放到 Automobile 類中。為此,您可以在 Outline 視圖中選擇 motor ,然后選擇 Refactor > Push Down。
Eclipse 還是挺聰明的,它知道您不可能總是單單移動(dòng)某個(gè)屬性本身,因此還提供了 Add Required 按鈕,不過在 Eclipse 2.1 中,這個(gè)功能并不總是能正確地工作。您需要驗(yàn)證一下,看所有依賴于這個(gè)屬性的方法是否都推到了下一層。在本例中,這樣的方法有兩個(gè),即與 motor 相伴的 getter 和 setter 方法,如圖3所示。
圖 3. 加入所需的成員
在按過 OK按鈕之后, motor 屬性以及 getMotor() 和 setMotor() 方法就會(huì)移動(dòng)到 Automobile 類中。清單7顯示了在進(jìn)行了這次重構(gòu)之后 Automobile 類的情形。 清單 7. 經(jīng)過重構(gòu)的 Automobile 類
public class Automobile extends Vehicle
{
private String make;
private String model;
protected String motor;
public String getMake()
{
return make;
}
public String getModel()
{
return model;
}
public void setMake(String string)
{
make = string;
}
public void setModel(String string)
{
model = string;
}
public String getMotor()
{
return motor;
}
public void setMotor(String string)
{
motor = string;
}
}
|
Pull Up 重構(gòu)與 Push Down 幾乎相同,當(dāng)然 Pull Up 是將類成員從一個(gè)類中移到其父類中,而不是子類中。如果您稍后改變主意,決定還是把 motor 移回到 Vehicle 類中,那么您也許就會(huì)用到這種重構(gòu)。同樣需要提醒您,一定要確認(rèn)您是否選擇了所有必需的成員。
Automobile 類中具有成員 motor,這意味著您如果創(chuàng)建另一個(gè)子類,比方說 Bus ,您就還需要將 motor (及其相關(guān)方法)加入到 Bus 類中。有一種方法可以表示這種關(guān)系,即創(chuàng)建一個(gè)名為 Motorized 的接口, Automobile 和 Bus 都實(shí)現(xiàn)這個(gè)接口,但是 RowBoat 不實(shí)現(xiàn)。
創(chuàng)建 Motorized 接口最簡(jiǎn)單的方法是在 Automobile 上使用 Extract Interface 重構(gòu)。為此,您可以在 Outline 視圖中選擇 Automobile ,然后從菜單中選擇 Refactor > Extract Interface。您可以在彈出的對(duì)話框中選擇您希望在接口中包含哪些方法,如圖4所示。
圖 4. 提取 Motorized 接口
點(diǎn)擊 OK 之后,接口就創(chuàng)建好了,如清單8所示。 清單 8. Motorized 接口
public interface Motorized
{
public abstract String getMotor();
public abstract void setMotor(String string);
}
|
同時(shí), Automobile 的類聲明也變成了下面的樣子:
public class Automobile extends Vehicle implements Motorized
|
使用父類
本重構(gòu)工具類型中最后一個(gè)是 User Supertyp Where Possible。想象一個(gè)用來管理汽車細(xì)帳的應(yīng)用程序。它自始至終都使用 Automobile 類型的對(duì)象。如果您想處理所有類型的交通工具,那么您就可以用這種重構(gòu)將所有對(duì) Automobile 的引用都變成對(duì) Vehicle 的引用(參看圖5)。如果您在代碼中用 instanceof 操作執(zhí)行了任何類型檢查的話,您將需要決定在這些地方適用的是原先的類還是父類,然后選中第一個(gè)選項(xiàng)“Use the selected supertype in 'instanceof' expressions”。
圖 5. 將 Automobile 改成其父類 Vehicle
使用父類的需求在 Java 語言中經(jīng)常出現(xiàn),特別是在使用了 Factory Method 模式的情況下。這種模式的典型實(shí)現(xiàn)方式是創(chuàng)建一個(gè)抽象類,其中具有靜態(tài)方法 create() ,這個(gè)方法返回的是實(shí)現(xiàn)了這個(gè)抽象類的一個(gè)具體對(duì)象。如果需創(chuàng)建的具體對(duì)象的類型依賴于實(shí)現(xiàn)的細(xì)節(jié),而調(diào)用類對(duì)實(shí)現(xiàn)細(xì)節(jié)并不感興趣的情況下,可以使用這一模式。
改變類內(nèi)部的代碼
最大一類重構(gòu)是實(shí)現(xiàn)了類內(nèi)部代碼重組的重構(gòu)方法。在所有的重構(gòu)方法中,只有這類方法允許您引入或者移除中間變量,根據(jù)原有方法中的部分代碼創(chuàng)建新方法,以及為屬性創(chuàng)建 getter 和 setter 方法。
提取與內(nèi)嵌
有一些重構(gòu)方法是以 Extract 這個(gè)詞開頭的:Extract Method、Extract Local Variable 以及Extract Constants。第一個(gè) Extract Method 的意思您可能已經(jīng)猜到了,它根據(jù)您選中的代碼創(chuàng)建新的方法。我們以清單8中那個(gè)類的 main() 方法為例。它首先取得命令行選項(xiàng)的值,如果有以 -D 開頭的選項(xiàng),就將其以名-值對(duì)的形式存儲(chǔ)在一個(gè) Properties 對(duì)象中。 清單 8. main()
import java.util.Properties;
import java.util.StringTokenizer;
public class StartApp
{
public static void main(String[] args)
{
Properties props = new Properties();
for (int i= 0; i < args.length; i++)
{
if(args[i].startsWith("-D"))
{
String s = args[i].substring(2);
StringTokenizer st = new StringTokenizer(s, "=");
if(st.countTokens() == 2)
{
props.setProperty(st.nextToken(), st.nextToken());
}
}
}
//continue...
}
}
|
將一部分代碼從一個(gè)方法中取出并放進(jìn)另一個(gè)方法中的原因主要有兩種。第一種原因是這個(gè)方法太長(zhǎng),并且完成了兩個(gè)以上邏輯上截然不同的操作。(我們不知道上面那個(gè) main() 方法還要處理哪些東西,但是從現(xiàn)在掌握的證據(jù)來看,這不是從其中提取出一個(gè)方法的理由。)另一種原因是有一段邏輯上清晰的代碼,這段代碼可以被其他方法重用。比方說在某些時(shí)候,您發(fā)現(xiàn)自己在很多不同的方法中都重復(fù)編寫了相同的幾行代碼。那就有可能是需要重構(gòu)的原因了,不過除非真的需要重用這部分代碼,否則您很可能并不會(huì)執(zhí)行重構(gòu)。
假設(shè)您還需要在另外一個(gè)地方解析名-值對(duì),并將其放在 Properties 對(duì)象中,那么您可以將包含 StringTokenizer 聲明和下面的 if 語句的這段代碼抽取出來。為此,您可以高亮選中這段代碼,然后從菜單中選擇 Refactor > Extract Method。您需要輸入方法名稱,這里輸入 addProperty ,然后驗(yàn)證這個(gè)方法的兩個(gè)參數(shù), Properties prop 和 Strings 。清單9顯示由 Eclipse 提取了 addProp() 方法之后類的情況。 清單 9. 提取出來的 addProp()
import java.util.Properties;
import java.util.StringTokenizer;
public class Extract
{
public static void main(String[] args)
{
Properties props = new Properties();
for (int i = 0; i < args.length; i++)
{
if (args[i].startsWith("-D"))
{
String s = args[i].substring(2);
addProp(props, s);
}
}
}
private static void addProp(Properties props, String s)
{
StringTokenizer st = new StringTokenizer(s, "=");
if (st.countTokens() == 2)
{
props.setProperty(st.nextToken(), st.nextToken());
}
}
}
|
Extract Local Variable 重構(gòu)取出一段被直接使用的表達(dá)式,然后將這個(gè)表達(dá)式首先賦值給一個(gè)局部變量。然后在原先使用那個(gè)表達(dá)式的地方使用這個(gè)變量。比方說,在上面的方法中,您可以高亮選中對(duì) st.nextToken() 的第一次調(diào)用,然后選擇 Refactor > Extract Local Variable。您將被提示輸入一個(gè)變量名稱,這里輸入 key 。請(qǐng)注意,這里有一個(gè)將被選中表達(dá)式所有出現(xiàn)的地方都替換成新變量的引用的選項(xiàng)。這個(gè)選項(xiàng)通常是適用的,但是對(duì)這里的 nextToken() 方法不適用,因?yàn)檫@個(gè)方法(顯然)在每一次調(diào)用的時(shí)候都返回不同的值。確認(rèn)這個(gè)選項(xiàng)未被選中。參見圖6。
圖 6. 不全部替換所選的表達(dá)式
接下來,在第二次調(diào)用 st.nextToken() 的地方重復(fù)進(jìn)行重構(gòu),這一次調(diào)用的是一個(gè)新的局部變量 value 。清單10顯示了這兩次重構(gòu)之后代碼的情形。 清單 10. 重構(gòu)之后的代碼
private static void addProp(Properties props, String s)
{
StringTokenizer st = new StringTokenizer(s, "=");
if(st.countTokens() == 2)
{
String key = st.nextToken();
String value = st.nextToken();
props.setProperty(key, value);
}
}
|
用這種方式引入變量有幾點(diǎn)好處。首先,通過為表達(dá)式提供有意義的名稱,可以使得代碼執(zhí)行的任務(wù)更加清晰。第二,代碼調(diào)試變得更容易,因?yàn)槲覀兛梢院苋菀椎貦z查表達(dá)式返回的值。最后,在可以用一個(gè)變量替換同一表達(dá)式的多個(gè)實(shí)例的情況下,效率將大大提高。
Extract Constant 與 Extract Local Variable 相似,但是您必須選擇靜態(tài)常量表達(dá)式,重構(gòu)工具將會(huì)把它轉(zhuǎn)換成靜態(tài)的 final 常量。這在將硬編碼的數(shù)字和字符串從代碼中去除的時(shí)候非常有用。比方說,在上面的代碼中我們用“-D”這一命令行選項(xiàng)來定義名-值對(duì)。先將“-D”高亮選中,選擇 Refactor > Extract Constant,然后輸入 DEFINE 作為常量的名稱。重構(gòu)之后的代碼如清單11所示: 清單 11. 重構(gòu)之后的代碼
public class Extract
{
private static final String DEFINE = "-D";
public static void main(String[] args)
{
Properties props = new Properties();
for (int i = 0; i < args.length; i++)
{
if (args[i].startsWith(DEFINE))
{
String s = args[i].substring(2);
addProp(props, s);
}
}
}
// ...
|
對(duì)于每一種 Extract... 類的重構(gòu),都存在對(duì)應(yīng)的 Inline... 重構(gòu),執(zhí)行與之相反的操作。比方說,如果您高亮選中上面代碼中的變量 s,選擇 Refactor > Inline...,然后點(diǎn)擊 OK,Eclipse 就會(huì)在調(diào)用 addProp() 的時(shí)候直接使用 args[i].substring(2) 這個(gè)表達(dá)式,如下所示:
if(args[i].startsWith(DEFINE))
{
addProp(props,args[i].substring(2));
}
|
這樣比使用臨時(shí)變量效率更高,代碼也變得更加簡(jiǎn)要,至于這樣的代碼是易讀還是含混,就取決于您的觀點(diǎn)了。不過一般說來,這樣的內(nèi)嵌重構(gòu)沒什么值得推薦的地方。
您可以按照用內(nèi)嵌表達(dá)式替換變量的相同方法,高亮選中方法名,或者靜態(tài) final 常量,然后從菜單中選擇 Refactor > Inline...,Eclipse 就會(huì)用方法的代碼替換方法調(diào)用,或者用常量的值替換對(duì)常量的引用。
封裝屬性
通常我們認(rèn)為將對(duì)象的內(nèi)部結(jié)構(gòu)暴露出來是一種不好的做法。這也正是 Vehicle 類及其子類都具有 private 或者 protected 屬性,而用 public setter 和 getter 方法來訪問屬性的原因。這些方法可以用兩種不同的方式自動(dòng)生成。
第一種生成這些方法的方式是使用 Source > Generate Getter and Setter 菜單。這將會(huì)顯示一個(gè)對(duì)話框,其中包含所有尚未存在的 getter 和 setter 方法。不過因?yàn)檫@種方式?jīng)]有用新方法更新對(duì)這些屬性的引用,所以并不算是重構(gòu);必要的時(shí)候,您必須自己完成更新引用的工作。這種方式可以節(jié)約很多時(shí)間,但是最好是在一開始創(chuàng)建類的時(shí)候,或者是向類中加入新屬性的時(shí)候使用,因?yàn)檫@些時(shí)候還不存在對(duì)屬性的引用,所以不需要再修改其他代碼。
第二種生成 getter 和 setter 方法的方式是選中某個(gè)屬性,然后從菜單中選擇 Refactor > Encapsulate Field。這種方式一次只能為一個(gè)屬性生成 getter 和 setter 方法,不過它與 Source > Generate Getter and Setter 相反,可以將對(duì)這個(gè)屬性的引用改變成對(duì)新方法的調(diào)用。
例如,我們可以先創(chuàng)建一個(gè)新的簡(jiǎn)版 Automobile 類,如清單12所示。 清單 12. 簡(jiǎn)單的 Automobile 類
public class Automobile extends Vehicle
{
public String make;
public String model;
}
|
接下來,創(chuàng)建一個(gè)類實(shí)例化了 Automobile 的類,并直接訪問 make 屬性,如清單13所示。 清單 13. 實(shí)例化 Automobile
public class AutomobileTest
{
public void race()
{
Automobilecar1 = new Automobile();
car1.make= "Austin Healy";
car1.model= "Sprite";
// ...
}
}
|
現(xiàn)在封裝 make 屬性。先高亮選中屬性名稱,然后選擇 Refactor > Encapsulate Field。在彈出的對(duì)話框中輸入 getter 和 setter 方法的名稱——如您所料,缺省的方法名稱分別是 getMake() 和 setMake()。您也可以選擇與這個(gè)屬性處在同一個(gè)類中的方法是繼續(xù)直接訪問該屬性,還是像其他類那樣改用這些訪問方法。(有一些人非常傾向于使用這兩種方式的某一種,不過碰巧在這種情況下您選擇哪一種方式都沒有區(qū)別,因?yàn)?Automobile 中沒有對(duì) make 屬性的引用。)
圖7. 封裝屬性
點(diǎn)擊 OK之后, Automobile 類中的 make 屬性就變成了私有屬性,也同時(shí)具有了 getMake() 和 setMake() 方法。 > 清單 14. 經(jīng)過重構(gòu)的 Automobile 類
public class Automobile extends Vehicle
{
private String make;
public String model;
public void setMake(String make)
{
this.make = make;
}
public String getMake()
{
return make;
}
}
|
AutomobileTest 類也要進(jìn)行更新,以便使用新的訪問方法,如清單15所示。
>清單 15. AutomobileTest 類
public class AutomobileTest
{
public void race()
{
Automobilecar1 = new Automobile();
car1.setMake("Austin Healy");
car1.model= "Sprite";
// ...
}
}
|
改變方法的簽名
本文介紹的最后一個(gè)重構(gòu)方法也是最難以使用的方法:Change Method Signature(改變方法的簽名)。這種方法的功能顯而易見——改變方法的參數(shù)、可見性以及返回值的類型。而進(jìn)行這樣的改變對(duì)于調(diào)用這個(gè)方法的其他方法或者代碼會(huì)產(chǎn)生什么影響,就不是那么顯而易見了。這么也沒有什么魔方。如果代碼的改變?cè)诒恢貥?gòu)的方法內(nèi)部引發(fā)了問題——變量未定義,或者類型不匹配——重構(gòu)操作將對(duì)這些問題進(jìn)行標(biāo)記。您可以選擇是接受重構(gòu),稍后改正這些問題,還是取消重構(gòu)。如果這種重構(gòu)在其他的方法中引發(fā)問題,就直接忽略這些問題,您必須在重構(gòu)之后親自修改。
為澄清這一點(diǎn),考慮清單16中列出的類和方法。 清單 16. MethodSigExample 類
public class MethodSigExample
{
public int test(String s, int i)
{
int x = i + s.length();
return x;
}
}
|
上面這個(gè)類中的 test() 方法被另一個(gè)類中的方法調(diào)用,如清單17所示。 清單 17. callTest 方法
public void callTest()
{
MethodSigExample eg = new MethodSigExample();
int r = eg.test("hello", 10);
}
|
在第一個(gè)類中高亮選中 test ,然后選擇 Refactor > Change Method Signature。您將看到如圖8所示的對(duì)話框。
圖 8. Change Method Signature 選項(xiàng)
第一個(gè)選項(xiàng)是改變?cè)摲椒ǖ目梢娦?。在本例中,將其改變?yōu)?protected 或者 private,這樣第二個(gè)類的 callTest() 方法就不能訪問這個(gè)方法了。(如果這兩個(gè)類在不同的包中,將訪問方法設(shè)為缺省值也會(huì)引起這樣的問題。) Eclipse 在進(jìn)行重構(gòu)的時(shí)候不會(huì)將這些問題標(biāo)出,您只有自己選擇適當(dāng)?shù)闹怠?
下面一個(gè)選項(xiàng)是改變返回值類型。如果將返回值改為 float ,這不會(huì)被標(biāo)記成錯(cuò)誤,因?yàn)?test() 方法返回語句中的 int 會(huì)自動(dòng)轉(zhuǎn)換成 float 。即便如此,在第二個(gè)類的 callTest() 方法中也會(huì)引起問題,因?yàn)?float 不能轉(zhuǎn)換成 int 。您需要將 test() 的返回值改為 int ,或者是將 callTest() 中的 r 改為 float 。
如果將第一個(gè)參數(shù)的類型從 String 變成 int ,那么也得考慮相同的問題。在重構(gòu)的過程中這些問題將會(huì)被標(biāo)出,因?yàn)樗鼈儠?huì)在被重構(gòu)的方法內(nèi)部引起問題: int 不具有方法 length() 。然而如果將其變成 StringBuffer ,問題就不會(huì)標(biāo)記出來,因?yàn)?StringBuffer 的確具有方法 length() 。當(dāng)然這會(huì)在 callTest() 方法中引起問題,因?yàn)樗谡{(diào)用 test() 的時(shí)候還是把一個(gè) String 傳遞進(jìn)去了。
前面提到過,在重構(gòu)引發(fā)了問題的情況下,不管問題是否被標(biāo)出,您都可以一個(gè)一個(gè)地修正這些問題,以繼續(xù)下去。還有一種方法,就是先行修改這些錯(cuò)誤。如果您打算刪除不再需要的參數(shù) i ,那么可以先從要進(jìn)行重構(gòu)的方法中刪除對(duì)它的引用。這樣刪除參數(shù)的過程就更加順利了。
最后一件需要解釋的事情是 Default Value 選項(xiàng)。這一選項(xiàng)值僅適用于將參數(shù)加入方法簽名中的情況。比方說,如果我們加入了一個(gè)類型為 String 的參數(shù),參數(shù)名為 n ,其缺省值為 world ,那么在 callTest() 方法中調(diào)用 test() 的代碼就變成下面的樣子:
public void callTest()
{
MethodSigExample eg = new MethodSigExample();
int r = eg.test("hello", 10, "world");
}
|
在這場(chǎng)有關(guān) Change Method Signature 重構(gòu)的看似可怕的討論中,我們并沒有隱藏其中的問題,但卻一直沒有提到,這種重構(gòu)其實(shí)是非常強(qiáng)大的工具,它可以節(jié)約很多時(shí)間,通常您必須進(jìn)行仔細(xì)的計(jì)劃才能成功地使用它。
結(jié)束語
Eclipse 提供的工具使重構(gòu)變得簡(jiǎn)單,熟悉這些工具將有助于您提高效率。敏捷開發(fā)方法采用迭代方式增加程序特性,因此需要依賴于重構(gòu)技術(shù)來改變和擴(kuò)展程序的設(shè)計(jì)。但即便您并沒有使用要求進(jìn)行正式重構(gòu)的方法,Eclipse 的重構(gòu)工具還是可以在進(jìn)行一般的代碼修改時(shí)提供節(jié)約時(shí)間的方法。如果您花些時(shí)間熟悉這些工具,那么當(dāng)出現(xiàn)可以利用它們的情況時(shí),您就能意識(shí)到所花費(fèi)的時(shí)間是值得的。
參考資料
- 您可以參閱本文在 developerWorks 全球站點(diǎn)上的 英文原文.
- 有關(guān)重構(gòu)的核心著作是 Refactoring: Improving the Design of Existing Code, 作者 Martin Fowler、Kent Beck、John Brant、William Opdyke 和 Don Roberts(Addison-Wesley,1999年)。
- 重構(gòu)是一種正在發(fā)展的方法,在 Eclipse In Action: A Guide for Java Developers (Manning, 2003年)一書中,作者 David Gallardo,Ed Burnette 以及 Robert McGovern 從在 Eclipse 中設(shè)計(jì)和開發(fā)項(xiàng)目的角度討論了這一話題。
- 模式(如本文中提到的 Factory Method 模式)是理解和討論面向?qū)ο笤O(shè)計(jì)的重要工具。這方面的經(jīng)典著作是 Design Patterns: Elements of Reusable Object-Oriented Software,作者為 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides (Addison-Wesley,1995年)。
- Design Patterns 中的例子是用 C++ 寫成的,這對(duì)于 Java 程序員是不小的障礙;Mark Grand 所著的 Patterns in Java, Volume One: A Catalog of Reusable Design Patterns Illustrated with UML(Wiley,1998年)將模式翻譯成了 Java 語言。
- 有關(guān)敏捷編程的一個(gè)變種,請(qǐng)參看 Kent Beck 所著的 Extreme Programming Explained: Embrace Change(Addison-Wesley,1999年)
Web 站點(diǎn)
developerWorks 上的文章與教程
關(guān)于作者
 |
|
David Gallardo 是 Studio B 上的一名作家,他是一名獨(dú)立軟件顧問和作家,專長(zhǎng)為軟件國(guó)際化、Java Web 應(yīng)用程序和數(shù)據(jù)庫開發(fā)。他成為專業(yè)軟件工程師已經(jīng)有十五年了,他擁有許多操作系統(tǒng)、編程語言和網(wǎng)絡(luò)協(xié)議的經(jīng)驗(yàn)。他最近在一家 BtoB 電子商務(wù)公司 TradeAccess, Inc 領(lǐng)導(dǎo)數(shù)據(jù)庫和國(guó)際化開發(fā)。在這之前,他是 Lotus Development Corporation 的 International Product Development 組中的高級(jí)工程師,負(fù)責(zé)為 Lotus 產(chǎn)品(包括 Domino)提供 Unicode 和國(guó)際語言支持的跨平臺(tái)庫的開發(fā)。David 是 Eclipse In Action: A Guide for Java Developers(2003年)一書的合著者??梢酝ㄟ^ david@gallardo.org 與 David 聯(lián)系。 | |