在项目中咱们通常会为实际问题域定义领域数据模型,譬如开发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