抽象是代码重用的基础。Clojure语言本身对序列,容器和可调用性进行了抽象。在Java中,这通常是通过接口和类来实现的。在Clojure中一般使用protocol来完成这些任务。
面向抽象编程
Clojure内置的spit和slurp函数建构在两个抽象的基础上,即写和读。可以将之使用在很多的源和目标类型上。包括文件、URL和socket,并且还可以扩展到其他已经存在或者新创建的类型上。
gulp和expectorate
我们试着创建两个函数gulp和expectorate,分别对应于Clojure的slurp和spit函数。
;当前只能操作java.io.File类型的对象
(ns examples.gulp
(:import (java.io FileInputStream InputStreamReader BufferedReader)))
(defn gulp [src]
(let [sb (StringBuilder.)]
(with-open [reader (-> src FileInputStream. InputStreamReader. BufferedReader.)]
(loop [c (.read reader)]
(if (neg? c) (str sb) (do (.append sb (char c)) (recur (.read reader))))))))
(ns examples.expectorate
(:import (java.io FileOutputStream OutputStreamWriter BufferedWriter)))
(defn expectorate [dst content]
(with-open [writer (-> dst
FileOutputStream.
OutputStreamWriter.
BufferedWriter.)]
(.write writer (str content))))
;如果我们想要这两个函数支持其他类型呢,如socket、URL
;首先我们想到创建其他两个函数make-reader和make-writer,使用条件表达式从这些类型中创建BufferedReader或者BufferedWriter。
(defn make-reader [src]
(-> (condp = (type src)
java.io.InputStream src
java.lang.String (FileInputStream. src)
java.io.File (FileInputStream. src)
java.net.Socket (.getInputStream src)
java.net.URL (if (= "file" (.getProtocol src))
(-> src .getPath FileInputStream.)
(.openStream src)))
InputStreamReader.
BufferedReader.))
(defn make-writer [dst]
(-> (condp = (type dst)
java.io.OutputStream dst
java.io.File (FileOutputStream. dst)
java.lang.String (FileOutputStream. dst)
java.net.Socket (.getOutputStream dst)
java.net.URL (if (= "file" (.getProtocol dst))
(-> dst .getPath FileOutputStream.)
(throw (IllegalArgumentException. "Can't write to non-file URL"))))
OutputStreamWriter.
BufferedWriter.))
;对应的gulp和expectorate分别改写如下
(defn gulp [src]
(let [sb (StringBuilder.)]
(with-open [reader (make-reader src)]
(loop [c (.read reader)]
(if (neg? c) (str sb) (do (.append sb (char c)) (recur (.read reader))))))))
(defn expectorate [dst content]
(with-open [writer (make-writer dst)]
(.write writer (str content))))
这种抽象机制很原始,原因在于其封闭性。假如要支持其他类型,那么make-reader和make-writer就必须得改写。为了处理这种问题,应运而生的就是接口机制。
接口
将make-reader和make-writer抽象成一个接口如下
(definterface IOFactory
(^java.io.BufferReader make-reader [this])
(^java.io.BufferedWriter make-writer [this]))
需要得到支持的类型只需要实现这个接口就行。
然而接口机制依旧有问题,如果要支持已经存在的类型,怎么办。
事实上,Clojure中有一个更好的机制,那就是协议(protocol)。
协议
将make-reader和make-writer抽象成一个协议如下
(defprotocol IOFactory
"A protocol for things that can be read from and written to."
(make-reader [this] "Creates a BufferedReader.")
(make-writer [this] "Creates a BufferedWriter."))
然后可以使用该协议扩展InputStream和OutStream类型
(extend InputStream
IOFactory
{:make-reader (fn [src]
(-> src InputStreamReader. BufferedReader.))
:make-writer (fn [dst]
(throw (IllegalArgumentException. "Can't open as an InputStream.")))})
(extend OutputStream
IOFactory
{:make-reader (fn [src]
(throw
(IllegalArgumentException. "Can't open as an OutputStream.")))
:make-writer (fn [dst]
(-> dst OutputStreamWriter. BufferedWriter.))})
extend-type宏的语法更加清晰一点
;注意递归的调用了make-reader和make-writer对InputStream和OutStream的实现
(extend-type File
IOFactory
(make-reader [src]
(make-reader (FileInputStream. src)))
(make-writer [dst]
(make-writer (FileOutputStream. dst))))
使用extend-protocol宏,可以一次性添加多个类型对该协议的实现
(extend-protocol IOFactory
Socket
(make-reader [src]
(make-reader (.getInputStream src)))
(make-writer [dst]
(make-writer (.getOutputStream dst)))
URL
(make-reader [src]
(make-reader
(if (= "file" (.getProtocol src))
(-> src .getPath FileInputStream.)
(.openStream src))))
(make-writer [dst]
(make-writer
(if (= "file" (.getProtocol dst))
(-> dst .getPath FileInputStream.)
(throw (IllegalArgumentException. "Can't write to non-file URL"))))))
数据类型
接下来,我们使用deftype宏定义一个新的数据类型CryptoVault,该数据类型将会实现两个协议,其中包括IOFactory。
;该数据类型包含三个字段
(deftype CryptoVault [filename keystore password])
;创建该类型的一个实例
(def vault (->CryptoVault "vault-file" "keystore" "toomanysecrets"))
;获取实例的字段值
(.filename vault)
;给CryptoVault添加方法,即定义一个协议
(defprotocol Vault
(init-vault [vault])
(vault-output-stream [vault])
(vault-input-stream [vault]))
(deftype CryptoVault [filename keystore password]
Vault
(init-vault [vault]
(let [password (.toCharArray (.password vault))
key (.generateKey (KeyGenerator/getInstance "AES"))
keystore (doto (KeyStore/getInstance "JCEKS") (.load nil password) (.setEntry "vault-key" (KeyStore$SecretKeyEntry. key) (KeyStore$PasswordProtection. password)))]
(with-open [fos (FileOutputStream. (.keystore vault))]
(.store keystore fos password))))
(vault-output-stream [vault]
(let [cipher (doto (Cipher/getInstance "AES") (.init Cipher/ENCRYPT_MODE (vault-key vault)))]
(CipherOutputStream. (io/output-stream (.filename vault)) cipher)))
(vault-input-stream [vault]
(let [cipher (doto (Cipher/getInstance "AES") (.init Cipher/DECRYPT_MODE (vault-key vault)))]
(CipherInputStream. (io/input-stream (.filename vault)) cipher)))
proto/IOFactory
(make-reader [vault]
(proto/make-reader (vault-input-stream vault)))
(make-writer [vault]
(proto/make-writer (vault-output-stream vault))))
;为了使得内置的spit和slurp函数能作用于CryptoVault。需要扩展以实现clojure.java.io/IOFactory,这个版本的IOFactory有四个方法,除了我们定义的两个,还有两个默认方法定义在default-streams-impl映射表中。我们需要重写这两个方法。
(extend CryptoVault
clojure.java.io/IOFactory
(assoc clojure.java.io/default-streams-impl
:make-input-stream (fn [x opts] (vault-input-stream x))
:make-output-stream (fn [x opts] (vault-output-stream x))))
记录(record)
简而言之,记录是一种特殊的数据类型,实现了PersistentMap,因而可以当做map使用。
;定义了一个record
(defrecord Note [pitch octave duration])
-> user.Note
;创建一个实例
(->Note :D# 4 1/2)
-> #user.Note{:pitch :D#, :octave 4, :duration 1/2}
;访问字段
(.pitch (->Note :D# 4 1/2))
-> :D#
;记录同样也是一个map
(map? (->Note :D# 4 1/2))
-> true
;因而可以像map一样使用关键字访问字段
(:pitch (->Note :D# 4 1/2))
-> :D#
;使用assoc和update-in修改记录
(assoc (->Note :D# 4 1/2) :pitch :Db :duration 1/4)
-> #user.Note{:pitch :Db, :octave 4, :duration 1/4}
(update-in (->Note :D# 4 1/2) [:octave] inc)
-> #user.Note{:pitch :D#, :octave 5, :duration 1/2}
;关联一个额外的字段
(assoc (->Note :D# 4 1/2) :velocity 100)
-> #user.Note{:pitch :D#, :octave 4, :duration 1/2, :velocity 100}
;assoc和update-in函数返回一个新的记录,然而dissoc函数情况比较复杂,如果dissoc删除的字段是可选的,如上例添加的:velocity,那么同样返回记录。如果dissoc删除的字段是定义记录时指定的,则返回一个普通的map。
(dissoc (->Note :D# 4 1/2) :octave)
-> {:pitch :D#, :duration 1/2}
;跟map的不同的是,记录不可以当关键字的函数使用
((->Note. :D# 4 1/2) :pitch)
-> user.Note cannot be cast to clojure.lang.IFn
;如下定义了一个代表音符(Note)的record,并且实现了MidiNote接口,并且定义了一个演奏函数perform,用于演奏Note序列。
(ns examples.datatypes.midi
(:import [javax.sound.midi MidiSystem]))
(defprotocol MidiNote
(to-msec [this tempo])
(key-number [this])
(play [this tempo midi-channel]))
(defn perform [notes & {:keys [tempo] :or {tempo 88}}]
(with-open [synth (doto (MidiSystem/getSynthesizer).open)]
(let [channel (aget (.getChannels synth) 0)]
(doseq [note notes]
(play note tempo channel)))))
(defrecord Note [pitch octave duration]
MidiNote
(to-msec [this tempo]
(let [duration-to-bpm {1 240, 1/2 120, 1/4 60, 1/8 30, 1/16 15}]
(* 1000 (/ (duration-to-bpm (:duration this))
tempo))))
(key-number [this]
(let [scale {:C 0, :C# 1, :Db 1, :D 2,
:D# 3, :Eb 3, :E 4, :F 5,
:F# 6, :Gb 6, :G 7, :G# 8,
:Ab 8, :A 9, :A# 10, :Bb 10,
:B 11}]
(+ (* 12 (inc (:octave this)))
(scale (:pitch this)))))
(play [this tempo midi-channel]
(let [velocity (or (:velocity this) 64)]
(.noteOn midi-channel (key-number this) velocity)
;演奏大白鲨的插曲
(def jaws (for [duration [1/2 1/2 1/4 1/4 1/8 1/8 1/8 1/8]
pitch [:E :F]]
(Note. pitch 2 duration)))
-> #'user/jaws
(perform jaws)
-> nil
reify(使具体化)
reify宏用于实现接口或者协议的匿名对象。
(import '[examples.datatypes.midi MidiNote])
(let [min-duration 250
min-velocity 64
rand-note (reify
MidiNote
(to-msec [this tempo] (+ (rand-int 1000) min-duration))
(key-number [this] (rand-int 100))
(play [this tempo midi-channel]
(let [velocity (+ (rand-int 100) min-velocity)] (.noteOn midi-channel (key-number this) velocity) (Thread/sleep (to-msec this tempo)))))]
(perform (repeat 15 rand-note)))