Java語言為避免C++多重繼承的複雜性,刻意僅保留單一繼承與介面,極少提供擴展機制。但Java下一代語言(Groovy、Scala、Clojure)卻擁有豐富的擴展潛力。本系列三文將探索「不依賴繼承擴展既有類別」的多種路徑,首期聚焦「向既有類別添加方法」的三大核心技術。
Philip Wadler提出的「表達式問題」直指痛點:如何在不重編既有碼、不使用型別轉換或if判斷的前提下,為類別層級添加新功能?
實例:系統原假設長度單位為「米」,合併對手採用「英尺」。解決方案之一是擴展Integer添加轉換方法,使雙格式無縫切換。現代語言提供三種主流解法:
借鑒Objective-C「分類」概念,類別類別為含靜態方法的普通類別,首參數標示擴展類型:
class IntegerConv {
static Double getAsMeters(Integer self) { self * 0.30480 }
static Double getAsFeet(Integer self) { self * 3.2808 }
}
使用需包裹在use區塊,限制詞法作用域:
@Test void test_conversion_with_category() {
use(IntegerConv) {
assertEquals(1 * 3.2808, 1.asFeet, 0.1)
assertEquals(1 * 0.30480, 1.asMeters, 0.1)
}
}
亮點:
getAsMeters()可作1.asMeters調用,構建DSL常用技巧。類別類別的詞法限制阻礙Grails框架開發,ExpandoMetaClass應運而生——懶惰實例化的擴展持有者,可從任何類別「生長」新行為:
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
}
}
靜態初始化器確保擴展優先發生。兩機制均優先調用擴展方法,得以添加、修改、甚至刪除既有方法:
@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)
}
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")
調用1.asM時,Scala發現Int無此方法,檢查隱式轉換自動包裝為UnitWrapper。同樣支援省略括號的語法糖(語言特性而非命名慣例)。
隱式轉換精妙且類型安全,但無法修改或刪除既有方法,這是與開放類別的關鍵差異。
Clojure結合extend函數與協議(概念類似Java介面:純簽名集合)解決問題。雖非OO語言,但可與類別互操作、將方法映射為函數:
(defprotocol UnitConversions
(asF [this]) (asM [this]))
(extend Number UnitConversions
{:asF (fn [this] (* this 3.2808))
:asM #(* % 0.30480)})
REPL驗證:(println "1 foot is " (asM 1) " meters") → 1 foot is 0.3048 meters。匿名函數兩種語法(fn與#(... % ...))並存。
(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) ...)))
extend-protocol宏分組協議擴展,_忽略不關心參數。為nil、String、APersistentMap、IFn等類型統一定義呈現語義,極具實用價值。
三大語言以不同技術(開放類別、包裝器、協議)實現既有類別的乾淨擴展。但表達式問題遠不止類型擴充——下期將探討協議進階功能、特徵與混入機制。