Java下一代: 沒有繼承性的擴展,第 1 部分
Java 語言的設計有目的地進行了一定的刪減,以避免前代產品中已發現的一些問題。例如,Java 語言的設計人員感覺 C++ 中的多重繼承性帶來了太多復雜性,所以它們選擇不包含該特性。事實上,他們在該語言中很少構建擴展性選項,僅依靠單一繼承和接口。
其他語言(包括 Java 下一代語言)存在巨大的擴展潛力。在本期和接下來的兩期文章中,我將探索擴展 Java 類而不涉及繼承性的途徑。在本文中,您會了解如何向現有類添加方法,無論是直接還是通過語法糖 (syntactic sugar)。
表達式問題
表達式問題是最近的計算機科學歷史上的一個眾所周知的觀察結果,首創于貝爾實驗室的 Philip Wadler 的一篇未發表的論文(參見 參考資料)。(Stuart Sierra 在其 developerWorks 文章 “通過 Clojure 1.2 解決表達式問題” 中出色地解釋了它。在這篇文章中,Wadler 說道:
表達式問題是老問題的新名字。我們的目標是通過案例定義數據類型,在這里,在不重新編譯現有代碼的情況下,您可以將新的案例添加到數據類型和數據類型的新函數中,同時保留靜態類型安全(例如,沒有轉換)。
換句話說,您如何向一個分層結構中的類添加功能,而不求助于類型轉換或 if
語句?
我們將通過一個簡單的例子來表明表達式問題在真實世界中的表現形式。假設您公司始終假設應用程序中的長度單位為米,沒有在您的類中為任何其他長度單位構建任何功能。但是,有一天,您公司與一家競爭對手合并了,而這個競爭對手始終假設長度單位為英尺。
解決該問題的一種方法是,通過使用轉換方法擴展 Integer
,使兩種格式之間的切換變得無關緊要。現代語言提供了多種解決方案來實現此目的;在本期中,我將重點介紹其中的 3 種:
- 開放類
- 包裝器類
- 協議
Groovy 的類別和 ExpandoMetaClass
Groovy 包含兩種使用開放類 擴展現有的類的不同方式,“重新開放” 一個類定義來實現更改(例如添加、更改或刪除方法)的能力。
類別類
類別類(一種借鑒自 Objective-C 的概念)是包含靜態方法的常規類。每個方法至少接受一個參數,該參數表示方法擴充的類型。如果希望向 Integer 添加方法,例如我需要接受該類型作為第一個參數的靜態方法,如清單 1 所示:
清單 1. Groovy 的類別類
- class IntegerConv {
- static Double getAsMeters(Integer self) {
- self * 0.30480
- }
- static Double getAsFeet(Integer self) {
- self * 3.2808
- }
- }
清單 1 中的 IntegerConv
類包含兩個擴充方法,每個擴充方法都接受一個名為 self
(一個通用的慣用名稱)的 Integer
參數。要使用這些方法,我必須將引用代碼包裝在一個 use
代碼塊中,如清單 2 所示:
清單 2. 使用類別類
- @Test void test_conversion_with_category() {
- use(IntegerConv) {
- assertEquals(1 * 3.2808, 1.asFeet, 0.1)
- assertEquals(1 * 0.30480, 1.asMeters, 0.1)
- }
- }
清單 2 中有兩個特別有趣的地方。首先,盡管 清單 1 中的擴展方法名為 getAsMeters()
,但我將它稱為 1.asMeters
。Groovy 圍繞 Java 中的屬性的語法糖使我能夠執行 getAsMeters()
方法,好像它是名為 asMeters
的類的一個字段一樣。如果我在擴展方法中省略了 as
,對擴展方法的調用需要使用空括號,就像 1.asMeters()
中一樣。一般而言,我喜歡更干凈的屬性語法,這是編寫特定于域的語言 (DSL) 的一種常見技巧。
清單 2 中第二個需要注意的地方是對 asFeet
和 asMeters
的調用。在 use
代碼塊中,我同等地調用新方法和內置方法。該擴展在 use
代碼塊的詞法范圍內是透明的,這很好,因為它限制了擴充(有時是一些核心)類的范圍。
ExpandoMetaClass
類別是 Groovy 添加的第一種擴展機制。但事實證明對構建 Grails(基于 Groovy 的 Web 框架)而言,Groovy 的詞法范圍限制太多了。由于不滿類別中的限制,Grails 的創建者之一 Graeme Rocher 向 Groovy 添加了另一種擴展機制:ExpandoMetaClass
。
ExpandoMetaClass
是一種懶惰實例化的擴展持有者,它可從任何類 “成長” 而來。清單 3 展示了如何使用 ExpandoMetaClass
,為我的 Integer
類實現我的擴展:
清單 3. 使用 ExpandoMetaClass
擴展 Integer
- class IntegerConvTest{
- static {
- Integer.metaClass.getAsM { ->
- delegate * 0.30480
- }
- Integer.metaClass.getAsFt { ->
- delegate * 3.2808
- }
- }
- @Test void conversion_with_expando() {
- assertTrue 1.asM == 0.30480
- assertTrue 1.asFt == 3.2808
- }
- }
在 清單 3 中,我使用 metaClass
holder 添加 asM
和 asFt
屬性,采用與 清單 2 相同的命名約定。對 metaclass 的調用出現在測試類的一個靜態初始化器中,因為我必須確保擴充操作在遇到擴展方法之前發生。
類別類和 ExpandoMetaClass
都在內置方法之前調用擴展類方法。這使您能夠添加、更改或刪除現有方法。清單 4 給出了一個示例:
清單 4. 取代現有方法的擴展類
- @Test void expando_order() {
- try {
- 1.decode()
- } catch(NullPointerException ex) {
- println("can't decode with no parameters")
- }
- Integer.metaClass.decode { ->
- delegate * Math.PI;
- }
- assertEquals(1.decode(), Math.PI, 0.1)
- }
清單4 中的第一個 decode()
方法調用是一個內置的靜態 Groovy 方法,它設計用于更改整數編碼。正常情況下,它會接受一個參數;如果調用時沒有任何參數,它將拋出 NullPointerException
。但是,當我使用自己的 decode()
方法擴充 Integer
類時,它會取代原始類。
Scala 的隱式轉換
Scala 使用包裝器類 來解決表達式問題的這個方面。要向一個類添加一個方法,可將它添加到一個幫助類中,然后提供從原始類到您的幫助器的隱式轉換。在執行轉換之后,您就可以從幫助器隱式地調用該方法,而不是從原始類調用它。清單 5 中的示例使用了這種技術:
#p#
清單 5. Scala 的隱式轉換
- class UnitWrapper(i: Int) {
- def asFt = {
- i * 3.2808
- }
- def asM = {
- i * 0.30480
- }
- }
- implicit def unitWrapper(i:Int) = new UnitWrapper(i)
- println("1 foot = " + 1.asM + " meters");
- println("1 meter = " + 1.asFt + "foot")
在清單5中,我定義了一個名為 UnitWrapper
的幫助器類,它接受一個構造函數參數和兩個方法:asFt
和 asM
。在擁有轉換值的幫助類后,我創建了一個 implicit def
,實例化一個新的 UnitWrapper
。要調用該方法,可以像調用原始類的一個方法那樣調用它,比如 1.asM
。當 Scala 未在 Integer
類上找到 asM
方法時,它會檢查是否存在隱式轉換,從而允許將調用類轉換為一個包含目標方法的類。像 Groovy 一樣,Scala 擁有語法糖,因此我能夠省略方法調用的括號,但這是一種語言特性而不是命名約定。
Scala 中的轉換幫助器通常是 object
而不是類,但我使用了一個類,因為我希望傳遞一個值作為構造函數參數(object
不允許這么做)。
Scala 中的隱式轉換是一種擴充現有類的精妙且類型安全的方式,但不能向開放類一樣,使用這種機制更改或刪除現有方法。
Clojure 的協議
Clojure 采用了另一種方法來解決表達式問題的這個方面,那就是結合使用 extend
函數和 Clojure 協議 抽象。協議在概念上類似于一個 Java 接口:一個沒有實現的方法簽名集合。盡管 Clojure 實質上不是面向對象的,而是偏向于函數,但您可以與類進行交互(并擴展它們),并將方法映射到函數。
為了擴展數字以添加轉換,我定義了一個協議,它包含我的兩個函數(asF
和 asM
)。我可使用該協議 extend
一個現有類(比如 Number
)。extend
函數接受目標類作為第一個參數,接受該協議作為第二個參數,以及一個使用函數名為鍵并使用實現(以匿名函數形式)為值的映射。清單 6 顯示了 Clojure 單位轉換:
清單 6. Clojure 的擴展協議
- (defprotocol UnitConversions
- (asF [this])
- (asM [this]))
- (extend Number
- UnitConversions
- {:asF (fn [this] (* this 3.2808))
- :asM #(* % 0.30480)})
我可以在 Clojure REPL(interactive read-eval-print loop,交互式讀取-重新運算-打印循環)上使用新的擴展來驗證該轉換:
- user=> (println "1 foot is " (asM 1) " meters")
- 1 foot is 0.3048 meters
在清單6中,兩個轉換函數的實現演示了匿名函數聲明的兩種語法變體。每個函數只接受一個參數(asF
函數中的 this
)。單參數函數很常見,以至于 Clojure 為它們的創建提供了語法糖,如 AsM
函數中所示,其中 %
是參數占位符。
協議創建了一種將方法(以函數形式)添加到現有類中的簡單解決方案。Clojure 還包含一些有用的宏,使您能夠將一組擴展整合在一起。例如,Compojure Web 框架(參見參考資料)使用協議擴展各種類型,以便它們 “知道” 如何呈現自身。清單 7 顯示了來自 Compojure 中的 Renderable
的一段代碼:
清單 7. 通過協議擴展許多類型
- (defprotocol Renderable
- (render [this request]
- "Render the object into a form suitable for the given request map."))
- (extend-protocol Renderable
- nil
- (render [_ _] nil)
- String
- (render [body _]
- (-> (response body)
- (content-type "text/html; charset=utf-8")))
- APersistentMap
- (render [resp-map _]
- (merge (with-meta (response "") (meta resp-map))
- resp-map))
- IFn
- (render [func request]
- (render (func request)
- ; . . .
在清單 7中,Renderable
協議是使用單個 render
函數來定義的,該函數接受一個值和一個請求映射作為參數。Clojure 的 extend-protocol
宏(它可用于將協議定義分組到一起)接受類型和實現對。在 Clojure 中,您可使用下劃線代替不關心的參數。在清單7 中,這個定義的可看見部分為 nil
、String
、APersistentMap
和 IFn
(Clojure 中的函數的核心接口)提供了呈現指令。(該框架中還包含其他許多類型,但為節省空間,清單中省略了它們。)可以看到這在實踐中非常有用:對于您可能需要呈現的所有類型,您可將語義和擴展放在一起定義。
結束語
在本期中,我介紹了表達式問題,剖析了 Java 下一代語言如何處理以下方面:現有類的干凈擴展。每種語言都使用一種不同的技術(Groovy 使用開放類,Scala 使用包裝器類,而 Clojure 實現了協議)來實現類似的結果。
但是,表達式問題比類型擴充更深刻。在下一期中,我將繼續討論使用其他協議功能、特征和 mix-in 的擴展。
原文鏈接:http://www.ibm.com/developerworks/cn/java/j-jn5/index.html