(cljs/run-at (JSVM. :all) "一次说白DataType、Record和Protocol")

简介: 在项目中我们一般会为实际问题域定义领域数据模型,譬如开发VDOM时自然而言就会定义个VNode数据类型,用于打包存储、操作相关数据。clj/cljs不单内置了List、Vector、Set和Map等数据结构,还提供deftype和defrecord让我们可以自定义数据结构,以满足实际开发需求。

前言

 在项目中我们一般会为实际问题域定义领域数据模型,譬如开发VDOM时自然而言就会定义个VNode数据类型,用于打包存储、操作相关数据。clj/cljs不单内置了ListVectorSetMap等数据结构,还提供deftypedefrecord让我们可以自定义数据结构,以满足实际开发需求。

定义数据结构从Data Type和Record开始

 提及数据结构很自然就想起C语言中的struct,结构中只有字段并没有定义任何方法,而这也是deftypedefrecord最基础的玩法。
示例

(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可以让我们实施面对接口编程。上面我们通过deftypedefrecord我们可以自定义数据结构,其实我们可以通过实现已有的Protocol或自定义的Protocol来扩展数据结构的能力。

deftypedefrecord在定义时实现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

specifyspecify!为实例追加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,defrecorddefprotocol让我们从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

目录
相关文章
|
10月前
|
容器
【问题处理】Error response from daemon: Pool overlaps with other one on this address space
Error response from daemon: Pool overlaps with other one on this address space
166 0
|
18天前
|
索引
Elasticsearch exception [type=illegal_argument_exception, reason=index [.1] is the write index for data stream [slowlog] and cannot be deleted]
在 Elasticsearch 中,你尝试删除的索引是一个数据流(data stream)的一部分,而且是数据流的写入索引(write index),因此无法直接删除它。为了解决这个问题,你可以按照以下步骤进行操作:
|
6月前
报错modify sync object Modify sync object Failed!
报错modify sync object Modify sync object Failed!
25 1
|
11月前
|
应用服务中间件
Error:Remote staging type or host is not specified.
Error:Remote staging type or host is not specified.
|
网络协议 Java
filebeat:Failed to publish events caused by: write tcp 5044: write: connection reset by peer
filebeat:Failed to publish events caused by: write tcp 5044: write: connection reset by peer
377 0
filebeat:Failed to publish events caused by: write tcp 5044: write: connection reset by peer
solidity中transfer异常"send" and "transfer" are only available for objects of type address
solidity中transfer异常"send" and "transfer" are only available for objects of type address
416 0
callbackend entry point - iwfndcl_mgw_request_manager~read_entity
007. callbackend entry point - /iwfnd/cl_mgw_request_manager~read_entity Created by Wang, Jerry, last modified on Jan 06, 2015
148 0
callbackend entry point - iwfndcl_mgw_request_manager~read_entity
how to verify that Listener is entry point of application
how to verify that Listener is entry point of application
how to verify that Listener is entry point of application