程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 診斷Java代碼: 消除包間的耦合關聯

診斷Java代碼: 消除包間的耦合關聯

編輯:關於JAVA

測試優先編程(test-first programming)中反復遇到的一個問題是,似乎不可能對程序的許多部分進行自動測試。尤其當程序在很大程度上要利用外部資源和庫時,似乎很難對它進行測試,因為沒有很好的方法來模擬程序與這些外部資源的連接。

然而,雖然只使用 Java 代碼很難測試這樣的程序,但有一種類型的編程(帶有開發工具)可以解決這個問題 ― 基於組件的編程。

基於組件的編程和 Java 語言

我所說的基於組件的編程是指什麼?我只是指,編程時程序的各個單元處於分布狀態,而不是象 JavaBeans 或類似技術這樣的運行時“組件”。

從概念上講,這些分布的各個單元大致類似於 Java 包。然而,Java 語言中的包非常受限,因為它們相互之間是耦合的。每個包中的類與它們導入的包之間是硬連接的(因為這些類必須顯式地引用所導入的包)。

由於這些包之間是相互耦合的,因此很難統一用提供同一功能的其它包的引用來替代程序中這些包的引用。

同樣,獨立開發的各個團隊可能偶爾會用到重復的包名,這些團隊試圖使用對方的包時,就會引起問題。為確保包名的唯一性,Sun 強烈主張每個開發團隊使用這樣的約定:用倒序排列的因特網地址作為團隊開發的所有包的前綴。開發人員通常都遵守這個約定,但未必總是如此。

然而即便嚴格遵守了這個包命名約定,仍然有其它一些原因使程序員想解除組件之間的耦合。其中一個原因是,這樣可以更有效地測試這些組件 ― 在談到基於組件的編程工具(Jiazzi 組件系統)時,會解釋這一點。

Jiazzi:針對 Java 語言的組件系統

Jiazzi 是一個富有前途的、用 Java 語言進行基於組件編程的系統,它與 JVM 完全兼容,並且完全解除了各組件間的耦合,它是猶他大學計算機科學系所開發的一個項目。這個系統使程序員可以疊加組件,並在現有 Java 代碼之上將這些組件連接起來。而不需修改 Java 語言或 JVM。

開發人員的描述

Jiazzi 開發人員是這樣描述的:

……是這樣一個系統,支持用 Java 編寫的大規模二進制組件的構造 [添加了對用 Java 編寫的大規模二進制組件的支持]。可以將 Jiazzi 組件看成是對 Java 包的泛化,同時向這些 Java 包添加了外部鏈接和獨立編譯的支持。Jiazzi 組件很實用,因為它們是從標准 Java 源代碼構造出來的。Jiazzi 既不需要對 Java 語言擴展,也不需要對編寫 Java 源碼進行特殊的約定,這些擴展和約定將寫在組件內部。我們的組件是富有表現力的,因為 Jiazzi 支持循環組件鏈接和 mixin,在開放的類模式中一起使用了循環組件鏈接和 mixin,這種模式支持將具有新特性的模塊添加到現有的類。

當前的 Jiazzi 實現用 鏈接程序(linker,用於操作組件)和 存根生成器(使 Jiazzi 可以與常規的 Java 源碼編譯器共同使用)集成進了 Java 平台。Jiazzi 中的組件可以包含、導入和導出 Java 類,可以跨組件邊界使用 Java 平台的用於繼承的語言內支持。除了富有表現力之外,這些組件還很健壯 ― 可以分別對組件的實現和鏈接進行類型檢查。

觀察解除組件的耦合

讓我們研究一個 Java 包 view 的簡短示例,來看一下 Jiazzi 如何解除組件間的耦合,這個示例用到了 GUI 庫包。我們將調用 toolkit 包。為了引用該包中的所有類,在我們的包中源文件的開頭,放置了一條 import 語句:

package view;
import toolkit.*;
...

通常,這會將 view 包與 toolkit 包聯系起來。但可以設想一下,我們希望編譯這個源文件,卻不能確定我們實際上正在導入哪個包。我們沒有將 toolkit 與 view 包硬連接起來,而是設想在這兩個包之上定義一個函數,它可以接受 toolkit 包,並返回與 toolkit 包相關的 view 包。在 Jiazzi 中,這些函數稱為 單元(unit)。

單元類似於 LEGO 積木;可以將它們拼裝在一起創建一個程序。如果將單元視為函數,則可以說,Jiazzi 提供了 函數復合(functional composition)。每個單元接受一個或多個帶有指定“包簽名”的包,同樣,可以用指定的簽名導出一個或多個包。

包簽名類似於類型;它們限制了包的形狀。包簽名會定義包中所期望的類以及這些類的方法簽名等。導出包的簽名可以取決於導入包的簽名。

有兩類單元:

原子(atom),是包之間的簡單映射

復合(Compound),是其它單元的組合

原子描述了直接導入和導出的包。復合從所組合的單元繼承了導入和導出的包。如果將單元視為 LEGO 積木,那麼原子就是單個的 LEGO 積木,復合就是從多個 LEGO 積木構建而來的結構。

在單獨的文件中,用特定的規范語言描述單元。該語言給單元輸入和輸出的包分配名稱。例如,這裡有一個簡單的原子單元,它接受 toolkit 包,並輸出 view 包:

atom app_view {
   import toolkit: toolkit_s;
   export view: view_s;
}

這個單元稱為 app_view 。它將“toolkit”名稱分配給它所導入的包。聲明這個包以與 toolkit_s 這個特定的包簽名相匹配。該單元導出的包稱為 view ,並聲名這個包以與 view_s 簽名匹配。

正如前面所提到的,包簽名類似於類型;它們限制了可能傳遞給某個單元(或從某個單元返回)的參數的類型。例如, toolkit_s 簽名可以象這樣指定一組類(這些類非常類似於 javax.swing 包中的類):

signature toolkit_s {
   class Frame {
     public Container getContentPane();
     public Component getGlassPane();
     ...
   }
   class OptionPane {
     public Object getDialog();
     ...
   }
   ...
}

我們還可以象這樣指定 view_s 簽名:

signature view_s {
   class EditorPane {...}
   class InteractionsPane {...}
   ...
}

當然,在簽名中所引用的某些類本身可能在單獨的包中。為了解除特定包中包簽名間的耦合,Jiazzi 允許對包簽名使用 參數。在簽名名稱後的尖括號內是簽名的包參數。例如,我們可能希望 toolkit_s 簽名使用 awt 包參數,如下所示:

signature toolkit_s<awt> {
   class Frame {
     public awt.Container getContentPane();
     public awt.Component getGlassPane();
     ...
   }
   class OptionPane {
     public Object getDialog();
     ...
   }
   ...
}

然後,我們必須設計新的簽名(稱為 awt_s ),並修改 app_view toolkit 來接受 awt 包,並用這個包實例化 toolkit_s 簽名:

atom app_view {
   import
     my_toolkit: toolkit_s%lt;awt_s>;
     my_awt: awt_s;
   export my_view: view_s;
}

接著,Java 源文件可以引用 my_toolkit 和 my_view 這兩個已經分配的名稱,就好象它們是真正的包名一樣。事實上,Jiazzi 可以讓我們在根本不做任何修改的情況下重新編譯上面的源文件,以引用這些已經分配的名稱!(請繼續閱讀下面的內容,其中解釋了如何這樣做。)

象 LEGO 積木一樣,可以使用復合單元拼裝單元。復合單元以特殊的“link”子句將其它單元組合起來,其中“link”子句標識出由一些單元導出的類,而這些類是由其它單元導入的。例如,可以將 app_view atom 與 default_toolkit 和 default_awt 單元按如下方式組合起來:

atom default_toolkit {
   import my_awt: awt_s;
   export my_toolkit: toolkit_s<my_awt>;
}
atom default_awt {
   export my_awt: awt_s;
}
compound app {
   export my_view: view_s;
   local v: app_view, a: default_awt, t: default_toolkit;
   link
     a@my_awt to t@my_awt, a@my_awt to v@my_awt,
     t@my_toolkit to v@my_toolkit,
     v@my_view to my_view;
}

注意單元 app 中的 local 子句。這條子句定義了表示“單元實例”的局部變量。這些單元實例中的類是實際鏈接的元素。通過鏈接單元實例(而不是直接鏈接單元),Jiazzi 可以清楚地表明 復合單元中的鏈接不影響各組成單元的定義。

通過為程序指定這些簽名、原子和復合,我們描述了如何將程序中的各個包鏈接在一起。Java 源文件是指單元所輸入和輸出的包,但在另一方面它們看上去象普通的 Java 文件。下一節將詳細講述如何使用 Jiazzi 來以單元所指定的方式將 Java 類真正鏈接在一起。

Jiazzi 的工作原理

Jiazzi 分三個階段編譯代碼:

首先,將一組簽名和單元定義傳送給 Jiazzi 存根生成器,然後,Jiazzi 存根生成器為傳遞給該生成器任何單元的簽名中所導入的所有類生成存根類文件。

然後,常規的 Java 編譯器使用這些類文件來編譯源文件,這些源文件對應於提供給存根生成器的單元所導出的類。

編譯完源文件後,Jiazzi 單元鏈接程序檢查結果類文件是否與原來單元中所聲明的類簽名匹配。必需要有這一步,因為:

Jiazzi 可以與任何第三方的編譯器共同使用

Jiazzi 從不檢查 Java 源文件

(順便說一句,請注意,Jiazzi 這種事實上從不檢查 Java 源代碼的方法有其優越之處。它使 Jiazzi 可以與用於 JVM 的非 Java 語言的編譯器一起使用,譬如 Jython、JSR-14 和 NextGen 編譯器。事實上,Jiazzi 本身就是用 JSR-14 編寫的。)

檢查完之後,組件鏈接程序為提供給它的每個單元生成一個 JAR 文件。這個 JAR 文件包含已編譯的源文件和存根以及作為元數據的簽名信息。接著,通過將這些 JAR 文件傳遞給 Jiazzi 及相應的復合單元,從而將這些 JAR 文件鏈接起來。

Jiazzi 單元鏈接程序是脫機工作的,並且單獨地工作在類文件常量池之上,而且會重命名隱藏的方法,從而避免了偶爾會發生的方法名稱沖突。

通過特定的類裝入器,還可以聯機鏈接單元。然而,由於不能使用編譯單元所依據的存根類,因此必須在類裝入器中進行類型檢查,作為“遞增性整體程序分析”。事實上,Jiazzi 程序員目前必須將脫機鏈接和聯機鏈接結合起來使用,因為在標准 Java 庫中,有許多類只能通過類裝入器來鏈接。

當前系統的另一個限制是,重命名會影響到 JNI 和反射庫。尤其是不能重命名本機方法,因為它們是用 C 語言編寫的。其結果是,許多類庫(那些過度依賴 JNI 和反射的庫)不能作為 Jiazzi 組件進行重新打包。

如上所說,復合單元描述其它單元間的連接。這些鏈接與該復合的導入和導出單元的綁定名稱相關;已鏈接的單元完全不會意識到已將它們鏈接起來了(鏈接程序最終會將這些復合單元宏展開成原子)。

通過這種方式,用簡單的重新編譯換入和換出新包,從而使我們可以創建和分發程序的 JAR 文件。而不需要涉及一行 Java 源代碼。

此外,其他 Jiazzi 用戶能夠在可以用 JAR 文件之前,根據程序所提供的類進行開發;他們所需要的只是相應導出的包簽名。他們對包所做的擴展也只與那個包簽名的任何其它實現相鏈接。

單元測試和 Jiazzi

基於組件的編程提供了許多優點。最常受到人們贊譽的優點是組件更大程度地方便了代碼重用。可以單獨分發組件成品,並按照自己的意願將其插進新的應用程序。而且這種類型的編程還使單元測試效率更高。

在測試時,可以使用特殊的“仿制品”組件將程序的各個組件鏈接起來,這些“仿制品”組件的類只是記錄測試組件的行為。實質上,這些仿制品組件充當了與記錄器(請參閱 參考資料中的“進行記錄器測試以正確調用方法”)類似的角色,但它是在組件這一層次上的。

被測試組件類似於笛卡爾的缽中之腦(brains in a vat);它們不能區分是與仿制品組件相連還是與真正的組件相連。例如,在先前的樣本應用程序中,我們可以編寫一個特殊的 test_app 復合,它將 app_view 單元與 test_toolkit 單元聯系起來,如下所示:

compound test_app {
   export my_view: view_s;
   local v: app_view, a: default_awt, t: test_toolkit;
   link
     a@my_awt to t@my_awt, a@my_awt to v@my_awt,
     t@my_toolkit to v@my_toolkit,
     v@my_view ot my_view;
}

test_toolkit 包會導出與 toolkit 相同簽名的包,但這個包只包含一些仿制的對象,使 app_view 認為它是與真正的 toolkit 相連。這些仿制對象可以記錄 app_view 的行為,因為可以在單元測試時檢查這些記錄。

將這個系統與 Java 包系統相比較會發現,在這個系統中,每個源文件必須顯式地聲明要導入的包。在 Java 包系統中,欺騙整個包鏈接到測試包的唯一方法是編輯所有的源文件,然後重新編譯這些文件。這一過程不能真正實現自動化測試。

遺憾的是,因為 Jiazzi 不能處理反射或 JNI(這一點是有情可原的),而且還因為許多內置的 Java API 廣泛地使用這些設施,所以不可能將現有的 API 包轉換成 Jiazzi 組件。但是,如果可以轉換成 Jiazzi,那麼我們能夠對程序執行更強大的測試。例如,在測試期間,可以將使用 Swing API 的程序連接到仿制的 Swing 組件,從而在沒有真正嘗試畫圖形對象的情況下,可以確保調用所有相應的 API。用類似的手段,我們可以測試與 java.io 包、Java3D、JDBC、RMI 以及任何其它 API 的交互,在這些 API 中,性能或功能的本質造成了客戶機難以進行測試。

好了,這是一個美好的夢想,是嗎?不過,即使反射和 JNI 不能讓我們使現有的 API 成為一流的 Jiazzi 單元,但仍可能有折衷的辦法。

雖然不能去耦現有 API 之間的連接,但可以去耦新單元與這些 API 的連接。實質上,可以將這些 API 與只導出類的特殊單元捆綁在一起;導入仍然與現有的 Java 包系統硬性地連接在一起。如果這些 API 是真正的單元,則這樣做可以實現 99% 的功能;只有那些想對現有 API 構建第三方選擇的程序員會對此不滿意。

可喜的是,Jiazzi 將朝著這個方向或通過類似的途徑變化著,這使我們可以和現有的 API 一起使用它。同時,它還為測試不使用反射或 JNI 的包提供了功能強大的機制。

去耦歷史

最初開發 Jiazzi 的動機是為特性的可擴展性創建一種機制 ― 將一個設計拆分成幾個特性(每個單獨的組件包含其中一個特性)。基於組件的編程主張,在以後的編程中,開發人員可以從組件供應商提供的成品組件上開發程序。具有通用簽名的組件就可以象汽車零部件一樣進行互換。

無論這一天是否會到來,即使沒有繁榮的組件軟件市場,基於組件編程仍有許多好處。尤其是,這種類型的編程使單元測試效率更高。

正如我們所展示的,用 Jiazzi 進行基於組件的編程提供了一種功能強大的測試程序組件的手段,並且它可以使用現有的 Java(或 Jython,或 JSR-14)源文件。我們可以從這一強大的工具中獲益。這一工具及其它類似的工具是面向測試編程的必要工具。

下次,我將討論 Jam(一種 Java 語言的擴展),它允許基於 mixin 的編程。就象 Jiazzi 提供了去耦包相關性的方法一樣,mixin 提供了去耦類相關性的方法。正如您可能猜到的,minxin 給我們提供了測試程序的另一種強大的機制。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved