😆

ValueObjectをいい感じに演算子計算できるようにしてみた

2022/03/11に公開

演算子オーバーロードとは

https://kotlinlang.org/docs/operator-overloading.html

kotlinには演算子(operator)による算術をIntやLongといった数値プリミティブ以外にも適用することができる言語機構が備わっており、演算子オーバーロードと呼ばれています。
規定の名称の関数を実装することで、+*といった演算子を自作の型にも適用ができるようになる便利な仕組みです。

ValueObjectに適用してみる

内部にプリミティブを一つ抱えこむようなバリューオブジェクト[1]を作ることって頻繁にあるかと思います。数値的なものだと足したり・引いたり・比較したりしたくなるケースは必ず出てくると思うので、そういった時に記述をシンプルにできます。

@JvmInline
value class Age(val value: Int) {
    operator fun plus(other: Age): Age {
        return Age(value + other.value)
    }
}

fun main() {
    println(Age(10) + Age(20)) // => Age(30)を得られます
}

計算できるValueObjectに効率良く適用してみる

ValueObjectを用意する度にボイラーのように実装するのは辛いなって思ったのでなんとかできないか考えてみました。(※リフレクションを使います。)

  • Kotlinでリフレクションを使うためには追加のライブラリが必要になります。
build.gradle.kt
runtimeOnly("org.jetbrains.kotlin:kotlin-reflect:1.6.10")

共通実装を提供するためのインターフェースを定義して、そのインターフェースに対する拡張関数の形で演算子オーバーロードを実装していきます。

interface IntegralNumberValue {
    val value: Long
}

inline operator fun <reified T: IntegralNumberValue> T.plus(other: T): T {
    val newValue = value + other.value
    return T::class.constructors.first().call(newValue)
}

これを先のValueObjectに対して実装します。

@JvmInline
value class Age(override val value: Long): IntegralNumberValue

fun main() {
    println(Age(10) + Age(20)) // => Age(30)を得られます
}

基本になりそうないくつかの演算子に対応する関数をひとつ定義しておけば後はinterfaceを実装するだけで新しいValueObjectがみな自動的に演算子計算に対応してくれるようにできました。

まとめ

演算子オーバーロードは可読性を高めてくれるので積極的に使っていきたいなと思います。
実装面でリフレクションを使わずに演算後のインスタンスの再生成を可能にする方法がすぐには思いつかなかったので、そこが課題となりました。

作ったもの

https://github.com/PGUMA/values

脚注
  1. DDD(ドメイン駆動設計)におけるクラスの設計パターンのひとつです。参考 ↩︎

Discussion