すきま風

勉強したことのメモとか

Kotlin sealed classを活用したい (願望)

sealed classはクラスの継承を制限するための機構です。 継承を同一ソースファイル内で定義したclassのみに制限します。(Kotlin 1.3)
残念ながら個人的にいまいち使いこなせていない (^^;) のでブログに使用法をまとめてみます。

拡張enumとしての利用

概念としては同じだが、propertyが微妙に異なるような場合に活用します。

// AmazonのようなECサイトを想定
// 自社倉庫管理商品とマーケットプレイス商品を定義する
// Item.kt
sealed class Item(
    val name: String,
    private val price: Int
) {
    abstract fun getPrice(): Int
}

data class PrivateItem(
    private val _name: String,
    private val _price: Int
) : Item(_name, _price) {
    override fun getPrice() = _price
}

data class MarketPlaceItem(
    private val _name: String,
    private val _price: Int,

    // 手数料がかかる場合がある
    val fee: Int
) : Item(_name, _price) {
    override fun getPrice() = _price + fee
}

// main.kt
val items = listOf(
    PrivateItem("自社商品", 1000),
    MarketPlaceItem("アディダス", 2000, 200)
)

val sum = items.sumBy { it.getPrice() }
println(sum) // -> 3200

// sealed classで定義すると同ソースコード以外での継承が不可能であることをコンパイラが認知できるためelse句が不要になる
val type = when (items[0]) {
    is PrivateItem -> "自社商品"
    is MarketPlaceItem -> "マーケットプレイス商品"
}
println(type) // -> 自社商品

Exception Handlingの代替

https://phauer.com/2019/sealed-classes-exceptions-kotlin/ など、 その他でもいろいろなところで紹介されているパターンです。
私は主に、domain層からinfra層のmethodをcallした際に、正常に終了した場合と、Domain的な例外 (処理自体は終了しているが、取得した値が不正だったりするケース) の表現に活用しています。

// SuccessとFailureを表現するsealed class
sealed class DomainResult<out S, out F> {
    class Success<S>(val result: S) : DomainResult<S, Nothing>()

    class Failure<F>(val result: F) : DomainResult<Nothing, F>()
}

// APIをcallしてKeyを取得するService class
object GetKeyService {
    private lateinit var api: GetKeyApi

    fun fetch(): DomainResult<SuccessResult, ErrorResult> {
        // 外部APIをcall
        val key: Key = api.get()

        // keyが正しい値の場合Successを返す。不正の場合はエラーメッセージを返す。
        // KeyNotFoundException() などはthrowしない
        return if (key.isValid())
            DomainResult.Success(SuccessResult(key))
        else
            DomainResult.Failure(ErrorResult("invalid key"))
    }
}

data class SuccessResult(
    val key: Key
)

data class ErrorResult(
    val message: String
)

data class Key(val value: String) {
    fun isValid() = value.isNotBlank() && value != "invalid"
}

// service call
// kotlin.resultを利用してみる
// https://github.com/Kotlin/KEEP/blob/master/proposals/stdlib/result.md
runCatching {
    GetKeyService.fetch()
}.onSuccess {
    when (it) {
        is DomainResult.Success -> {
            val key = it.result.key
        }
        is DomainResult.Failure -> {
            val errorMessage = it.result.message
        }
    }
}.onFailure {
    // IOExceptionなど. 明らかな例外をキャッチしてハンドリングする
    logger.info(it)
}

whenを使ったわかりやすい記述ができるようになり、例外をキャッチしてハンドリングする従来のやり方よりも可読性が向上します。

まとめ

実際に使っているところを見るとなるほどなーと思うのですが 自分で開発していて、ここはsealed classを使うべきだな、となかなか気づけないんですよね。自分の感覚に落とし込むにはもう少し実践が必要そうです。