前言

?在項(xiàng)目中我們一般會(huì)為實(shí)際問題域定義領(lǐng)域數(shù)據(jù)模型,譬如開發(fā)VDOM時(shí)自然而言就會(huì)定義個(gè)VNode數(shù)據(jù)類型,用于打包存儲(chǔ)、操作相關(guān)數(shù)據(jù)。clj/cljs不單內(nèi)置了ListVector、SetMap等數(shù)據(jù)結(jié)構(gòu),還提供deftypedefrecord讓我們可以自定義數(shù)據(jù)結(jié)構(gòu),以滿足實(shí)際開發(fā)需求。

定義數(shù)據(jù)結(jié)構(gòu)從Data Type和Record開始

?提及數(shù)據(jù)結(jié)構(gòu)很自然就想起C語(yǔ)言中的struct,結(jié)構(gòu)中只有字段并沒有定義任何方法,而這也是deftypedefrecord最基礎(chǔ)的玩法。
示例

(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!"}})

?這樣一看兩者貌似沒啥區(qū)別,其實(shí)區(qū)別在于成員的操作上

;; deftype取成員值(.-tag vnode1) ;;=> DIV;; defrecord取成員值(:tag vnode2)  ;;=> DIV;; deftype修改成員值(set! (.-tag vnode1) "SPAN");; 或 (aset vnode1 "tag" "SPAN")(.-tag vnode1) ;;=> SPAN;; defrecord無(wú)法修改值,只能產(chǎn)生一個(gè)新實(shí)例(def vnode3
  (assoc vnode2 :tag "SPAN"))
(:tag vnode2) ;;=> DIV(:tag vnode3) ;;=> SPAN

?從上面我們可以看到defrecord定義的數(shù)據(jù)結(jié)構(gòu)可以視作Map來(lái)操作,而deftype則不能。
?但上述均為術(shù),而背后的道則是:
在OOP中我們會(huì)建立兩類數(shù)據(jù)模型:1.編程領(lǐng)域模型;2.應(yīng)用領(lǐng)域模型。對(duì)于編程領(lǐng)域模型(如String等),我們可以采用deftype來(lái)定義,從而提供特殊化能力;但對(duì)于應(yīng)用領(lǐng)域模型而言,我們應(yīng)該對(duì)其進(jìn)行抽象,從而采用已有的工具(如assoc,filter等)對(duì)其進(jìn)行加工,并且對(duì)于應(yīng)用領(lǐng)域模型而言,一切屬性應(yīng)該均是可被訪問的,并不存在私有的需要,因?yàn)橐磺袑傩跃鶠椴豢勺兊呐丁?/p>

Protocol

?Protocol如同Interface可以讓我們實(shí)施面對(duì)接口編程。上面我們通過deftypedefrecord我們可以自定義數(shù)據(jù)結(jié)構(gòu),其實(shí)我們可以通過實(shí)現(xiàn)已有的Protocol或自定義的Protocol來(lái)擴(kuò)展數(shù)據(jù)結(jié)構(gòu)的能力。

deftypedefrecord在定義時(shí)實(shí)現(xiàn)Protocol

;; 定義protocol IA(defprotocol IA
  (println [this])
  (log [this msg]));; 定義protocol IB(defprotocol IB
  (print [this]
         [this msg]));; 定義數(shù)據(jù)結(jié)構(gòu)VNode并實(shí)現(xiàn)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)))));; 各種調(diào)用(def vnode (VNode. "DIV" {:textContent "Hello!"}))
(println vnode)
(log vnode "Oh-yeah:")
(print vnode)

注意IB中定義print為Multi-arity method,因此實(shí)現(xiàn)中即使是僅僅實(shí)現(xiàn)其中一個(gè)函數(shù)簽名,也要以Multi-arity method的方式實(shí)現(xiàn)。

(print ([this] (print (:tag this))))

否則會(huì)報(bào)java.lang.UnsupportedOperationException: nth not supported on this type: Symbol的異常

對(duì)已有的數(shù)據(jù)結(jié)構(gòu)追加實(shí)現(xiàn)Protocol

?Protocol強(qiáng)大之處就是我們可以在運(yùn)行時(shí)擴(kuò)展已有數(shù)據(jù)結(jié)構(gòu)的行為,其中可通過extend-type對(duì)某個(gè)數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)多個(gè)Protocol,通過extend-protocol對(duì)多個(gè)數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)指定Protocol。
1.使用extend-type

;; 擴(kuò)展js/NodeList,讓其可轉(zhuǎn)換為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"));; 擴(kuò)展js/RegExp,讓其可直接作為函數(shù)使用(extend-type js/RegExp
  IFn
  (-invoke ([this s]
    (re-matches this s))));; 使用(#"s.*" "some") ;;=> some

2.使用extend-protocol

;; 擴(kuò)展js/RegExp和js/String,讓其可直接作為函數(shù)使用(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?來(lái)檢查某數(shù)據(jù)類型實(shí)例是否實(shí)現(xiàn)指定的Protocol

(satisfies? IFn #"test") ;;=> true;;對(duì)于IFn我們可以直接調(diào)用Ifn?(Ifn? #"test") ;;=>true

reify構(gòu)造實(shí)現(xiàn)指定Protocol的無(wú)屬性實(shí)例

(defn user
  [firstname lastname]
  (reify
    IUser
    (full-name [_] (str firstname lastname))));; 使用(def me (user "john" "Huang"))
(full-name me) ;;=> johnHuang

specifyspecify!為實(shí)例追加Protocol實(shí)現(xiàn)

specify可為不可變(immutable)和可復(fù)制(copyable,實(shí)現(xiàn)了ICloneable)的值,追加指定的Protocol實(shí)現(xiàn)。其實(shí)就是向cljs的值追加啦!

(def a "johnHuang")
(def b (specify a
         IUser
         (full-name [_] "Full Name")))

(full-name a) ;;=>報(bào)錯(cuò)(full-name b) ;;=>Full Name

specify!可為JS值追加指定的Protocol實(shí)現(xiàn)

(def a #js {})
(specify! a
  IUser
  (full-name [_] "Full Name"))

(full-name a) ;;=> "Full Name"

總結(jié)

?cljs建議對(duì)數(shù)據(jù)結(jié)構(gòu)進(jìn)行抽象,因此除了List,Map,Set,Vector外還提供了Seq;并內(nèi)置一系列數(shù)據(jù)操作的函數(shù),如map,filter,reduce等。而deftype、defrecord更多是針對(duì)面向?qū)ο缶幊虂?lái)使用,或者是面對(duì)內(nèi)置操作不足以描述邏輯時(shí)作為擴(kuò)展的手段。也正是deftype,defrecorddefprotocol讓我們從OOP轉(zhuǎn)FP時(shí)感覺更加舒坦一點(diǎn)。
?另外deftype,defrecord和protocol這套還有效地解決Expression Problem,具體請(qǐng)查看http://www.ibm.com/developerworks/library/j-clojure-protocols/

尊重原創(chuàng),轉(zhuǎn)載請(qǐng)注明來(lái)自:http://www.cnblogs.com/fsjohnhuang/p/7154085.html ^_^肥仔John

http://www.cnblogs.com/fsjohnhuang/p/7154085.html