(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

目录
相关文章
|
人工智能 前端开发 API
开源一周即飙升7.9K Star!Money Printer:一款可以自动创建带有配乐及字幕的短视频工具!
开源一周即飙升7.9K Star!Money Printer:一款可以自动创建带有配乐及字幕的短视频工具!
423 1
|
存储 安全 算法
使用jotp实现双因子验证
扫盲使用totp增强身份安全性指南,原理看懂也不用自己造轮子呀,最讨厌哪些啥也不懂的搬运工,我这里给大家解惑吧
904 0
|
存储 缓存 编译器
【CMake 命令相关知识】深入理解 CMake命令中的 内置缓存变量
【CMake 命令相关知识】深入理解 CMake命令中的 内置缓存变量
313 0
|
7月前
|
Java Spring
Spring中事务失效的场景
因为Spring事务是基于代理来实现的,所以某个加了@Transactional的⽅法只有是被代理对象调⽤时, 那么这个注解才会⽣效 , 如果使用的是被代理对象调用, 那么@Transactional会失效 同时如果某个⽅法是private的,那么@Transactional也会失效,因为底层cglib是基于⽗⼦类来实现 的,⼦类是不能重载⽗类的private⽅法的,所以⽆法很好的利⽤代理,也会导致@Transactianal失效 如果在业务中对异常进行了捕获处理 , 出现异常后Spring框架无法感知到异常, @Transactional也会失效
|
7月前
|
SQL 分布式计算 IDE
如何在IDE中通过Spark操作Hive
通过以上方法和代码示例,你可以在IDE中成功通过Spark操作Hive,实现大规模数据处理和分析。确保理解每一步的实现细节,应用到实际项目中时能有效地处理各种复杂的数据场景。
373 28
|
7月前
|
安全 API
获取百炼API-KEY
获取百炼API-KEY
|
存储 人工智能 Cloud Native
云栖重磅|从数据到智能:Data+AI驱动的云原生数据库
阿里云瑶池在2024云栖大会上重磅发布由Data+AI驱动的多模数据管理平台DMS:OneMeta+OneOps,通过统一、开放、多模的元数据服务实现跨环境、跨引擎、跨实例的统一治理,可支持高达40+种数据源,实现自建、他云数据源的无缝对接,助力业务决策效率提升10倍。
|
机器学习/深度学习 人工智能 前端开发
【机器学习】FlyFlowerSong【人工智能】资源指南
FlyFlowerSong是一个创新的音乐合成与处理项目,它利用先进的机器学习算法,为用户提供了一个简单而有趣的音乐创作平台。作为人工智能领域的技术自媒体创作者,我整理了关于FlyFlowerSong的完整教程、论文复现指南以及demo项目源代码,旨在帮助开发者、音乐爱好者以及AI研究者深入探索这一领域。
209 1
|
存储 编译器 C语言
你应该知道的C语言干货(1)
小数直接写出来编译器默认为double类型,后面加上f才是float类型
125 0