雖然可以使用 deftype
、defrecord
或 reify
實作介面或協定,但是缺點是必須在定義型別時就確認,Clojure 提供了在建立型別之後,仍然可以將型別或協定擴充的方式。
我們可以使用 extend
函式擴充已經定義好的型別:
(defrecord Rectangle [x y])
;; => user.Rectangle
(def rect (Rectangle. 5 5))
;; => #'user/rect
(defprotocol Shape
(draw [this]))
;; => Shape
(extend Rectangle
Shape
{:draw (fn [this] (println "Draw Rectangle:" (.x this) (.y this)))})
(draw rect)
;; => Draw Rectangle: 5 5
範例中先創建 Rectangle 記錄類型,並以此記錄類型建立執行實體 (Instance) 後,定義了名爲 Shape 的協定,再讓 Rectangle 型態實作 Shape 協定。先在協定之前建立好的 Rectangle 記錄類型,便有了 Shape 協定的實作。
可以看到 extend
方法的寫法是,先寫上欲實作介面的類型名稱,再寫上欲實作的協定名稱,最後是加上映射,內容爲關鍵字與匿名函式,關鍵字名稱即是協定中函式的名稱。
extend
雖然很神奇很方便,但是定義實作函式的地方太繁瑣了,你可以利用 extend-type
簡化定義方式:
(extend-type Rectangle
Shape
(draw [this] (println "Draw Rectangle still using extend-type:"
(.x this)
(.y this))))
(draw rect)
;; => Draw Rectangle using extend-type: 5 5
還有 extend-protocol
,可以讓多個型別同時實作某個協定,而 extend-type
則是讓某個型別同時實作多個協定:
(extend-protocol AProtocol
AType
(method-from-AProtocol [this x]
(;.. implementation of AType
))
BType
(method-from-AProtocol [this x]
(;.. implementation of BType
))
CType
(method-from-AProtocol [this x]
(;.. implementation of CType
)))
以上只是示例,並無法實際在 REPL 執行
由 defrecord
與 defprotocol
的介紹,我們已經看到了主流物件導向語言如 Java/C++ 支持多型 (Polymorphism) 的方式,就是根據型態的不同,決定該執行的函式。
主流的物件導向語言使用繼承建立階層,以繼承階層實現多型。Clojure 的多型並不一定要綁定在型別上。除了協定和記錄類型之外,Clojure 還提供了更靈活的多型設計方法,稱爲多重方法 (Multi-method)。
要使用多重方法達到多型,首先必須先使用 defmulti
巨集。defmulti
包括函式名稱和一個分派函式 (Dispatch function),分派函式被調用後,返回值用來決定該使用哪個函式。
接下來使用 defmethod
定義多重函式。參數接受函式名稱、代表該函式應該被呼叫的分派值、和函式參數與函式本體。
以下範例使用多重方法,計算不同計酬方式員工的薪水。正職員工以月薪給付薪水,不管超過月平均工作時數與否,都是領月薪;而派遣員工則是以小時計酬,如果工作時數不滿 40 小時,便以時薪乘以工作時數給薪,如果超過 40 小時,則給薪方式爲 40 小時時薪,再加上超過 40 小時的工作時數乘以時薪再乘以 1.5 倍:
(defrecord Employee [type hours salary])
(defmulti earnings
(fn [employee] (.type employee))) ; 1
(defmethod earnings
:salaried ; 2
[employee] (.salary employee))
(defmethod earnings
:hourly
[employee]
(let [hours (.hours employee)
salary (.salary employee)]
(if (< hours 40)
(* hours salary)
(+ (* 40 hours)
(* (- hours 40) salary 1.5)))))
(earnings (Employee. :salaried 70 30000)) ; 3
;; => 30000
(earnings (Employee. :hourly 50 200))
;; => 11800.0
以上範例在步驟 1
的地方爲此多重方法的分派函式,此函式的返回值決定該執行哪個函式;步驟 2
則是分派值,當分派函式的返回值與這個值一樣,便執行此處的函式。第一個計算的是正職員工的薪水,再來是派遣員工的薪水。步驟 3
呼叫 earnings
函式的參數會先丟給分派函式求得分派值,再根據分派值選擇適當的函式。
由於多重方法中的分派函式可以是任何函式,因此可以有各種變化,不像主流物件導向語言只能依據類型與階層完成單一分派 (Single dispatch)。
Clojure 提供一些具有反射 (Reflective) 能力的函式,用來檢查或驗證型態與協定之間的關係。首先介紹的是 extends?
函式,參數接受協定和型別,結果爲該型別是否擴充提供的協定:
(defprotocol Vehicle (go [this]))
(defrecord Car []
Vehicle
(go [this] "Go car"))
(extends? Vehicle Car)
;; => true
extenders
則是列出有哪些型別以 extend
相關的函式實作當作參數的協定:
(defrecord Motorcycle [color])
;; => user.Motorcycle
(defrecord Truck [color])
;; => user.Truck
(extend-protocol Vehicle
Motorcycle
(go [this] "Go motorcycle")
Truck
(go [this] "Go truck"))
;; => (user.Motorcycle user.Truck)
(extenders Vehicle)
;; => (user.Motorcycle user.Truck)
satisfies?
接受協定與執行實體爲參數,如果執行實體以 extend
相關函式實作此協定則返回真,反之則否:
(satisfies? Vehicle (Truck. "red"))
;; => true
(satisfies? Vehicle 123)
;; => false
經由本篇文章你了解到如何在 Clojure 中自定型別,和建立相似於 Java 中介面的協定;也知道了擴充協定的方法,更了解了比物件導向的繼承式多型還強大的多重方法,並知道了一些反射方法,取得型別與協定之間的關係。
還不賴吧?今天就先到這裡,下一篇文章再見囉!
(本篇文章同步刊登於 GitHub,歡迎在文章下方留言或發送 PR 給予建議與指教)