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 中的示例使用了這種技術:
清單 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 的擴展。