最近思いついた実装テクニックについて書きます。
車輪の再発明だとは思いますが、このような実装パターンを何と呼ぶか分からなかったので、コメントで教えて頂けると嬉しいです!
状況
動的なメンバーを持つ(実装クラスがobjectでない)sealed class1をenum的に使いたい状況は多々ある一方、いい感じの書き味を実現するには工夫が必要です。
例として、以下のStatusと、Statusをメンバーに持つFooが有るとします。
sealed interface Status {
val remarks: String
data class Active(override val remarks: String) : Status
data class Inactive(override val remarks: String) : Status
}
data class Foo(val status: Status)
ここで、「特定Statusを持つFooだけ抽出したい」という場合、検索関数はどのようなインターフェースになるでしょうか?
Statusがenumであったなら、以下のように書けます。
interface Repository {
fun searchFoo(vararg statuses: Status): List<Foo>
}
一方、実際のStatusは静的でないため、上記のような書き方はできません。
データストアへアクセスするためにStatusのインスタンスが必要なのに、Statusのインスタンスを得るためにはデータストアアクセスが必要という、矛盾した状態が生じます。
解決策
Statusの下にsealed classを定義し、それを継承したcompanion objectを各実装クラスへ持たせると、いい感じの書き味にできます。
sealed interface Status {
val remarks: String
sealed interface Meta
data class Active(override val remarks: String) : Status {
companion object : Meta
}
data class Inactive(override val remarks: String) : Status {
companion object : Meta
}
}
検索関数の例
先ほど紹介した検索関数は、以下のように定義できるようになります。
外見上はほとんどenumの場合と同じになっています。
interface Repository {
fun searchFoo(vararg statuses: Status.Meta): List<Foo>
}
利用側は以下のようになります。
これは外見上enumだった場合と同じにできています。
val result = repository.searchFoo(Status.Active, Status.Inactive)
その他の利点
sealed classは当然メンバーを持てます。
つまり、enumのnameのような定数プロパティを用意したり、静的な関数を持たせることができます。
この書き味についても、enumと同等と言えるでしょう。
val name: String = Status.Active.name
補足
実装クラスにobjectが混じる場合、実装自体にMetaインターフェースを継承させることで、同じ書き味を維持できます。
sealed interface Status {
val remarks: String
sealed interface Meta
/* ... */
data object Inactive : Status, Meta {
override val remarks: String get() = "..."
}
}
終わりに
このようにいい感じの書き味を実現できることはKotlinの非常に大きな魅力だと日々感じています。
Have a nice Kotlin!
-
簡単のため、記事内では
sealed classとsealed interfaceをまとめてsealed classと呼称します。 ↩