Clojureは他の有力なJVM系言語(Java, Kotlinなど)と比較すると関数指向であり、状態の管理をカプセル化する概念のまま実装するのは少しむずかしい。できるけど atom
や ref
などでいろいろ気にしながら状態の変更をコーディングしないといけず、イミュータブルなものとしてコーディングしたほうがいい場面も多い。
イミュータビリティはKotlinでも実装しやすいが、値とふるまいのパッケージング、コンパイルによる縛りあたりはClojureだと難しいので工夫が必要そう。
プログラミング言語とシステムデザインClojure, Java, デザインパターン, DDD, Clean Architecture にインスパイアされた、Clojureでのドメイン駆動設計な実装の検討です。
関数指向とドメイン駆動設計は直行しうるもので、必ずしもクラスベースでなくてもいいと思っている。かんたんな値オブジェクトを例にとります。
年齢の値オブジェクト。加齢だけが振る舞いとして定義されている。
data class Age (
val value: Int
) {
init {
if (this.value < 0) {
throw RuntimeException("年齢は0才以下になりません")
}
}
fun getOld(): Age {
return Age(this.value + 1)
}
}
Ageクラスを使う。ここまでは普通です。
val now = Age(30)
println(now.getOld())
// ==> 31
データの価値は、関数が決めるということで年齢の値オブジェクトを、レコードと年齢に特化した関数で表現する。 クラスベースの言語ならコンストラクタにいろいろやらせるが、レコードにそれはさせない。
(defrecord Age [value])
代わりに、かんたんなファクトリメソッドを同じ名前空間においておく。動的型付けであるがゆえに、関数と値の紐付けを言語仕様ではしばれない...
(defn create-age [this value]
(if (< value 0) (throw (RuntimeException. "年齢は0才以下になりません")))
(->Age value))
そして、年をとる。この関数は Age を引数に取らない限りおよそ意味をなさないようにしておく。これも同じ名前空間に。
(defn get-old [age]
(let [now (:value age)]
(->Age (+ now 1))))
今。そして歳を取ると...
```clojure
(def now (create-age 30))
; => Age{:value 30}
(get-old now)
; 評価すると Age{:value 31} になる
まだまだ触っていかないとなんともいえないけど、データとふるまいが結構離れてしまってるのでプロトコルでうまいこと抽象化できないかな