問題背景
在基於 Java 開發的電信級系統中,會有大量的 GUI 界面設計工作,但眾所周知 Java 的目前的 IDE 解決方案對 Swing 界面開發支持的友好性不盡如人意,要做出友好的界面還是要耗費大量的時間,對有些模塊可能比業務 邏輯的工作量還要大。所以,現在對於 GUI 界面比較多的系統中,很多公司都會用到界面引擎和 XML 方式來自動生成界面 ,優點在於:
1、使用 XML 文檔描述界面,通過界面生成引擎來解釋 XML 文檔並最終產生顯示的界面。這使得開發 界面更加容易,界面風格更加一致,維護更加方便。
2、實現了功能代碼和界面代碼的分離,使它們之間的耦合性減 小,這也降低了故障發生的概率,提高了軟件的重用率,減少了代碼 Java 代碼數量。
其基本實現原理見下圖 1:
圖 1.XML 文件自動生成界面的原理
具體的界面引擎代碼看 GUIEngine.java 文 件。
我們給一個簡單的界面描述文件的范例見如下清單 1:
清單 1. XML 界面描述文件實例
<?xml version="1.0" encoding="GB2312"?> <gui_desc> <init> <window_width>260</window_width> <window_height>230</window_height> </init> <component type="javax.swing.JLabel"> <height>45</height> <label>UPS Type</label> <name>labeltest</name> <positionY>12</positionY> <width>230</width> <positionX>12</positionX> </component> <component type="javax.swing.JTextField"> <height>45</height> <default_value>0</default_value> <name>txttest</name> <positionY>67</positionY> <width>230</width> <positionX>12</positionX> </component> <component type="javax.swing.JButton"> <name>btnOK</name> <width>91</width> <action>OutdoorUPS_OkAction</action> <disable /> <positionY>132</positionY> <positionX>12</positionX> <icon>ok.gif</icon> <label> 確定 </label> <height>23</height> </component> <component type="javax.swing.JButton"> <name>btnCancel</name> <width>91</width> <action>CancelAction</action> <disable /> <positionY>132</positionY> <positionX>112</positionX> <icon>cancel.gif</icon> <label> 取消 </label> <height>23</height> </component> </gui_desc>
通過程序創建顯示出來的 Swing 界面如下圖 2:
圖 2 .XML 描述文件生成的界面
程序調 用邏輯如下:
請單 2. 根據描述文件創建界面程序清單
/* 創建一個主程序框架 */ JFrame jf=new JFrame("test"); /* 傳遞界面描述文件,初始化界面引擎實例 */ GUIEngine ge=new GUIEngine("Outdoor_UPS.xml"); ge.createJDialog(jf, "hello world!!").setVisible(false);
界面輔助類
但在實際使用過程中,基於我 們已有的開發習慣,我們會發現如果您要訪問裡面的具體控件就不是那麼方便了。您需要通過界面引擎提供的接口來實現 getComponentByName(String name),其實現邏輯就是在創建界面時將所有的界面控件以控件名稱為關鍵字存放在一個 HashMap 中,獲取控件就是從存放所有控件的 HashMap 讀取出來。
以上面程序為例,我們在 XML 界面文件中定義 了一個控件名稱為“txttest”,此時我是不能直接用 ge.txtest 的方式來訪問的,我只能通過調 GUIEngine 實例提供的 接口來完成,ge.getComponentByName(“txttest”),而且獲取後的對象其類型信息已經丟失了,必須要用強制類型轉換為 javax.swing.JTextField 才能使用。按此種方法訪問的缺點是顯而易見的:
1、從 GUIEngine 實例中獲取的控件對 象丟失了類型;
2、另一個問題是需要使用字符串作為關鍵字從 HashMap 中出錯時,由於沒有 Java 的編譯類型的 檢查過程,不容易發現錯誤。
為解決上述問題,在實踐過程中出於使用習慣的,通常會針對每一個界面都會構建一 個輔助類。在輔助類中直接定義各種界面定義的各種類型的組件的 public 成員,並在輔助類中提供一個靜態方法用於根據 GUIEngine 生成一個輔助類。在需要對界面控件訪問時,直接使用輔助類的成員進行訪問。在輔助類中調用 GUIEngine 的 方法獲取組件並賦給輔助類中的成員對象,在其他地方就可以避免調用 getComponentByName 方法來訪問界面控件了。
以上面界面為例:
清單 3. 界面輔助類的實例
/** * 增加、修改 UPS TYPE 數據的操作方法。 */ public class UPSTypeHelper { public JButton btnOk;// 確定按鈕 public JButton btnCancel;// 取消按鈕 public JTextField txttest;// 文本框 public JLabel labeltest ;// 文本標簽 /** * 本類的實例對象池。 key:界面描述文件名。 value:類實例。 */ private static HashMap instancePool = new HashMap(); /** * 當前界面引擎 */ private GUIEngine guiEngine = null; private UPSTypeHelper (GUIEngine guiEngine) { this.guiEngine = guiEngine; this.getAllComponents(); } /** * 通過 GUIEngine 獲取界面上的所有組件對象。 */ private void get All Components () { txttest = (JTextField) this.guiEngine.jcmSet.get("txttest"); btnOk=(JButton) this.guiEngine.jcmSet.get("btnOk"); btnCancel=(JButton) this.guiEngine.jcmSet.get("btnCancel"); labeltest=(JLabel) this.guiEngine.jcmSet.get("labeltest"); } /** * 采用單例模式,保證只創建一次同樣的界面對應的輔助類 */ public static UPSTypeHelper getInstance(GUIEngine guiEngine) { /* 獲取該界面引擎對應的 xml 界面描述文件 */ String guifile = guiEngine.getXmlfile(); UPSTypeHelper instance = (UPSTypeHelper) instancePool.get(guifile ); if (instance == null) { instance = new UPSTypeHelper (guiEngine); instancePool .put(guifile , instance); } return instance; } /** * 清除使用過的實例。 */ public static void removeInstance(String guifile ) { if (instancePool .get(guifile ) != null) { instancePool .remove(guifile ); } } }
這種輔助類與每一個界面 XML 文件 ( 實際上是根據 XML 文件生成的 GUIEngine 對象 ) 一一對應。即每畫一 個界面 XML 文件都需要創建一個界面助手類。
界面輔助類的缺陷
界面輔助類使用中也存在可以優化的地方 ,比如輔助類的構建,完全可以在界面創建時自動構建並完成組件的映射設置工作,不用在 getAllComponents 方法中再手 工設置實現映射功能。
另外也存在一個問題,當主窗體 ( 或其上的彈出對話框 ) 關閉時,為了清除內存,需要調 用 removeInstance 來將輔助類的實例移除,否則會導致內存洩露。在一個由 100 多人組成的開發團隊,同時新老搭配, 產品生產周期很長的情況下,我們會發現很多 BUG 的出現就是忘記調用該方法。很多員工特別對新員工,對其原理不是太 理解,可能還認為,Java 內存還需要我去管理的麼?而且,錯誤定位非常復雜麻煩。
界面助手類的對象特性分析
圖 3. 界面助手類特性分析示意圖
讓我們回想一下我們創建界面助手類的目的 是什麼?很簡單,簡化訪問。界面助手類所訪問的真正對象是 GUIEngine 中的界面組件。那麼是否應有這樣的特性, GUIEngine 對象不存在了,助手類的對象也應隨之消失。或者說,界面助手類對象就象是 GUIEngine 對象的寄生體。當宿 主人不存在了,寄生體肯定也消失了。
如果將 GUIEngine 對象比喻成人,那麼界面助手類對象就象是使用 X 射線 機對人體進行透視看到的圖像。雖然通過理論知識的學習,我們知道人體有多少塊骨骼,但是卻被皮膚遮住了,我們無法直 觀地看到,通過 X 射線機我們就可以清晰地看到了。
界面助手類的管理
既然界面助手類主要麻煩在內存需 要手動釋放其原理見下面示意圖 4,我們需要找到一個更為簡單釋放內存的方法。
圖 4 手動釋放內存原理示意圖
當界面 關閉時,由於輔助類是調用界面引擎的實例,所以界面類內存並沒有完全釋放,需要手動銷毀輔助類實例,否則,長期運行 會導致內存洩露。
此時我們可以首先想到,我們把這個輔助類,直接放在宿主類不就可以了嗎?見下圖 5
圖 5 改善後內存自動釋放示意圖
當界面關閉時,由於輔助類是掛接在界面引 擎類中,所以界面關閉時,根據 Java 內存回收原理,輔助類就自動被銷毀,內存對象被收回,避免了內存洩露的情況。
這樣在能夠訪問 GUIEngine 的地方我們都可以獲得輔助類,當 GUIEngine 銷毀時,也無法再獲取到界面助手類了 。我們可以在 GUIEngine 中,增加一個方法 getGUIHelpMap(), 返還一個 HashMap 來替代輔助類中的
private static HashMap instancePool = new HashMap();
方法可以改寫一下見下面代碼:
清單 4. 可以自動釋放內存的關鍵代碼清單
public static UPSTypeHelper getInstance(GUIEngine guiEngine) { String guifile = guiEngine.getXmlfile(); UPSTypeHelper instance = (UPSTypeHelper) guiEngine.getGUIHelpMap().get(guifile ); if (instance == null) { instance = new UPSTypeHelper(guiEngine); guiEngine.getGUIMap().put(guifile , instance); } return instance; }
代碼行數差不多,但不用再手工調用 removeInstance 方法釋放實例了。Java 語言的一個重要特性就是垃 圾回收,當 GUIEngine 對象被銷毀後,附著在其上的對象也不再可達,輔助類就將被 GC 垃圾回收器回收。我們利用 Java 的垃圾回收特性實現了輔助對象的自動銷毀,不用再負責輔助類的生命周期的管理工作了,這不也正是 Java 的便捷特性之 一麼。
我們還可以再深入想想,解決自動映射的問題,是否可以自動地完成 GUIEngine 中的界面控件到輔助類中定 義的成員的映射呢?我們知道,完成這個映射工作的主要是輔助類中的 getAllComponents 方法 , 既然要自動完成映射, 這個方法首先挪開。
輔助類的自動映射
我們知道在 Java 的反射功能,為自動映射提供了可能性。從實際應 用中,可以判斷,在界面輔助類中,需要映射的都是控件類型,都派生自 JComponent 類,我們可以遍歷所有定義的 Java 成員,如果其類型為 Swing 界面控件類型,則從 GUIEngine 中提取相應的界面組件並進行成員賦值。當然為能夠正確地完 成界面組件自動映射的條件是:界面組件的名稱要與界面輔助類中定義的 public 成員名稱保持一致。實現方法很簡單,如 下面代碼所示:
清單 5. 輔助類自動映射實現關鍵代碼清單
private void reflectComponents(GUIEngine ge){ /* 遍歷類所有的屬性 */ Field[] fields = getClass().getFields(); try { for (int i = 0; i < fields.length; i++) { Type type=fields[i].getType(); String tmpstr=type.toString(); if (tmpstr.indexOf("javax.swing")!=-1){ fields[i].set(this ,ge.getComponentByName(fields[i].getName())); } } } catch (Exception iae) { iae.printStackTrace(); } }
由於這個方法對所有的輔助類都是公用的方法,所以可以抽象一個父類 UIHelp,在構造函數中,完成控件 屬性的自動映射功能。在每個界面輔助類中的 getAllComponents() 方法可以取消了,代碼更為的簡潔。給出 UIHelp 的代 碼:
清單 6. 優化後的 UIHelp 類實現代碼清單
public class UIHelp { public static <T extends UIHelp>T getInstance( GUIEngine ge,Class<T> cls){ T help=(T)ge.getGUIHelpMap().get(cls.getName()); try { if (help==null ){ help=cls.newInstance(); help.reflectComponents(ge,cls); String clsname= help.getClass().getName(); ge.getGUIHelpMap().put(clsname, help); } }catch (InstantiationException il){ }catch (IllegalAccessException ie){} return help; } private void reflectComponents(GUIEngine ge,Class<? extends UIHelp> cls){ // 該代碼前面已經給出,這裡不重復了。 } }
上面代碼分下面幾步:
在 GUIEngine 實例中,判斷是否創建有該界面對應的輔助類;
因為所有的輔 助類都是 UIHelp 的子類,所以通過泛型的方法返還的也是 UIHelp 的子類,傳遞的 Class 也是該子類,不用再單獨進行 類型轉換的判斷;
自動完成對傳遞進來的 輔助類的界面控件的自動映射;
將創建的輔助類,附著在 GUIEngine;
上述輔助類的代碼就非常簡單了:
清單 7. 清單 1 描述的界面的輔助類的實現
public class UPSTypeHelper extends UIHelp { public JButton btnOk;// 確定按鈕 public JButton btnCancel;// 取消按鈕 public JTextField txttest;// 文本框 public JLabel labeltest ;// 文本標簽 public UPSTypeHelper getInstance(GUIEngine ge){ return getInstance (ge,UPSTypeHelper.class ); } }
只需要在輔助類中,定義好控件類為 public 類型,調用 getInstance 就可以自動在父類中完成映射功能了。
功能的擴展
根據下面的示意圖和上面的代碼我們可以看出,一個 GUIEngine 其實不只可以附著一個輔助類 ,因為定義了一個 HashMap 來存放輔助類的實例,我們只要保證實例的名稱不一樣就可以了,這樣我們可以用幾個輔助類 來映射界面類。在配置數據的界面中,當字段非常多的時候,而且字段有明顯的分類比如通過 Tab 頁的形式,防止輔助類 過大,可以定義多個輔助類來完成。
圖 6 界面引擎與輔助類的映射關系示意圖
另外一種情況是,輔助類其實也可以復用, 當界面大部分相同時,我們可以對一些經常使用到的控件,創建公用的界面助手類。每個界面助手類只映射 GUIEngine 對 象的一部分界面控件,則助手類可以被更廣泛地重用
寄生模式的導出
根據上面的對輔助類內存洩露方案的處 理,可以推廣到所有存在寄生特征的設計場景中進行通用化,進而定義寄生模式。
圖 7. 寄生模式結構示意圖
宿主對象:為其他對象提供所需服務的。調用寄生對象提供服務並為它提供一個鉤子,以前面章節的實例對應可以理解 為,GUIEngine 是宿主提供 Outdoor_UPS.xml 文件描述的界面創建服務,實際是調用對應的輔助類來實現界面組件的訪問 服務的,輔助類會調用 GUIEngine 類,同樣 GUIEngine 也會掛接輔助類。
寄生對象:為宿主對象提供服務,如:為真實對象提供服務的模擬等;此處可以理解為,輔助類為界面對象提供模擬服 務,使得操作起來更方面和符合習慣。
開關變量:確定寄生對象如何提供服務的變量,我們可以認為其實宿主類本身是可以提供服務的,但有寄生類來提供更 為方便,但在某些情況下,以上文的案例,如果僅僅只需要臨時訪問某個界面的很少數量的控件,而界面本身控件數量很多 ,如果還創建寄生類就不太合算了,此時可以由宿主類直接提供服務;
創建者:創建宿主對象的對象,它將開關變量傳遞給寄生變量以動態決定誰提供服務;
客戶對象:使用宿主對象服務的對象,當開關變量被設置,則由寄生對象提供服務;
以前面的案例為例,我們想訪問並設置 UPS TYPE 界面中文本輸入控件的值為 100,首先創建一個 GUIEngine 對象,通 過開關變量,設置是否創建輔助界面類,如果創建,可以通過 UPSTypeHelper 來完成對界面值的設置了。
圖 8 寄 生模式對象調用順序
我們來看代碼的實現過程
清單 8. 寄 生模式的實現實例
public class Client { private Create ct; public Client(){ ct=new Create("Outdoor_UPS.xml",true );/* 打開開關輔助對象提供服務 */ } public boolean SetUPSTypeValue(int typeValue) { try { ct.getServiceObj().setText(String.valueOf (typeValue)); return true ; }catch (Exception ex){ ex.printStackTrace(); return false ; } } /** * @param args */ public static void main(String[] args) { Client clt=new Client(); System.out .println(clt.SetUPSTypeValue(100)); } }
使用者,主要是通過創建者,創建然後完成設置值的操作;
清單 9. 寄生模式的創建者的實現實例
public class Create { private String guixml=null ;/* 界面描述文件 */ public boolean Switch;/* 開關量是否創建寄生對象 */ private GUIEngine ge; public Create(String guifile,boolean open){ this .guixml=guifile; this .Switch=open; ge=new GUIEngine(this .guixml); ge.createJDialog(null , "Hello Test").setModal(false ); ge.getCurrJD().setVisible(true ); } /* 獲取要設置值的控件對象 */ public JTextField getServiceObj(){ if (Switch){ UPSTypeHelper upshelp= UPSTypeHelper.getInstance (ge, UPSTypeHelper.class ); return upshelp.txttest; }else { return (JTextField)ge.getComponentByName("txttest"); } } }
創建者,根據 open 這個開關量,決定是否創建寄生對象,如果不創建則調用宿主對象的自身方法使用,該代 碼的執行結果如下圖:
圖 9 代碼執行結果界面
顯示設置成功。
結束語
本文 所描述的編碼方式,簡化了程序開發,而且對於大型的基於 Swing 的 GUI 應用開發可以大幅度的降低代碼量,代碼更清晰 易懂。本文重點在於面向對象編程中對象生命期的一種管理方式,是對現有的代碼中不當的對象生命期管理方式的改進。一 旦了解,其實非常簡單。
相比較與代理模式和工廠模式,對象的創建可以更靈活動態確定。對寄生對象做了改變, 這些改變只會傳遞到與其相關的系統對象而不會影響其余的系統對象。而且提供一種界面輔助操作可重用的方法。
下載