此為過去的舊文,2014 年 5 月 3 日初次發表於 logdown。
今天離開 Lambda 相關的議題,看一下 Java 8 另一個和語言相關的改變:default methods。過去,Java 的介面只能宣告函式卻不能提供實作 (或是說是定義函式,定義函式和宣告函式的差別就是有沒有實作),因此大多數的 library 設計都將工具函式 (static utility methods) 放在類別中,這基本上沒什麼大問題,但是對有些潔癖的人來說,就會考慮要不要把該類別的建構子設為 private?
另外,Java 不支援多重繼承,但能實作多個介面,所以在Java的函式庫裡常會看到一種設計慣例:介面宣告需要的函式,透過抽象類別提供共用的預設實作,最後由預設類別提供完整的實作,例如:TableModel
宣告 JTable
所需要的最小函式集合,AbstractTableModel
提供部分共用的實作 (listeners的管理),最後 DefaultTableModel
提供一個完整的實作。這樣的慣例引起另一個問題:但是當 A 類別繼承了 B 類別,但又想使用 C 介面的抽象類別實作就沒辦法了 (因為不能多重繼承),A 類別只能自己實作 C 介面,某種程度上算是一種 duplicated code。
自己在設計 Comic Surfer 的過程中,為了讓既有的外掛和主程式能維持 binary compatibility,也採用上述的慣例,在公開的外掛 SDK 裡,同時放了介面和抽象類別,不過在開發文件上鼓勵開發者以繼承抽象類別的方式開發外掛,這樣的設計是當我想在介面中新增函式的宣告,同時在抽象類別中為新增的函式提供預設的實作,如此,即使外掛沒有用新的 SDK 進行修改,還是能在新版的 Comic Surfer 中掛載使用,而不會讓 JVM 抱怨說某個類別沒有提供完整的實作 (不相容)。
上述的問題,在這次的 default methods 得到了一個不錯的解法。首先,工具函式不需要再放在類別中了,如下所示,靜態函式的實作能直接放在介面中。
接著,抽象類別某種程度上可以被取代 (如果抽象類別帶有成員變數就沒辦法),假設要替溫度感測器設計一個介面,可以像 TemperatureSensor
介面,並提供二個 default methods:getTemperatureInCelsius()
和 getTemperatureInFahrenheit()
分別將絕對溫度轉成常用的攝氏溫度和華氏溫度 (這個例子是一個簡化的示意。不算是一個好設計,因為這轉換應該算是能抽離到外部變成一個獨立類別)。這時過去需要提供類似 AbstractTemperatureSensor
的抽象類別就完全不需要了,因為實作已經放在介面裡了。
不需要抽象類別,提供了一個設計上的彈性,例如要替一個溫度感測裝置控制器 (AbstractDeviceControl
) 寫 Adapter時,在沒有 default methods 前,因為單一繼承,要思考是讓 Adapter 繼承 AbstractDeviceControl
比較好,還是繼承 AbstractTemperatureSensor
比較好?當然,用 composition 解決更好,只是有時候會多寫些程式。現在可以像 TemperatureSensorDevice
用透過實作介面的方式取得預設實作,然後將唯一的繼承機會讓給其他可能需要的設計。
然後,如果要替介面新增函式,也可以透過直接在介面中提供預設實作,提供向下相容性。例如新增蘭金溫度表示法 (Rankine scale) 到 TemperatureSensor
中,並提供預設的實作,這時 TemperatureSensorDevice
不需做任何修改就能夠使用。
既然是介面,當然能用擴充 (extend) 的方式產生新介面,例如,某些國家習慣用華氏溫度,所以很多感測器都只提供華氏溫度,這時可以擴充 TemperatureSensor
介面產生新介面 FahrenheitTemperatureSensor
,在擴充的時候,可以用 abstract
關鍵字將一個有預設實作的 getTemperatureInFahrenheit()
重新宣告為抽象函式,或是複寫既有的實作,例如複寫 getTemperatureInCelsius()
變成直接取華氏溫度轉攝氏溫度,不用多轉一次絕對溫度然後再轉攝氏溫度。
其實,default methods 似乎也引起不小的討論,因為 default methods 加上可以實作多個介面,已經有點像 C++ 的多重繼承了,只差在沒辦法繼承成員變數而已,是好是壞就看怎麼使用了。我個人覺得還蠻方便的,如果不是因為 default methods 要到 Java 8 才有,我倒是考慮將 Comic Surfer 的外掛介面和工具函式類別用 default methods 改寫。