zoukankan      html  css  js  c++  java
  • (cljs/run-at (JSVM. :all) "一次说白DataType、Record和Protocol")

    前言

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

  • 相关阅读:
    Java 插入排序
    Java 浮点型与双精度数值比较
    Java 包装类Integer的值比较
    ORA-00942 table or view does not exist
    logging模块
    面向对象
    模块和包
    异常处理
    序列化模块
    css3选择器
  • 原文地址:https://www.cnblogs.com/fsjohnhuang/p/7154085.html
Copyright © 2011-2022 走看看