見様見真似でKotlinで値オブジェクト用のクラスを作成してみたらいろいろと学びがあったので、そのメモです。
値オブジェクトの定義
今回の記事で言及する値オブジェクトは、ドメイン駆動設計の文脈での値オブジェクトのつもりです。
「つもり」と強調したのは、私が今回初めて自力で値オブジェクト用のクラスの作成にトライしたため、理解がそもそも間違っている可能性があるからです。
もし間違っていたら記事へのコメントでもTwitterへのリプライでも何でもいいのでコメントいただけると幸いです🙇♂️
実際に値オブジェクト用のクラスを作成してみた感じでは、アプリケーションで扱う全ての値に対してこのような実装を必ずしも行う必要はなく、値が固有のルールを持つ必要があるときに有効なのではないかと思いました(あくまで私の現状の感覚です)。
自然数を表現するNaturalNumber
クラス
今回は、自然数を値オブジェクトとして扱うためにNaturalNumber
というクラスを作成しました。
また、NaturalNumber
には
という2つの処理を行うメソッドを実装しました。
これらを実装したのは、一般の整数ではなく自然数だけに対して定義される概念であるためです。
作成したNaturalNumber
クラスは以下の通りです。
// ①data classの利用 data class NaturalNumber(private val value: Int) { companion object { private const val MIN_VALUE = 1 } // ②プロパティのバリデーション init { if (value < MIN_VALUE) { throw IllegalArgumentException("value must be greater than or equal to $MIN_VALUE.") } } // ③素数かどうか判定する fun isPrime(): Boolean { if (value == 1) return false return !(2 until value).any { value % it == 0 } } // ③階乗した値を取得する fun getFactorialValue() = cache ?: value.factorial() private var cache: NaturalNumber? = null private fun Int.factorial(): NaturalNumber { tailrec fun Int.factorial(accumulation: Int = 1): NaturalNumber { return if (this == 1) { NaturalNumber(accumulation) } else { if (accumulation > Int.MAX_VALUE / this) throw ArithmeticException("overflow occurred.") (this - 1).factorial(accumulation * this) } } return this.factorial() } // ④二項演算子のoverload operator fun plus(other: NaturalNumber) = NaturalNumber(value + other.value) operator fun minus(other: NaturalNumber) = NaturalNumber(value - other.value) operator fun times(other: NaturalNumber) = NaturalNumber(value * other.value) operator fun div(other: NaturalNumber) = NaturalNumber(value / other.value) // ⑤比較演算子のoverload operator fun compareTo(other: NaturalNumber) = value.compareTo(other.value) }
①data class
を利用したクラス宣言
Kotlinでは、data class
を利用することで値オブジェクトの同一性の判定処理がデフォルトで実装されます。
つまり、data class
から生成した2つの値オブジェクトのプロパティが全て等しいとき、equals()
はtrueとなります。
また、プロパティをimmutableな変数として宣言することで不変性も保たれます。
②init
ブロックを利用したプロパティのバリデーション
init
ブロックを利用することでプロパティのバリデーションを行っています。
今回の例では、自然数ではない整数がプロパティに指定されたときにIllegalArgumentException
をthrowしています。
init { if (value < MIN_VALUE) { throw IllegalArgumentException("value must be greater than or equal to $MIN_VALUE.") } }
③素数かどうか判定する、階乗した値を取得する
ここではただ関数を実装しているだけですが、階乗した値を取得するgetFactorialValue()
メソッドにおいて内部的にtailrec
修飾子を利用しています。
階乗の計算のように、関数が自分自身を最後に呼び出すいわゆる末尾再帰関数(tail recursive function)にtailrec
修飾子を付与することで、スタック・オーバーフローを防ぐ形でコンパイラが最適に再帰呼び出しを行ってくれるそうです。
また、階乗の計算結果は非常に大きな値になるため、オーバーフロー(こちらは単にIntの)したときにArithmeticException
をthrowするようにしました(いい方法が思い浮かばなかったので本当に一応ですが…)。
tailrec fun Int.factorial(accumulation: Int = 1): NaturalNumber { return if (this == 1) { NaturalNumber(accumulation) } else { if (accumulation > Int.MAX_VALUE / this) throw ArithmeticException("overflow occurred.") (this - 1).factorial(accumulation * this) } }
④二項演算子のoverload
Kotlinでは、自作のクラス内で特定の関数の前にoperator
修飾子を付与することで、その関数に対応する演算子のoverloadが可能です。
関数と二項演算子の対応関係はこちらに記載されているので、必要に応じてoverloadします。
簡単のため、今回は加減乗除演算子だけoverloadしました。
operator fun plus(other: NaturalNumber) = NaturalNumber(value + other.value) operator fun minus(other: NaturalNumber) = NaturalNumber(value - other.value) operator fun times(other: NaturalNumber) = NaturalNumber(value * other.value) operator fun div(other: NaturalNumber) = NaturalNumber(value / other.value)
⑤比較演算子のoverload
同様に、比較演算子のoverloadも可能です。
関数と比較演算子の対応関係はこちらです。大小関係のある値オブジェクトを扱うときは必ず実装することになると思います。
operator fun compareTo(other: NaturalNumber) = value.compareTo(other.value)
まとめ
完全に思いつきで作成し始めたのですが、値オブジェクト用のクラスを作成してみたことでKotlinの便利な文法をいくつか知ることもでき、一石二鳥という感じでした。
ただ、「値オブジェクト用のクラス」の呼び方は何なのか(そもそもあるのか)最後まで疑問なまま書き進めてしまいました。抽象データ型というやつ?うーん。