前言
在專案中我們一般會為實際問題域定義領域資料模型,譬如開發VDOM時自然而言就會定義個VNode資料型別,用於打包儲存、操作相關資料。clj/cljs不單內建了List
、Vector
、Set
和Map
等資料結構,還提供deftype
和defrecord
讓我們可以自定義資料結構,以滿足實際開發需求。
定義資料結構從Data Type和Record開始
提及資料結構很自然就想起C語言中的struct,結構中只有欄位並沒有定義任何方法,而這也是deftype
和defrecord
最基礎的玩法。
示例
(deftype VNode1 [tag props])
(defrecord VNode2 [tag props])
(def vnode1
(VNode1. "DIV" {:textContent "Hello world!"}))
;; 或 (->VNode1 "DIV" {:textContent "Hello world!"})
(def vnode2
(VNode2. "DIV" {:textContent "Hello world!"}))
;; 或 (->VNode2 "DIV" {:textContent "Hello world!"})
;; 或 (map->VNode2 {:tag "DIV", :props {:textContent "Hello world!"}})
這樣一看兩者貌似沒啥區別,其實區別在於成員的操作上
;; deftype取成員值
(.-tag vnode1) ;;=> DIV
;; defrecord取成員值
(:tag vnode2) ;;=> DIV
;; deftype修改成員值
(set! (.-tag vnode1) "SPAN")
;; 而 (aset vnode1 "tag" "SPAN"),這種方式不會改變vnode1的值
(.-tag vnode1) ;;=> SPAN
;; defrecord無法修改值,只能產生一個新例項
(def vnode3
(assoc vnode2 :tag "SPAN"))
(:tag vnode2) ;;=> DIV
(:tag vnode3) ;;=> SPAN
從上面我們可以看到defrecord
定義的資料結構可以視作Map來操作,而deftype
則不能。
但上述均為術,而背後的道則是:
在OOP中我們會建立兩類資料模型:1.程式設計領域模型;2.應用領域模型。對於程式設計領域模型(如String等),我們可以採用deftype
來定義,從而提供特殊化能力;但對於應用領域模型而言,我們應該對其進行抽象,從而採用已有的工具(如assoc
,filter
等)對其進行加工,並且對於應用領域模型而言,一切屬性應該均是可被訪問的,並不存在私有的需要,因為一切屬性均為不可變的哦。
Protocol
Protocol如同Interface可以讓我們實施面對介面程式設計。上面我們通過deftype
和defrecord
我們可以自定義資料結構,其實我們可以通過實現已有的Protocol或自定義的Protocol來擴充套件資料結構的能力。
deftype
和defrecord
在定義時實現Protocol
;; 定義protocol IA
(defprotocol IA
(println [this])
(log [this msg]))
;; 定義protocol IB
(defprotocol IB
(print [this]
[this msg]))
;; 定義資料結構VNode並實現IA和IB
(defrecord VNode [tag props]
IA
(println [this]
(println (:tag this)))
(log [this msg]
(println msg ":" (:tag this)))
IB
(print ([this]
(print (:tag this)))))
;; 各種呼叫
(def vnode (VNode. "DIV" {:textContent "Hello!"}))
(println vnode)
(log vnode "Oh-yeah:")
(print vnode)
注意IB
中定義print為Multi-arity method,因此實現中即使是僅僅實現其中一個函式簽名,也要以Multi-arity method的方式實現。
(print ([this] (print (:tag this))))
否則會報java.lang.UnsupportedOperationException: nth not supported on this type: Symbol
的異常
對已有的資料結構追加實現Protocol
Protocol強大之處就是我們可以在執行時擴充套件已有資料結構的行為,其中可通過extend-type
對某個資料結構實現多個Protocol,通過extend-protocol
對多個資料結構實現指定Protocol。
1.使用extend-type
;; 擴充套件js/NodeList,讓其可轉換為seq
(extend-type js/NodeList
ISeqable
(-seq [this]
(let [l (.-length this)
v (transient [])]
(doseq [i (range l)]
(->> i
(aget this)
(conj! v)))
(persistent! v))))
;; 使用
(map
#(.-textContent %)
(js/document.querySelector "div"))
;; 擴充套件js/RegExp,讓其可直接作為函式使用
(extend-type js/RegExp
IFn
(-invoke ([this s]
(re-matches this s))))
;; 使用
(#"s.*" "some") ;;=> some
2.使用extend-protocol
;; 擴充套件js/RegExp和js/String,讓其可直接作為函式使用
(extend-protocol IFn
js/RegExp
(-invoke ([this s] (re-matches this s)))
js/String
(-invoke ([this n] (clojure.string/join (take n this)))))
;; 使用
(#"s.*" "some") ;;=> some
("test" 2) ;;=> "te"
另外我們可以通過satisfies?
來檢查某資料型別例項是否實現指定的Protocol
(satisfies? IFn #"test") ;;=> true
;;對於IFn我們可以直接呼叫Ifn?
(Ifn? #"test") ;;=>true
reify
構造實現指定Protocol的無屬性例項
(defn user
[firstname lastname]
(reify
IUser
(full-name [_] (str firstname lastname))))
;; 使用
(def me (user "john" "Huang"))
(full-name me) ;;=> johnHuang
specify
和specify!
為例項追加Protocol實現
specify
可為不可變(immutable)和可複製(copyable,實現了ICloneable)的值,追加指定的Protocol實現。其實就是向cljs的值追加啦!
(def a "johnHuang")
(def b (specify a
IUser
(full-name [_] "Full Name")))
(full-name a) ;;=>報錯
(full-name b) ;;=>Full Name
specify!
可為JS值追加指定的Protocol實現
(def a #js {})
(specify! a
IUser
(full-name [_] "Full Name"))
(full-name a) ;;=> "Full Name"
總結
cljs建議對資料結構進行抽象,因此除了List,Map,Set,Vector外還提供了Seq;並內建一系列資料操作的函式,如map,filter,reduce等。而deftype、defrecord更多是針對物件導向程式設計來使用,或者是面對內建操作不足以描述邏輯時作為擴充套件的手段。也正是deftype
,defrecord
和defprotocol
讓我們從OOP轉FP時感覺更加舒坦一點。
另外deftype
,defrecord
和protocol這套還有效地解決Expression Problem,具體請檢視http://www.ibm.com/developerworks/library/j-clojure-protocols/
尊重原創,轉載請註明來自:http://www.cnblogs.com/fsjohnhuang/p/7154085.html ^_^肥仔John