Kotlinで値オブジェクト用のクラス作成

見様見真似でKotlinで値オブジェクト用のクラスを作成してみたらいろいろと学びがあったので、そのメモです。

# 値オブジェクトの定義
今回の記事で言及する値オブジェクトは、ドメイン駆動設計の文脈での値オブジェクトの<b>つもり</b>です。
「つもり」と強調したのは、私が今回初めて自力で値オブジェクト用のクラスの作成にトライしたため、理解がそもそも間違っている可能性があるからです。
もし間違っていたら記事へのコメントでも[Twitter](https://twitter.com/tkhs0604)へのリプライでも何でもいいのでコメントいただけると幸いです🙇‍♂️

実際に値オブジェクト用のクラスを作成してみた感じでは、アプリケーションで扱う全ての値に対してこのような実装を必ずしも行う必要はなく、<span style="color: #ff5252"><b>値が固有のルールを持つ必要があるとき</b></span>に有効なのではないかと思いました(あくまで私の現状の感覚です)。

# 自然数を表現する`NaturalNumber`クラス
今回は、自然数を値オブジェクトとして扱うために`NaturalNumber`というクラスを作成しました。
また、`NaturalNumber`には

- その自然数が素数かどうか判定する
- その自然数を階乗した値を取得する

という2つの処理を行うメソッドを実装しました。
これらを実装したのは、一般の整数ではなく<b><span style="color: #ff5252">自然数のみに対して定義される概念</span></b>であるためです。

作成した`NaturalNumber`クラスは以下の通りです。

```kotlin
// ①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`を利用することで値オブジェクトの<b>同一性</b>の判定処理がデフォルトで実装されます。
つまり、`data class`から生成した2つの値オブジェクトのプロパティが全て等しいとき、`equals()`はtrueとなります。

また、プロパティをimmutableな変数として宣言することで<b>不変性</b>も保たれます。

## ②`init`ブロックを利用したプロパティのバリデーション
`init`ブロックを利用することでプロパティのバリデーションを行っています。
今回の例では、自然数ではない整数がプロパティに指定されたときに`IllegalArgumentException`をthrowしています。

```kotlin
init {
    if (value < MIN_VALUE) {
        throw IllegalArgumentException("value must be greater than or equal to $MIN_VALUE.")
    }
}
```

## ③素数かどうか判定する、階乗した値を取得する
ここではただ関数を実装しているだけですが、階乗した値を取得する`getFactorialValue()`メソッドにおいて内部的に`tailrec`修飾子を利用しています。
階乗の計算のように、関数が自分自身を最後に呼び出すいわゆる[末尾再帰関数](https://kotlinlang.org/docs/reference/functions.html#tail-recursive-functions)(tail recursive function)に`tailrec`修飾子を付与することで、<b>スタック・オーバーフローを防ぐ形でコンパイラが最適に再帰呼び出しを行ってくれる</b>そうです。

また、階乗の計算結果は非常に大きな値になるため、オーバーフロー(こちらは単にIntの)したときに`ArithmeticException`をthrowするようにしました(いい方法が思い浮かばなかったので本当に一応ですが…)。

```kotlin
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`修飾子を付与することで、その関数に対応する<b>演算子のoverloadが可能</b>です。
関数と二項演算子の対応関係は[こちら](https://kotlinlang.org/docs/reference/operator-overloading.html#binary-operations)に記載されているので、必要に応じてoverloadします。
簡単のため、今回は加減乗除演算子のみoverloadしました。

```kotlin
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も可能です。
関数と比較演算子の対応関係は[こちら](https://kotlinlang.org/docs/reference/operator-overloading.html#comparison)です。大小関係のある値オブジェクトを扱うときは必ず実装することになると思います。

```kotlin
operator fun compareTo(other: NaturalNumber) = value.compareTo(other.value)
```

# まとめ
完全に思いつきで作成し始めたのですが、値オブジェクト用のクラスを作成してみたことでKotlinの便利な文法をいくつか知ることもでき、一石二鳥という感じでした。
ただ、「値オブジェクト用のクラス」の呼び方は何なのか(そもそもあるのか)最後まで疑問なまま書き進めてしまいました。抽象データ型というやつ？うーん。