多态性

Clojure虽然是一门函数式编程语言,当也能很容易支持类似OOP那种polymorphism,能让我们写出更好的抽象代码。

Multimethods

使用multimethod是一种快速在代码里面引入polymorphism的方法,我们可以定义一个dispatching function,然后指定一个dispatching value,通过它来确定调用哪一个函数。

譬如,我们需要计算一个图形的面积,我们知道,如果是一个长方形,那么方法就是with * heigth,如果是圆形,那么就是 PI * radius * radius

  1. ; define a multimethod for area with :Shape keyword.
  2. (defmulti area :Shape)
  3. (defn rect [wd ht] {:Shape :Rect :wd wd :ht ht})
  4. (defn circle [radius] {:Shape :Circle :radius radius})
  5. (defmethod area :Rect [r]
  6. (* (:wd r) (:ht r)))
  7. (defmethod area :Circle [c]
  8. (* (. Math PI) (* (:radius c) (:radius c))))
  9. (defmethod area :default [x] :oops)
  10. (def r (rect 4 13))
  11. (def c (circle 12))

我们在repl里面执行:

  1. user=> (area r)
  2. 52
  3. user=> (area c)
  4. 452.3893421169302
  5. user=> (area {})
  6. :oops

Protocol

从上面multimethod的实现可以,multimethod只是一个polymorphic操作,如果我们想实现多个,那么multimethod就不能满足了,这时候,我们就可以使用protocol

protocol其实更类似其他语言里面interface,我们定义一个protocol,然后用不同的类型去特化实现,我们以jepsen的代码为例,因为jepsen可以测试很多db,所以它定义了一个dbprotocol

  1. (defprotocol DB
  2. (setup! [db test node] "Set up the database on this particular node.")
  3. (teardown! [db test node] "Tear down the database on this particular node."))

上面的代码定义了一个DBprotocol,然后有两个函数接口,用来setupteardown对应的db,所以我们只需要在自己的db上面实现这两个函数就能让jepsen调用了,伪代码如下:

  1. (def my-db
  2. (reify DB
  3. (setup! [db test node] "hello db")
  4. (teardown! [db test node] "goodbye db")))

然后就能直接使用DB protocol了。

  1. user=> (setup! my-db :test :node)
  2. "hello db"
  3. user=> (teardown! my-db :test :node)
  4. "goodbye db"

Record

有些时候,我们还想在clojure中实现OOP语言中class的效果,用record就能很方便的实现。record类似于map,这点就有点类似于C++ class中的field,然后还能实现特定的protocol,这就类似于C++ classmember function了。

record的定义很简单,我们使用defrecord来定义:

  1. user=> (defrecord person [name age])
  2. user.person

这里,我们定义了一个personrecord,它含有nameage两个字段,然后我们可以通过下面的方法来具体创建一个person:

  1. ; 使用类似java . 操作符创建
  2. user=> (person. "siddon" 30)
  3. #user.person{:name "siddon", :age 30}
  4. ; 通过 ->person 函数创建
  5. user=> (->person "siddon" 30)
  6. #user.person{:name "siddon", :age 30}
  7. ; 通过 map->persion 函数创建,参数是map
  8. user=> (map->person {:name "siddontang" :age 30)

因为record其实可以认为是一个map,所以很多map的操作,我们也同样可以用于record上面。

  1. user=> (def siddon (->person "siddon" 30))
  2. #'user/siddon
  3. user=> (assoc siddon :name "tang")
  4. #user.person{:name "tang", :age 30}
  5. user=> (dissoc siddon :name)
  6. {:age 30}

record可以实现特定的protocol,譬如:

  1. (defprotocol SayP
  2. (say [this]))
  3. (defrecord person [name age]
  4. SayP
  5. (say [this]
  6. (str "hello " name)))

上面我们定义了SayP这个protocol,并且让person这个record实现了相关的函数,然后我们就可以直接使用了。

  1. user=> (say (->person "siddon" 30))
  2. "hello siddon"