手把手教你重構!-Refactor

 

使用 Eclipse 自動重構特性的方法與原因

developerWorks
文檔選項
將打印機的版面設置成橫向打印模式

打印本頁

將此頁作爲電子郵件發送

將此頁作爲電子郵件發送


級別: 初級

David Gallardo ([email protected]), 獨立軟件顧問和作家

2003 年 11 月 10 日

Eclipse 提供了一組強大的自動重構(refactoring)功能,這些功能穿插在其他功能當中,使您能夠重命名 Java元素,移動類和包,從具體的類中創建接口,將嵌套的類變成頂級類,以及從舊方法的代碼片斷中析取出新的方法。您熟悉了 Eclipse 的重構工具之後,就掌握了一種提高生產率的好方法。本文綜覽Eclipse 的重構特性,並通過例子闡明瞭使用這些特性的方法與原因。

爲什麼重構?

重構是指在不改變程序功能的前提下改變其結構。重構是一項功能強大的技術,但是執行起來需要倍加小心纔行。主要的危險在於可能在不經意中引入一些錯誤,尤其是在進行手工重構的時候更是如此。這種危險引發了對重構技術的普遍批評:當代碼不會崩潰的時候爲什麼要修改它呢?

您需要進行代碼重構的原因可能有以下幾個:傳說中的第一個原因是:需要繼承爲某個古老產品而開發的年代久遠的代碼,或者突然碰到這些代碼。最初的開發團隊已經不在了。我們必須創建增加了新特性的新版本軟件,但是這些代碼已經無法理解了。新的開發隊伍夜以繼日地工作,破譯代碼然後映射代碼,經過大量的規劃與設計之後,人們將這些代碼分割成碎片。歷經重重磨難之後,所有這些東西都按照新版本的要求歸位了。這是英雄般的重構故事,幾乎沒有人能在經歷了這些之後活着講述這樣的故事。

還有一種現實一些的情況是項目中加入了新的需求,需要對設計進行修改。至於是因爲在最初的規劃過程中失察,還是由於採用了迭代式的開發過程(比如敏捷開發,或者是測試驅動的開發)而在開發過程中有意引入需求,這兩者並沒有實質性的區別。這樣的重構的規模要小得多,其內容一般涉及通過引入接口或者抽象類來更改類的繼承關係,以及對類進行分割和重新組織,等等。

重構的最後一個原因是,當存在可用的自動重構工具時,可以有一個用來預先生成代碼的快捷方式——就好比在您無法確定如何拼寫某個單詞的時候,可以用某種拼寫檢查工具輸入這個單詞。比如說,您可以用這種平淡無奇的重構方法生成 getter 和 setter 方法,一旦熟悉了這樣的工具,它就可以爲您節省很多的時間。

Eclipse 的重構工具無意進行英雄級的重構——適合這種規模的工具幾乎沒有——但是不論是否用到敏捷開發技術,Eclipse 的工具對於一般程序員修改代碼的工作都具有無法衡量的價值。畢竟任何複雜的操作只要能夠自動進行,就可以不那麼煩悶了。只要您知道 Eclipse 實現了什麼樣的重構工具,並理解了它們的適用情況,您的生產力就會得到極大的提高。

要降低對代碼造成破壞的風險,有兩種重要的方法。第一種方法是對代碼進行一套完全徹底的單元測試:在重構之前和之後都必須通過這樣的測試。第二種方法是使用自動化的工具來進行重構,比如說 Eclipse 的重構特性。

將徹底的測試與自動化重構結合起來就會更加有效了,這樣重構也就從一種神祕的藝術變成了有用的日常工具。爲了增加新的功能或者改進代碼的可維護性,我們可以在不影響原有代碼功能的基礎上迅速且安全地改變其結構。這種能力會對您設計和開發代碼的方式產生極大的影響,即便是您沒有將其結合到正式的敏捷方法中也沒有關係。





回頁首


Eclipse 中重構的類型

Eclipse 的重構工具可以分爲三大類(下面的順序也就是這些工具在 Refactoring 菜單中出現的順序):

  1. 對代碼進行重命名以及改變代碼的物理結構,包括對屬性、變量、類以及接口重新命名,還有移動包和類等。
  2. 改變類一級的代碼邏輯結構,包括將匿名類轉變爲嵌套類,將嵌套類轉變爲頂級類、根據具體的類創建接口,以及從一個類中將方法或者屬性移到子類或者父類中。
  3. 改變一個類內部的代碼,包括將局部變量變成類的屬性、將某個方法中選中部分的代碼變成一個獨立的方法、以及爲屬性生成 getter 和 setter 方法。

還有幾個重構工具並不能完全歸入這三個種類,特別是 Change Method Signature,不過在本文中還是將這個工具歸入第三類。除了這種例外情況以外,本文下面幾節都是按照上面的順序來討論 Eclipse 重構工具的。





回頁首


物理重組與重命名

顯然,您即便沒有特別的工具,也可以在文件系統中重命名文件或者是移動文件,但是如果操作對象是 Java 源代碼文件,您就需要編輯很多文件,更新其中的 import 或 package 語句。與此類似,用某種文本編輯器的搜索與替換功能也可以很容易地給類、方法和變量重新命名,但是這樣做的時候必須十分小心,因爲不同的類可能具有名稱相似的方法或者變量;要是從頭到尾檢查項目中所有的文件,來保證每個東西的標識和修改的正確性,那可真夠乏味的。

Eclipse 的 Rename 和 Move 工具能夠十分聰明地在整個項目中完成這樣的修改,而不需要用戶的干涉。這是因爲 Eclipse 可以理解代碼的語義,從而能夠識別出對某個特定方法、變量或者類名稱的引用。簡化這一任務有助於確保方法、變量和類的名稱能夠清晰地指示其用途。

我們經常可以發現代碼的名字不恰當或者令人容易誤解,這是因爲代碼與最初設計的功能有所不同。比方說,某個用來在文件中查找特定單詞的程序也許會擴展爲在 Web 頁面中通過 URL 獲取 InputStream 的操作。如果這一輸入流最初叫做 file ,那麼就應該修改它的名字,以便能反映其新增的更加一般的特性,比方說 sourceStream 。開發人員經常無法成功地修改這些名稱,因爲這個過程是十分混亂和乏味的。這當然也會把下一個不得不對這些類進行操作的開發人員弄糊塗。

要對某個 Java 元素進行重命名,只需要簡單地從 Package Explorer 視圖中點擊這個元素,或者從Java 源代碼文件中選中這個元素,然後選擇菜單項 Refactor > Rename。在對話框中輸入新的名稱,然後選擇是否需要 Eclipse 也改變對這個名稱的引用。實際顯示出來的確切內容與您所選元素的類型有關。比方說,如果選擇的屬性具有 getter 和 setter 方法,那麼也就可以同時更新這些方法的名稱,以反映新的屬性。圖1顯示了一個簡單的例子。

圖 1. 重命名一個局部變量 
Renaming a local variable

就像所有的 Eclipse 重構操作一樣,當您指定了全部用來執行重構的必要信息之後,您就可以點擊 Preview 按鈕,然後在一個對話框中對比 Eclipse 打算進行哪些變更,您可以分別否決或者確認每一個受到影響的文件中的每一項變更。如果您對於 Eclipse 正確執行變更的能力有信心的話,您可以只按下 OK按鈕。顯然,如果您不確定重構到底做了什麼事情,您就會想先預覽一下,但是對於 Rename 和 Move 這樣簡單的重構而言,通常沒有必要預覽。

Move 操作與 Rename 十分相似:您選擇某個 Java 元素(通常是一個類),爲其指定一個新位置,並定義是否需要更新引用。然後,您可以選擇 Preview檢查變更情況,或者選擇 OK 立即執行重構,如圖2所示。

圖 2. 將類從一個包移到另一個包 
Moving a class

在某些平臺上(特別是 Windows),您還可以在 Package Explorer 視圖中通過簡單拖放的方法將類從一個包或者文件夾中移到另一個包或文件夾中。所有的引用都會自動更新。





回頁首


重新定義類的關係

Eclipse 中有大量的重構工具,使您能夠自動改變類的關係。這些重構工具並沒有 Eclipse 提供的其他工具那麼常用,但是很有價值,因爲它們能夠執行非常複雜的任務。可以說,當它們用得上的時候,就會非常有用。

提升匿名類與嵌套類

Convert Anonymous Class(轉換匿名類)和 Convert Nested Type(轉換嵌套類)這兩種重構方法比較相似,它們都將某個類從其當前範圍移動到包含這個類的範圍上。

匿名類是一種語法速寫標記,使您能夠在需要實現某個抽象類或者接口的地方創建一個類的實例,而不需要顯式提供類的名稱。比如在創建用戶界面中的監聽器時,就經常用到匿名類。在清單1中,假設 Bag 是在其他地方定義的一個接口,其中聲明瞭兩個方法, 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);
   }
}

當匿名類變得很大,其中的代碼難以閱讀的時候,您就應該考慮將這個匿名類變成嚴格意義上的類;爲了保持封裝性(換句話說,就是將它隱藏起來,使得不必知道它的外部類不知道它),您應該將其變成嵌套類,而不是頂級類。您可以在這個匿名類的內部點擊,然後選擇 Refactor > Convert Anonymous Class to Nested 就可以了。當出現確認對話框的時候,爲這個類輸入名稱,比如 BagImpl ,然後選擇 Preview或者 OK。這樣,代碼就變成了如清單2所示的情形。


清單 2. 經過重構的 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);
   }
}

當您想讓其他的類使用某個嵌套類時,Convert Nested Type to Top Level 就很有用了。比方說,您可以在一個類中使用值對象,就像上面的 BagImpl 類那樣。如果您後來又決定應該在多個類之間共享這個數據,那麼重構操作就能從這個嵌套類中創建新的類文件。您可以在源代碼文件中高亮選中類名稱(或者在 Outline 視圖中點擊類的名稱),然後選擇Refactor > Convert Nested Type to Top Level,這樣就實現了重構。

這種重構要求您爲裝入實例提供一個名字。重構工具也會提供建議的名稱,比如 example ,您可以接受這個名字。這個名字的意思過一會兒就清楚了。點擊 OK 之後,外層類BagExample 就會變成清單3所示的樣子。


清單 3. 經過重構的 Bag 類
public class BagExample
{
   void processMessage(String msg)
   {
      Bag bag = new BagImpl(this);
      bag.set(msg);
      MessagePipe pipe = new MessagePipe();
      pipe.send(bag);
   }
}

請注意,當一個類是嵌套類的時候,它可以訪問其外層類的成員。爲了保留這種功能,重構過程將一個裝入類 BagExample 的實例放在前面那個嵌套類中。這就是之前要求您輸入名稱的實例變量。同時也創建了用於設置這個實例變量的構造函數。重構過程創建的新類 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;
   }
}

如果您的情況與這個例子相同,不需要保留對 BagExample 的訪問,您也可以很安全地刪除這個實例變量與構造函數,將 BagExample 類中的代碼改成缺省的無參數構造函數。

在類繼承關係內移動成員

還有兩個重構工具,Push Down 和 Pull Up,分別實現將類方法或者屬性從一個類移動到其子類或父類中。假設您有一個名爲 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;
   }
}

您還有一個 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;
   }
}

請注意, Vehicle 有一個屬性是 motor 。如果您知道您將永遠只處理汽車,那麼這樣做就好了;但是如果您也允許出現划艇之類的東西,那麼您就需要將 motor 屬性從 Vehicle 類下放到 Automobile 類中。爲此,您可以在 Outline 視圖中選擇 motor ,然後選擇 Refactor > Push Down

Eclipse 還是挺聰明的,它知道您不可能總是單單移動某個屬性本身,因此還提供了 Add Required 按鈕,不過在 Eclipse 2.1 中,這個功能並不總是能正確地工作。您需要驗證一下,看所有依賴於這個屬性的方法是否都推到了下一層。在本例中,這樣的方法有兩個,即與 motor 相伴的 getter 和 setter 方法,如圖3所示。

圖 3. 加入所需的成員 
Adding required members

在按過 OK按鈕之後, motor 屬性以及 getMotor() 和 setMotor() 方法就會移動到 Automobile 類中。清單7顯示了在進行了這次重構之後 Automobile 類的情形。


清單 7. 經過重構的 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 重構與 Push Down 幾乎相同,當然 Pull Up 是將類成員從一個類中移到其父類中,而不是子類中。如果您稍後改變主意,決定還是把 motor 移回到 Vehicle 類中,那麼您也許就會用到這種重構。同樣需要提醒您,一定要確認您是否選擇了所有必需的成員。

Automobile 類中具有成員 motor,這意味着您如果創建另一個子類,比方說 Bus ,您就還需要將 motor (及其相關方法)加入到 Bus 類中。有一種方法可以表示這種關係,即創建一個名爲 Motorized 的接口, Automobile 和 Bus 都實現這個接口,但是 RowBoat 不實現。

創建 Motorized 接口最簡單的方法是在 Automobile 上使用 Extract Interface 重構。爲此,您可以在 Outline 視圖中選擇 Automobile ,然後從菜單中選擇 Refactor > Extract Interface。您可以在彈出的對話框中選擇您希望在接口中包含哪些方法,如圖4所示。

圖 4. 提取 Motorized 接口 
Motorized interface

點擊 OK 之後,接口就創建好了,如清單8所示。


清單 8. Motorized 接口
public interface Motorized
{
   public abstract String getMotor();
   public abstract void setMotor(String string);
}

同時, Automobile 的類聲明也變成了下面的樣子:

public class Automobile extends Vehicle implements Motorized

使用父類

本重構工具類型中最後一個是 User Supertyp Where Possible。想象一個用來管理汽車細帳的應用程序。它自始至終都使用 Automobile 類型的對象。如果您想處理所有類型的交通工具,那麼您就可以用這種重構將所有對 Automobile 的引用都變成對 Vehicle 的引用(參看圖5)。如果您在代碼中用 instanceof 操作執行了任何類型檢查的話,您將需要決定在這些地方適用的是原先的類還是父類,然後選中第一個選項“Use the selected supertype in 'instanceof' expressions”。

圖 5. 將 Automobile 改成其父類 Vehicle 
Supertype

使用父類的需求在 Java 語言中經常出現,特別是在使用了 Factory Method 模式的情況下。這種模式的典型實現方式是創建一個抽象類,其中具有靜態方法 create() ,這個方法返回的是實現了這個抽象類的一個具體對象。如果需創建的具體對象的類型依賴於實現的細節,而調用類對實現細節並不感興趣的情況下,可以使用這一模式。





回頁首


改變類內部的代碼

最大一類重構是實現了類內部代碼重組的重構方法。在所有的重構方法中,只有這類方法允許您引入或者移除中間變量,根據原有方法中的部分代碼創建新方法,以及爲屬性創建 getter 和 setter 方法。

提取與內嵌

有一些重構方法是以 Extract 這個詞開頭的:Extract Method、Extract Local Variable 以及Extract Constants。第一個 Extract Method 的意思您可能已經猜到了,它根據您選中的代碼創建新的方法。我們以清單8中那個類的 main() 方法爲例。它首先取得命令行選項的值,如果有以 -D 開頭的選項,就將其以名-值對的形式存儲在一個 Properties 對象中。


清單 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...
   }
}

將一部分代碼從一個方法中取出並放進另一個方法中的原因主要有兩種。第一種原因是這個方法太長,並且完成了兩個以上邏輯上截然不同的操作。(我們不知道上面那個 main() 方法還要處理哪些東西,但是從現在掌握的證據來看,這不是從其中提取出一個方法的理由。)另一種原因是有一段邏輯上清晰的代碼,這段代碼可以被其他方法重用。比方說在某些時候,您發現自己在很多不同的方法中都重複編寫了相同的幾行代碼。那就有可能是需要重構的原因了,不過除非真的需要重用這部分代碼,否則您很可能並不會執行重構。

假設您還需要在另外一個地方解析名-值對,並將其放在 Properties 對象中,那麼您可以將包含 StringTokenizer 聲明和下面的 if 語句的這段代碼抽取出來。爲此,您可以高亮選中這段代碼,然後從菜單中選擇 Refactor > Extract Method。您需要輸入方法名稱,這裏輸入 addProperty ,然後驗證這個方法的兩個參數, 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 重構取出一段被直接使用的表達式,然後將這個表達式首先賦值給一個局部變量。然後在原先使用那個表達式的地方使用這個變量。比方說,在上面的方法中,您可以高亮選中對 st.nextToken() 的第一次調用,然後選擇 Refactor > Extract Local Variable。您將被提示輸入一個變量名稱,這裏輸入 key 。請注意,這裏有一個將被選中表達式所有出現的地方都替換成新變量的引用的選項。這個選項通常是適用的,但是對這裏的 nextToken() 方法不適用,因爲這個方法(顯然)在每一次調用的時候都返回不同的值。確認這個選項未被選中。參見圖6。

圖 6. 不全部替換所選的表達式 
Extract variable

接下來,在第二次調用 st.nextToken() 的地方重複進行重構,這一次調用的是一個新的局部變量 value 。清單10顯示了這兩次重構之後代碼的情形。


清單 10. 重構之後的代碼
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);
      }
   }

用這種方式引入變量有幾點好處。首先,通過爲表達式提供有意義的名稱,可以使得代碼執行的任務更加清晰。第二,代碼調試變得更容易,因爲我們可以很容易地檢查表達式返回的值。最後,在可以用一個變量替換同一表達式的多個實例的情況下,效率將大大提高。

Extract Constant 與 Extract Local Variable 相似,但是您必須選擇靜態常量表達式,重構工具將會把它轉換成靜態的 final 常量。這在將硬編碼的數字和字符串從代碼中去除的時候非常有用。比方說,在上面的代碼中我們用“-D”這一命令行選項來定義名-值對。先將“-D”高亮選中,選擇 Refactor > Extract Constant,然後輸入 DEFINE 作爲常量的名稱。重構之後的代碼如清單11所示:


清單 11. 重構之後的代碼
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);
         }
      }
   }
   // ...

對於每一種 Extract... 類的重構,都存在對應的 Inline... 重構,執行與之相反的操作。比方說,如果您高亮選中上面代碼中的變量 s,選擇 Refactor > Inline...,然後點擊 OK,Eclipse 就會在調用 addProp() 的時候直接使用 args[i].substring(2) 這個表達式,如下所示:

        if(args[i].startsWith(DEFINE))
         {
            addProp(props,args[i].substring(2));
         }

這樣比使用臨時變量效率更高,代碼也變得更加簡要,至於這樣的代碼是易讀還是含混,就取決於您的觀點了。不過一般說來,這樣的內嵌重構沒什麼值得推薦的地方。

您可以按照用內嵌表達式替換變量的相同方法,高亮選中方法名,或者靜態 final 常量,然後從菜單中選擇 Refactor > Inline...,Eclipse 就會用方法的代碼替換方法調用,或者用常量的值替換對常量的引用。

封裝屬性

通常我們認爲將對象的內部結構暴露出來是一種不好的做法。這也正是 Vehicle 類及其子類都具有 private 或者 protected 屬性,而用 public setter 和 getter 方法來訪問屬性的原因。這些方法可以用兩種不同的方式自動生成。

第一種生成這些方法的方式是使用 Source > Generate Getter and Setter 菜單。這將會顯示一個對話框,其中包含所有尚未存在的 getter 和 setter 方法。不過因爲這種方式沒有用新方法更新對這些屬性的引用,所以並不算是重構;必要的時候,您必須自己完成更新引用的工作。這種方式可以節約很多時間,但是最好是在一開始創建類的時候,或者是向類中加入新屬性的時候使用,因爲這些時候還不存在對屬性的引用,所以不需要再修改其他代碼。

第二種生成 getter 和 setter 方法的方式是選中某個屬性,然後從菜單中選擇 Refactor > Encapsulate Field。這種方式一次只能爲一個屬性生成 getter 和 setter 方法,不過它與 Source > Generate Getter and Setter 相反,可以將對這個屬性的引用改變成對新方法的調用。

例如,我們可以先創建一個新的簡版 Automobile 類,如清單12所示。


清單 12. 簡單的 Automobile 類
public class Automobile extends Vehicle
{
   public String make;
   public String model;
}

接下來,創建一個類實例化了 Automobile 的類,並直接訪問 make 屬性,如清單13所示。


清單 13. 實例化 Automobile
public class AutomobileTest
{
   public void race()
   {
      Automobilecar1 = new Automobile();
      car1.make= "Austin Healy";
      car1.model= "Sprite";
      // ...
   }
}

現在封裝 make 屬性。先高亮選中屬性名稱,然後選擇 Refactor > Encapsulate Field。在彈出的對話框中輸入 getter 和 setter 方法的名稱——如您所料,缺省的方法名稱分別是 getMake() 和 setMake()。您也可以選擇與這個屬性處在同一個類中的方法是繼續直接訪問該屬性,還是像其他類那樣改用這些訪問方法。(有一些人非常傾向於使用這兩種方式的某一種,不過碰巧在這種情況下您選擇哪一種方式都沒有區別,因爲 Automobile 中沒有對 make 屬性的引用。)

圖7. 封裝屬性 
Encapsulating a field

點擊 OK之後, Automobile 類中的 make 屬性就變成了私有屬性,也同時具有了 getMake() 和 setMake() 方法。


清單 14. 經過重構的 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 類也要進行更新,以便使用新的訪問方法,如清單15所示。


>清單 15. AutomobileTest 類
public class AutomobileTest
{
   public void race()
   {
      Automobilecar1 = new Automobile();
      car1.setMake("Austin Healy");
      car1.model= "Sprite";
      // ...
   }
}

改變方法的簽名

本文介紹的最後一個重構方法也是最難以使用的方法:Change Method Signature(改變方法的簽名)。這種方法的功能顯而易見——改變方法的參數、可見性以及返回值的類型。而進行這樣的改變對於調用這個方法的其他方法或者代碼會產生什麼影響,就不是那麼顯而易見了。這麼也沒有什麼魔方。如果代碼的改變在被重構的方法內部引發了問題——變量未定義,或者類型不匹配——重構操作將對這些問題進行標記。您可以選擇是接受重構,稍後改正這些問題,還是取消重構。如果這種重構在其他的方法中引發問題,就直接忽略這些問題,您必須在重構之後親自修改。

爲澄清這一點,考慮清單16中列出的類和方法。


清單 16. MethodSigExample 類
public class MethodSigExample
{
   public int test(String s, int i)
   {
      int x = i + s.length();
      return x;
   }
}

上面這個類中的 test() 方法被另一個類中的方法調用,如清單17所示。


清單 17. callTest 方法
public void callTest()
   {
     MethodSigExample eg = new MethodSigExample();
     int r = eg.test("hello", 10);
   }

在第一個類中高亮選中 test ,然後選擇 Refactor > Change Method Signature。您將看到如圖8所示的對話框。

圖 8. Change Method Signature 選項 
Change Method Signature options

第一個選項是改變該方法的可見性。在本例中,將其改變爲 protected 或者 private,這樣第二個類的 callTest() 方法就不能訪問這個方法了。(如果這兩個類在不同的包中,將訪問方法設爲缺省值也會引起這樣的問題。) Eclipse 在進行重構的時候不會將這些問題標出,您只有自己選擇適當的值。

下面一個選項是改變返回值類型。如果將返回值改爲 float ,這不會被標記成錯誤,因爲 test() 方法返回語句中的 int 會自動轉換成 float 。即便如此,在第二個類的 callTest()方法中也會引起問題,因爲 float 不能轉換成 int 。您需要將 test() 的返回值改爲 int ,或者是將 callTest() 中的 改爲 float 。

如果將第一個參數的類型從 String 變成 int ,那麼也得考慮相同的問題。在重構的過程中這些問題將會被標出,因爲它們會在被重構的方法內部引起問題: int 不具有方法 length()。然而如果將其變成 StringBuffer ,問題就不會標記出來,因爲 StringBuffer 的確具有方法 length() 。當然這會在 callTest() 方法中引起問題,因爲它在調用 test() 的時候還是把一個 String 傳遞進去了。

前面提到過,在重構引發了問題的情況下,不管問題是否被標出,您都可以一個一個地修正這些問題,以繼續下去。還有一種方法,就是先行修改這些錯誤。如果您打算刪除不再需要的參數 i ,那麼可以先從要進行重構的方法中刪除對它的引用。這樣刪除參數的過程就更加順利了。

最後一件需要解釋的事情是 Default Value 選項。這一選項值僅適用於將參數加入方法簽名中的情況。比方說,如果我們加入了一個類型爲 String 的參數,參數名爲 n ,其缺省值爲 world ,那麼在 callTest() 方法中調用 test() 的代碼就變成下面的樣子:

      
         public void callTest()
   {
      MethodSigExample eg = new MethodSigExample();
      int r = eg.test("hello", 10, "world");
   }

在這場有關 Change Method Signature 重構的看似可怕的討論中,我們並沒有隱藏其中的問題,但卻一直沒有提到,這種重構其實是非常強大的工具,它可以節約很多時間,通常您必須進行仔細的計劃才能成功地使用它。





回頁首


結束語

Eclipse 提供的工具使重構變得簡單,熟悉這些工具將有助於您提高效率。敏捷開發方法採用迭代方式增加程序特性,因此需要依賴於重構技術來改變和擴展程序的設計。但即便您並沒有使用要求進行正式重構的方法,Eclipse 的重構工具還是可以在進行一般的代碼修改時提供節約時間的方法。如果您花些時間熟悉這些工具,那麼當出現可以利用它們的情況時,您就能意識到所花費的時間是值得的。



參考資料

  • 您可以參閱本文在 developerWorks 全球站點上的 英文原文
  • 有關重構的核心著作是 Refactoring: Improving the Design of Existing Code, 作者 Martin Fowler、Kent Beck、John Brant、William Opdyke 和 Don Roberts(Addison-Wesley,1999年)。 
  • 重構是一種正在發展的方法,在 Eclipse In Action: A Guide for Java Developers (Manning, 2003年)一書中,作者 David Gallardo,Ed Burnette 以及 Robert McGovern 從在 Eclipse 中設計和開發項目的角度討論了這一話題。 
  • 模式(如本文中提到的 Factory Method 模式)是理解和討論面向對象設計的重要工具。這方面的經典著作是 Design Patterns: Elements of Reusable Object-Oriented Software,作者爲 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides (Addison-Wesley,1995年)。 
  • Design Patterns 中的例子是用 C++ 寫成的,這對於 Java 程序員是不小的障礙;Mark Grand 所著的 Patterns in Java, Volume One: A Catalog of Reusable Design Patterns Illustrated with UML(Wiley,1998年)將模式翻譯成了 Java 語言。 
  • 有關敏捷編程的一個變種,請參看 Kent Beck 所著的 Extreme Programming Explained: Embrace Change(Addison-Wesley,1999年) 

Web 站點

developerWorks 上的文章與教程



關於作者

 

David Gallardo 是 Studio B 上的一名作家,他是一名獨立軟件顧問和作家,專長爲軟件國際化、Java Web 應用程序和數據庫開發。他成爲專業軟件工程師已經有十五年了,他擁有許多操作系統、編程語言和網絡協議的經驗。他最近在一家 BtoB 電子商務公司 TradeAccess, Inc 領導數據庫和國際化開發。在這之前,他是 Lotus Development Corporation 的 International Product Development 組中的高級工程師,負責爲 Lotus 產品(包括 Domino)提供 Unicode 和國際語言支持的跨平臺庫的開發。David 是Eclipse In Action: A Guide for Java Developers(2003年)一書的合著者。可以通過 [email protected] 與 David 聯繫。

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