Skip to content

Instantly share code, notes, and snippets.

@simonNozaki
Last active April 21, 2022 05:49
Show Gist options
  • Save simonNozaki/ff2ad9d8f1dc82bccb221b00ad2528cd to your computer and use it in GitHub Desktop.
Save simonNozaki/ff2ad9d8f1dc82bccb221b00ad2528cd to your computer and use it in GitHub Desktop.

Clojureでもドメイン駆動な設計がしたい

Clojureは他の有力なJVM系言語(Java, Kotlinなど)と比較すると関数指向であり、状態の管理をカプセル化する概念のまま実装するのは少しむずかしい。できるけど atomref などでいろいろ気にしながら状態の変更をコーディングしないといけず、イミュータブルなものとしてコーディングしたほうがいい場面も多い。

イミュータビリティは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

Clojureで関数指向に

データの価値は、関数が決めるということで年齢の値オブジェクトを、レコードと年齢に特化した関数で表現する。 クラスベースの言語ならコンストラクタにいろいろやらせるが、レコードにそれはさせない。

(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} になる

まだまだ触っていかないとなんともいえないけど、データとふるまいが結構離れてしまってるのでプロトコルでうまいこと抽象化できないかな

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment