在項目中咱們通常會爲實際問題域定義領域數據模型,譬如開發VDOM時天然而言就會定義個VNode數據類型,用於打包存儲、操做相關數據。clj/cljs不單內置了List
、Vector
、Set
和Map
等數據結構,還提供deftype
和defrecord
讓咱們能夠自定義數據結構,以知足實際開發需求。html
說起數據結構很天然就想起C語言中的struct,結構中只有字段並無定義任何方法,而這也是deftype
和defrecord
最基礎的玩法。
示例java
(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!"}})
這樣一看二者貌似沒啥區別,其實區別在於成員的操做上node
;; 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如同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強大之處就是咱們能夠在運行時擴展已有數據結構的行爲,其中可經過extend-type
對某個數據結構實現多個Protocol,經過extend-protocol
對多個數據結構實現指定Protocol。
1.使用extend-type
this
;; 擴展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
code
;; 擴展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?
來檢查某數據類型實例是否實現指定的Protocolhtm
(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