在 Java 語言中獲得單繼承編程的安全性需要付出極大的代價:有時必須沿著繼承層次結構中的多條路徑復制代碼。要重新獲得單繼承 Java 代碼中所失去的大多數表示,我們可以將 mixin集成為一個擴展。本月,Eric Allen 解釋了 mixin(那些由它們的父類參數化的類)的概念,以及它們如何能協助單元測試。他還描述了基於 mixin 編程的工具,並討論了將 mixin 添加到您的 Java 代碼中的可能方法。
自從面向對象的編程出現以來,OO 語言設計中一直存在著一個困擾人的基本問題。一方面,我們在域分析過程中開發的本質是有意使用從多個父類繼承的類。那是因為實際世界中的對象不會剛好適合一個簡單的單繼承層次結構。您最喜愛的啤酒或許口感既好純度 又比較高。另一方面,在編程語言中允許多重繼承的結果是語義極其復雜。
在語言中引入這樣的復雜性往往會使發生錯誤的概率增加,因此 Java 語言已經堅持采用單繼承的方法(接口繼承除外,其中的語義要簡單得多)。其結果是,Java 程序中的許多類結構要麼包含沿著繼承層次結構的多個分支復制的代碼,要麼包含通過使用責任鏈(Chain of Responsibility)設計模式、命令(Command)設計模式或策略(Strategy)設計模式而添加的各個間接級別。
例如,請考慮下面這個用於 GUI 庫可滾動窗格的 UML 分析圖示例:
圖 1. 選擇 GUI 元素的分析圖
理想情況下,我們希望將這個圖直接轉換成 Java 編程中的類層次結構。但是,因為 Java 編程是單繼承,所以我們不能這麼做。就算多重接口繼承允許我們構造對應的接口集,但是實現這些接口的類不能直接遵循該結構。另一種方法是,我們要麼必須沿著繼承層次結構中的多條路徑復制代碼,要麼使用策略模式(或其它使用限制的一些訣竅)來避免復制代碼。這兩種方法都不能完全讓人滿意。
博采眾長
但是如果多重繼承太容易出錯,而單繼承又太局限,那麼在 Java 編程中是否可以添加一些語言特性,這些語言特性會向我們提供集中這兩種方法的優點呢?答案是有的 ― 它就是 mixin。
mixin 是那些由它們的父類參數化的類。它們也可以被認為是將類映射到新子類的函數。根據特定上下文的要求,可以用不同的父類實例化 mixin。
例如,如下所示,通過使用 mixin 可以實現圖 1 中 ScrollPane 的類層次結構(其中,存在定向的虛線代表從 mixin 到父類的實例化關系):
圖 2. mixin 繼承圖
在圖 2 中,我們已經將類 Scrollable 轉換成 mixin,它可以繼承不同上下文中的不同類。在這個上下文中,我們實例化 Scrollable 以繼承 Pane ,來創建 ScrollPane 。我們也可以實例化 Scrollable 以繼承 Dialog ,而且我們可以對它實例化以繼承不同上下文所需的所有種類的其它 GUI 組件。
mixin 的簡史
詞語 mixin的初次使用源自 Lisp 社區。它被用於 CLOS 的主流中,實際上它在其中是一種設計模式,嘗試控制這種語言的多重繼承所帶來的不便。mixin 設計模式也已被 C++ 社區用於同樣目的。
之所以使用 mixin這個名稱,是因為這樣的類可以以各種方式與其它類混合在一起。盡管 mixin 只是這些語言中的一種設計模式,但在語言級別上支持它們應該是毫無問題的。對於要將 mixin 添加到 Java 語言,已經提出了許多建議,但迄今為止最受歡迎的建議是使用 Jam,這是一種使用 mixin 的 Java 擴展,它是由意大利研究人員 Davide Ancona、Giovanni Lagorio 和 Elena Zucca 提出的。
mixin、Java 代碼和 Jam:不僅僅是為了早餐
Jam 是一種向後兼容的 Java 平台 V1.0 擴展(帶有兩個新關鍵字: mixin 和 inherited )。無可否認,除非您正在將 Java 程序改寫成 .NET 程序,否則您可以使用這種語言相當舊的版本,但是基本設計可以延用至各個更新的版本。
所提供的實現作為 Jam 到 Java 語言轉換程序。注: jamc 實現不執行完整的程序類型檢查。與此相反,它轉換成 Java 源代碼,並依賴 Java 類型檢查器來捕獲類型錯誤。這使 Jam 實現更簡單,但是這也意味著要診斷從編譯器上取回的錯誤消息會比較困難,因為我們已在實際編寫的源代碼上刪除了這一步驟!最後,獨立的 Jam 類型檢查器對於生產使用是不可或缺的。
在 Jam 中,使用 mixin 類 def 內的聲明來聲明父類所需的方法,類似於: inherited <signature> 。
mixin 的實例化可以這樣編寫: class NAME = MIXIN extends CLASS {CONSTRUCTOR*}
CONSTRUCTOR 產品尾部的 * 意味著該產品可以不存在,也可以存在更多。如果在 mixin 實例化中沒有指定任何構造器,那麼就假定是缺省的不帶參數的(zeroary)構造器。
例如,如下所示,編寫 UML 圖(圖 2)中使用的 mixin(其中,我們在 Panes 中包含了 setVisible() 方法,在 mixin Scrollable 中包含了 maxScrollSize 字段):
清單 1. 在 Jam 中實例化 mixin
class Pane {
...
void setVisible(boolean value) {
...
}
}
class DialogBox {
...
}
mixin Scrollable {
int maxScrollSize;
inherited void setVisible(boolean value);
}
class ScrollDialog = Scrollable extends DialogBox {
ScrollDialog() {
this.maxScrollSize = 10;
}
}
class ScrollPane = Scrollable extends Pane {
ScrollPane(int maxScrollSize) {
this.maxScrollSize = maxScrollSize;
}
}
Jam 遵循著名的“用於 mixin 的復制原則”:
通過實例化父類P 上的 mixinM 而獲得的類應該具有與P 的一般繼承者相同的行為,其主體包含M 中定義的所有組件的副本。
盡管 mixin 的概念已經應用到了許多語言中,但是 Jam 還是很新穎,因為它在嚴格類型化的語言上下文中嚴格引入了基於 mixin 的編程。Jam 中的 mixin 與普通類相似,都定義類型;mixin 實例化擁有 mixin 的類型和父類的類型。一個 mixin 可以實現多個接口。
在 mixin 中不能聲明構造器,它只適用於 mixin 實例化。就象 Jam 的設計人員所聲明的,不允許構造器作為設計選擇,因為它們“與它們自己類的實現緊密聯系在一起,所以它們的說明往往變得非常不一般了。”
要注意這種語言的一些常規特性:
通過使用與標准 Java 語言所用的相同規則可以訪問字段成員。
靜態成員與 mixin 的實例化相關聯;沒有“可共享”的 mixin 靜態成員。
另外,Jam 對 mixin 的實例化強加了五個約束:
非法覆蓋/隱藏。 如果一個父類相應的“已復制”類合法,那麼允許對這個父類上的方法進行意外(也稱為“偶然”)覆蓋(更確切地說,方法不會擁有和父類中已覆蓋的方法相同的 arg類型,而是其它 return類型,或者可以擁有其它 throws子句,或諸如 靜態vs. 實例那樣不兼容的修飾語)。
不明確的重載。不明確的重載是個問題,因為方法參數可能是 mixin 類型,它允許兩個已重載的方法可用並且這兩個方法都不是比較特定的情況。如果除了某些參數具有兩種不同引用類型以外,這兩個方法擁有相同數目和類型的參數,那麼通過禁止重載可以解決這個問題。
方法注釋。用“Parent”類型來注釋被繼承的方法。
僅類實例化。只能根據類來實例化 Jam mixin;與 Jiazzi 中的組件不同的是,Jam 中沒有 mixin 組合的概念(但是,Jam 團隊有意探究這樣的擴展)。有關 Jiazzi 和基於組件編程的更多信息,請參閱 參考資料。
不能傳遞“this”。可能的顯示阻塞(show-stopper)將“this”作為參數從 mixin 內部傳遞給方法或構造器,這是被禁止的!這個 Jam 特性是保護類型系統的穩固性所必不可少的。沒有它,就無法確保 Jam mixin 類型將在所有可能的實例化上都是有效的。然而它仍是一個非常遺憾的約束,因為它限制了適合於轉換成 mixin 的類集合。
內幕:對實現的簡要一瞥
要轉換成 Java 語言,Jam mixin 類型就要被表示成接口,這是由實例化來實現的(所有實例化都是靜態的)。要處理 mixin 中引入的字段,在接口中引入了 getter/setter 方法: M_$get$_f 和 M_$set$_f 。然後在每個實例化中將 f 聲明為字段,並相應實現這兩個方法(同樣,對 來自外部代碼的靜態類型 M 的表示進行的所有字段訪問都轉換成調用 getter/setter)。mixin 中的靜態字段不可以在各個 mixin 實例化上共享,因此只能分別將它們插入到每個實例化中。
mixin 的每個實例化被編譯成獨立的 Java 類;各個副本上不存在任何共享的字節碼。還為 mixin 的父類構造了一個接口。這個父類接口是由 mixin 接口繼承而來的(而不是由父類的實例化繼承而來的)。
mixin 和單元測試
mixin 的每個實例化被編譯成獨立的 Java 類;各個副本上不存在任何共享的字節碼。還為 mixin 的父類構造了一個接口。這個父類接口是由 mixin 接口繼承而來的(而不是由父類的實例化繼承而來的)。
mixin 一般作為一種重新獲得一種語言中多重繼承的強大功能,同時不帶有任何缺陷的方法來激勵程序員。但是很重要的是,要注意它們還向我們提供了測試現有類的新繼承的功能強大的方法,特別是當父類的本質是很難對它直接進行測試的時候,此方法很有用(如同在 GUI 元素或 RMI 代理類中)。
事實上,就如同 Jiazzi 向我們提供了在與這些包導入的包無關的情況下測試這些包的方法,Jam(或任何其它基於 mixin 的Java 語言擴展)允許我們在與父類無關的情況下測試這些類,即使那些父類存在於同一個包中。執行清單 3 中的示例,我們可以用 Recorder 為只記錄所有超級方法調用的父類實例化我們的 mixin:
清單 2. 與 mixin 實例化無關的情況下測試 mixin
class TestLog {
private StringBuffer recording = new StringBuffer("");
public void record(String message) {
recording.append(message);
}
public String toString() {
return recording.toString();
}
}
class WidgetRecorder {
public TestLog testLog;
public void setVisible(boolean value) {
testLog.record("setVisible(" + value + "); ");
}
}
class ScrollableWidgetRecorder = Scrollable extends WidgetRecorder {
public TestScrollable() {
this.maxScrollSize = 10;
}
}
隨後我們可以根據預期的調用序列檢查這個日志:
清單 3. 用於 mixin 的 JUnit TestCase
import junit.framework.*;
public class ScrollableTest extends TestCase {
public ScrollableTest(String name) {super(name);}
public void testSetVisible() {
ScrollableWidgetRecorder test = new ScrollableWidgetRecorder();
test.initialize();
assertEquals("Scrollable initialization should've called setVisible(true)",
"setVisible(true); ",
test.testLog.toString())
}
...
}
通過這種方式,我們能夠測試本身很難測試的類的繼承,而不用考慮這些類的父類位置在哪裡。隨後難以測試的核心功能被分離成一個小型的父類集合,而依賴該集合的功能就可以在完全通過測試的 mixin 類中輕松得到。
有關 mixin 和類屬類型的最後幾句話
最後,我疏忽了一點:在討論 Java 編程中的 mixin 時,至少應該簡要討論一下 mixin 如何與向 Java 添加類屬類型的 JSR-14 建議相關聯。
因為類屬類型允許由類所引用的類型來參數化這些類,Java 語言中對類屬類型的真正一流支持必須支持一種 mixin 形式,因為可以定義類以繼承類型變量。
遺憾的是,Sun 的 JSR-14 原型編譯器所用的方法禁止這樣的“一流”繼承,因為在靜態編譯過程中會擦除類屬類型;即使在運行時也不存在任何類屬類型信息。在 mixin 情形中,這意味著會根據類型變量的限制而擦除 mixin 的父類,很明顯,這不是我們想要的。
與此相反,類屬類型的 NextGen 公式(2002 年 12 月 Rice JavaPLT 會發布 beta 測試發行版)在運行時使類屬類型信息保持可用。因此可以繼承它以支持一流的類屬類型,包括 mixin。事實上,在首個 beta 測試發行版之後不久的擴充版本中,應該只包含這樣的功能。在 參考資料中可以獲得已擴展語言的設計。
正如本文及上一篇專欄文章所演示的,當前的 Java 語言不是語言設計的終結者,特別是當我們使用測試優先的編程風格時。還存在許多功能強大的、自然語言的擴展,它們允許我們更迅速更全面地測試程序。
盡管如此,但令人高興的是,這兩篇文章都說明了 Java 語言所提供的巨大靈活性和可擴展性。這個擴展性是該語言和 JVM 設計的安全性和可移植性直接帶來的結果。因為最初的設計人員很有遠見,所以 Java 語言將會證明,在將來很長一段時間內,它會保持是一種功能非常強大的、有意義的語言,當程序員構建日益復雜的應用程序時,它繼續向程序員提供服務。