(cljs/run-at (JSVM. :all) "一次说白DataType、Record和Protocol")
前言
在项目中我们一般会为实际问题域定义领域数据模型,譬如开发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
则不能。
但上述均为术,而背后的道则是:
在OOP中我们会建立两类数据模型:1.编程领域模型;2.应用领域模型。对于编程领域模型(如String等),我们可以采用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。
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
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
(cljs/run-at (JSVM. :all) "一次说白DataType、Record和Protocol")的更多相关文章
- iOS开发系列--Objective-C之协议、代码块、分类
概述 ObjC的语法主要基于smalltalk进行设计的,除了提供常规的面向对象特性外,还增加了很多其他特性,这一节将重点介绍ObjC中一些常用的语法特性.当然这些内容虽然和其他高级语言命名不一样,但 ...
- iOS-Objective-C基础
一.Foundation框架 概述 我们前面的章节中就一直新建Cocoa Class,那么Cocoa到底是什么,它和我们前面以及后面要讲的内容到底有什么关系呢?Objective-C开发中经常用到NS ...
- RichTextBoxEx2
using System;using System.Collections.Specialized;using System.Drawing;using System.Drawing.Imaging; ...
- RichTextBoxEx
using System; using System.Collections.Specialized; using System.Drawing; using System.Drawing.Imagi ...
- LR11
HP LoadRunner Readme for the Windows operating system Software version: 11.00 Publication date: Octo ...
- 【Hadoop代码笔记】通过JobClient对Jobtracker的调用详细了解Hadoop RPC
Hadoop的各个服务间,客户端和服务间的交互采用RPC方式.关于这种机制介绍的资源很多,也不难理解,这里不做背景介绍.只是尝试从Jobclient向JobTracker提交作业这个最简单的客户端服务 ...
- Qtp自动测试工具(案例学习)
♣Qtp是什么? ♣测试用例网站 ♦注册与登录 ♦测试脚本 ◊录制/执行测试脚本 ◊分析录制的测试脚本 ◊执行.查看测试脚本 ♦建立检查点 ...
- Windows批量添加防火墙例外端口
Windows下批量添加防火墙例外端口,查了网上资料,基本上都是使用"Netsh命令",循环增加端口,这会导致建立的规则特别多,不便于管理,查了下微软的资料,原来是Netsh命令, ...
- jenkin插件整理
分类 plugin名称 wiki地址 源码地址 plugin作用范围 备注 Build Reports构建报告(此类插件用来分析构建结果,比果代码检查,测试CASE分析,并将这些结果以报表,趋势图等形 ...
随机推荐
- python中的数字
在编程中,通常使用数字来记录游戏得分,表示可视化数据.存储web应用信息等. #运算# 1,基本运算 >>> 2+35>>> 1-2-1>>> 3 ...
- kali虚拟机安装提示安装系统步骤失败
首先虚拟机不论是VM还是VirtualBox都可以直接安装kali镜像文件的,不过如果你采用虚拟机默认硬盘8G设置的话,到的系统安装步骤会出错无法继续,具体原因不明. 解决办法却很简单,将虚拟机的硬盘 ...
- Java阶段性测试--第二三大题参考代码
第二大题: 1.打印出所有的 "水仙花数 ",所谓 "水仙花数 "是指一个三位数,其各位数字立方和等于它本身 package Test1; //1.打印出所有的 ...
- 在linux下利用信号量实现一个写者线程多个读者线程
#include<pthread.h> #include<string.h> #include<stdlib.h> #include<stdio.h> ...
- Delphi使用Zint生成QR二维条码(zint.dll)
Delphi使用Zint生成QRCODE 本文使用的Zint Barcode Library(zint.dll)版本为2.6.0,和之前使用的2.4.3版本在zint_symbol这个结构体上会有差异 ...
- OC对象之旅 weak弱引用实现分析
Runtime学习 -- weak应用源码学习 Runtime源码分析,带你了解OC实现过程.其中参考了大量的大神的代码以及文献,里面也有个人的见解,欢迎拍砖,欢迎交流. 两种常见使用场景 /// ...
- 使用Oracle数据库实现树形结构表的子-父级递归查询和删除,通过级联菜单简单举例
前言: 我们在开发中,常常遇到单表的子-父id级联的表结构,在树形的深度不确定的情况下,一次查询出某个树形结构下的所有具有子-父级关系的数据变得十分困难. 这时,我们使用oracle提供的CONNEC ...
- 基于redis实现tomcat8及以上版本的tomcat集群的session持久化实现(tomcat-redis-session-manager二次开发)
前言: 本项目是基于jcoleman的tomcat-redis-session-manager二次开发版本 1.修改了小部分实现逻辑 2.去除对juni.jar包的依赖 3.去除无效代码和老版本tom ...
- MACOS关闭指定端口
因为用IDEA写项目的时候,有的时候结束Jetty导致端口没有释放,所以会出现占用的情况. MacOS结束端口占用进程的命令,和Linux的一样.先执行如下命令: lsof -i:8080 会有类似下 ...
- golang 浮点数 取精度的效率对比
需求 浮点数取2位精度输出 实现 代码 package main import ( "time" "log" "strconv" " ...