23
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

KotlinAdvent Calendar 2020

Day 9

KotlinのSealed Classは良いよ、という話

Last updated at Posted at 2020-12-05

普段Kotlinでサーバーサイドの開発をしてるのですが、Sealed Classという機能が好きで多用しているので、
今後初めてKotlinを使う人に布教するために何が良いのかをまとめておきます。
Kotlinアドベントカレンダーの9日目です。

Sealed Class is 何?

公式はこちら
https://kotlinlang.org/docs/reference/sealed-classes.html

sealed classの特徴を端的にまとめると

  • enumのように有限個の状態を表現できる
  • enumと違ってあくまでクラスなので、複数インスタンスを生成できる(=値や振る舞いを持たせることができる)
  • when句を使うことで網羅性を担保できる (←これ重要、というかこれが言いたい)

Sealed Classの例

上記の説明だとなんのこっちゃわからないと思うのでコードの例です。

sealed class BaseBallTeam {
    class Fighters: BaseBallTeam()
    
    class Hawks: BaseBallTeam()
    
    class Giants: BaseBallTeam()
    
    class Tigers: BaseBallTeam()
}

上記のようにスーパークラスであるBaseBallTeamの前にsealedという修飾子がついているだけで、
それ以外は通常のクラス定義と継承の書き方と変わりません。
今回は特にメソッドやプロパティを定義していませんが、当然それらを継承するという点でも通常のクラスと全く同じです。
(サブクラスがinner classになっているのはただの好みで、深い意味はないです。)

普通のクラスとの違い

これだけ書くと一体何が普通のクラスと違うのかという話になりますが、大きく違う点があります。
それは、スーパークラスを継承できるのは同じファイル内で定義されるクラスに限られていて、それによりサブクラスの種類が有限であることがコンパイル時点で保証されるという点です。

どういうことでしょうか。

通常のクラスを継承しようとしたら以下のような感じになるかと思います。

BaseBallTeam.kt
open class BaseBallTeam
Fighters.kt
class Fighters: BaseBallTeam
Hawks.kt
class Hawks: BaseBallTeam

よく見る一般的な継承ですが、この問題点はなんでしょうか?
それは BaseBallTeamはopenなのでどこからでも継承できる、すなわちどういったサブクラスが存在するかの全量を把握することができません。

一方Sealed Classは上記の通り、スーパークラスが定義された同じファイル内でしかサブクラスを定義できない、
つまりそのファイル内で定義されたサブクラスのみが存在し得る全てのサブクラスであると保証される、というのが大きな違いです。

繰り返しになりますが、この同じファイル内にのみ閉じている(sealed)というのが最大の特徴です。

サブクラスが有限だと何が嬉しいのか?

サブクラスが有限なのはわかったとして、だから何が良いのでしょうか?
それはwhen句を使ってenumのような列挙型と同じように網羅性を担保できるという点です。

例えば下記のようなメソッドを定義したとしましょう。

// BaseBallTeamがsealed classでなく通常のclassだった場合
fun getDirecterName(baseBallTeam: BaseBallTeam): String =
    when (baseBallTeam) {
        is BaseBallTeam.Fighters -> "栗山"
        is BaseBallTeam.Hawks -> "工藤"
        is BaseBallTeam.Giants -> "原"
        is BaseBallTeam.Tigers -> "矢野"
        else -> "N/A"
    }

このメソッドの問題点はなんでしょうか?それはサブクラスが新たに増えた時に顕在化します。
例えば新たにEaglesというチームが増えたとします。
このgetDirectorNameEaglesのインスタンスを渡した場合、期待されている監督の氏名ではなく「N/A」が出力されてしまいます。
しかもこれは実際に実行してみるまで気がつくことができません。

本来ならEaglesが追加された時点でこのwhenにもEaglesをハンドリングするための分岐が必要ですが、開発者がそれを把握していないと当然修正が漏れてしまいます。

そこでsealed classだとどうなるでしょうか

// BaseBallTeamがsealed classの場合
fun getDirecterName(baseBallTeam: BaseBallTeam): String =
    when (baseBallTeam) {
        is BaseBallTeam.Fighters -> "栗山"
        is BaseBallTeam.Hawks -> "工藤"
        is BaseBallTeam.Giants -> "原"
        is BaseBallTeam.Tigers -> "矢野"
    }

定義はこのように変わります。違いは一つ、isのいずれにも当てはまらなかった場合のデフォルト値を意味するelseが消えています。

なぜこのようなことが可能なのかというと、上記の通りBaseBallTeamのサブクラスはここで列挙されている4つしかないことが保証されているからです。

さて、ここで新たにサブクラスが増えたらどうなるのでしょうか?
先ほどと同様にEaglesというチームが増えた場合、
Eaglesをハンドリングする分岐が存在しないためコンパイルエラーになります。やったね。

これによりサブクラスが追加された際に、上記のwhenが抜け漏れなく全てのサブクラスを網羅されていることをコンパイル時点で保証することができるのです。
これこそがsealed classの最大の魅力であります。

逆に言えばsealed classを扱うwhenにはelse句は書いてはいけないということになりますね(もちろん書くことはできますが、それでは網羅性が担保されなくなります。)

それEnumじゃだめなの?

当然出てくる疑問です。whenで網羅性を確保したいならenumで良いんじゃないかと。
結論を言ってしまうとenumでもまあ出来ることもあるし出来ないこともある、が正解だと思います。

上記の例のようにただ列挙したい場合はenumでも十分だと思います。
enumとの大きな違いはenumは定数でありsealed classは状態を持つことができるインスタンスであるという点です。

上記の例ではほぼ定数しかなかったのですが、例えばhttpリクエストの結果を表現したい場合を考えてみます。

sealed class HttpResponse {
    data class Success(val responseCode: Int)
    data class Failure(val responseCode: Int, val errorMessage: String)
}

enum class HttpResponseEnum {
    Success,
    Failure
}

httpリクエストの結果を表す型をsealed classとenumで実装しました。長くなるので単純に成功と失敗の2種類のみにしています。
両者の違いはどこにあるでしょうか?それはsealed classの実装ではレスポンスコードやエラーメッセージなどのインスタンスごとに異なる状態を保持できる点です。

もしこれをenumと通常のクラスで実現するとしたら以下のような感じになるでしょうか(適当です)

enum class HttpResponseEnum {
    Success,
    Failure
}
abstract class HttpResponse(val statusCode: Int) {
    abstract val type: HttpResponseEnum
}
class HttpResponseSuccess(statusCode: Int): HttpResponse(statusCode) {
    override val type: HttpResponseEnum = HttpResponseEnum.Success
}
class HttpResponseFailure(statusCode: Int, val errorMessage: String): HttpResponse(statusCode) {
    override val type: HttpResponseEnum = HttpResponseEnum.Failure
}

もしwhenを使って列挙型を評価して、Failureだった場合にはエラーメッセージを表示するような関数を実装したい場合、どうすればいいでしょうか?
上記の実装では実現できないので、何かしら別のアプローチを取る必要があります。
方法は色々とあると思いますが、たった4行で実現できるsealed classを使うのが一番簡潔に表現できるのはほぼ間違い無いでしょう。

まとめ

思ったよりたらたらと書いてしまいましたが、sealed classの便利さを少しでもお伝えすることができていたら嬉しいです。
Kotlinは本当にかわいい言語だと思うのでみんなもっと使いましょう、特にサーバーサイドで。

23
12
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?