前言
在项目中我们一般会为实际问题域定义领域数据模型,譬如开发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
则不能。
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。
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/ 尊重原创,转载请注明来自: ^_^肥仔John