Upgrade to Pro — share decks privately, control downloads, hide ads and more …

状態管理で理解するKotlinの sealed classおよび周辺機能

状態管理で理解するKotlinの sealed classおよび周辺機能

Avatar for simonNozaki

simonNozaki

October 25, 2022
Tweet

Other Decks in Programming

Transcript

  1. 0 | はじめに 1 | 題材の仕様確認 2 | sealed classを使う

    3 | JavaからみたKotlin 4 | まとめ ⽬次
  2. 野崎 才⾨ Nozaki Saimon 株式会社ネットプロテクションズ リードエンジニア いくつかの内製開 発案件のPjMを担当し、現在はBtoB向け決済サービス「NP掛け払い」 にて、与信システム刷新PJTをリードしています。 Kotlinは個⼈開発

    で触り始め、気づいたら内製推進のプロジェクトに採⽤してしまうく らい、その可能性に惹かれています。 株式会社ネットプロテクションズ リードエンジニア 3
  3. 6 本⽇お話すること_0|はじめに お話する題材と実装の概要 Kotlinの良さ • ECサイトなどで、⽀払いを回収しきれなかっ た場合に催促を作成するユースケースを想定 • 簡単なドメインモデルをもとに、Kotlinのク ラス機能を活かした実装例と良さを紹介する

    • 実装例とあわせて、KotlinのコードがJavaか らはどのように⾒えているかも軽く抑えてお く ⾔語仕様のうち、直感的に利⽤シーンをイメージしづらいsealed classを取り上げ、Kotlinをこう使うと 簡潔‧安全に実装できるのではないか、という話をします。 • Kotlinは、条件分岐や状態管理を、⼿続き主 体ではなく型による表現で簡潔かつ安全に 実装する⼿段を豊富に提供している • Java開発者のみならず、普段他の⾔語をメ インで使っている開発者の⽅にも、静的⽅ 付けと便利なシンタックスのバランス良さ は⼗分オススメできる
  4. ⽬次 0 | はじめに 1 | 題材の仕様確認 2 | sealed

    classを使う 3 | JavaからみたKotlin 4 | まとめ
  5. 実⽤的で⽇々の開発に便利 One of the main ideas behind Kotlin is being

    pragmatic, i.e., being a programming language useful for day- to-day development, which helps the users get the job done via its features and its tools. 8 Kotlinの思想_1 | 題材の仕様確認 Kotlinには、多様な状況に対応できるよう⾔語仕様が豊富に⽤意されています。 ⼀⽅で、リリースサイクルも早く、⾔語機能が豊富であるがゆえ、使い⽅がわかりづらいものも... 簡潔、安全、Javaとの 相互運⽤性 Concise, Safe, Interoperable ifやtry-catchなどの制御構⽂ が式であるなど、⼿続き 的‧宣⾔的なパラダイムを 柔軟に選択できる 巨⼤なJavaのモノレポを運 ⽤しているGoogleでも、 Kotlinがサーバサイドの推奨 ⾔語に Kotlin
  6. 9 sealed classとは_1 | 題材の仕様確認 sealed classとは コードイメージ 継承するクラスの種類を制約する抽象クラスです。 abstract

    class を拡張したような機能で、複数の 継承したクラスを持つことができます。 A sealed class is implicitly abstract (and these two modifiers are exclusive); リファレンスによると、 値が制限されたセットの1つの型を持つが、他の型を持てない場合、 シールクラスが制限されたクラス階層を表現する際に⽤いられます。 sealed class Expression { data class IntegerLiteral( val value: Int ) : Expression() data class ArrayLiteral( val items: List<Expression> ) : Expression() data class FunctionCall( val name: String, val args: List<Expression> ) : Expression() } 本セッションで取り扱う、sealed class(/interface) を概観しておきます。 ※なお、Javaでも17から正式な機能にsealed classが追加され、この点における機能差は⼩さくなりました。
  7. 10 ユースケース_1 | 題材の仕様確認 serverside kotlin meetupということで、サーバサイドでありそうな、⽀払いが漏れていたユーザに催促を作 成する ユースケースを題材に、実装を考えていきます。 ECサイトなど、ユーザからオンラインで注⽂を受けるようなサービス。

    対象ドメイン 注⽂⾦額どおりの⽀払いを期⽇までに受けられない場合に、⽀払いの催促を⾏う。 ユースケース 注⽂サービス、ユーザ 登場⼈物 注⽂サービス以降のイベント送信、状態遷移。 あくまで催促を作成するまでを取り扱います。 扱わないこと
  8. 13 ドメインモデルの補⾜_1 | 題材の仕様確認 sealed classを駆使し て、状態の分岐を型と して表現するオブジェ クト群。 ⽀払い状況を

    使った振る舞 いをもつエン ティティ。 今回は脇役で す。 ユースケースから導いた今回のアプリケーションで実装する対象のドメインモデルです。 ※ソースコード全⽂では、注⽂にまつわる他のエンティティも含んでおり、ドメインモデルは注⽂するところのみ記載。
  9. ⽬次 0 | はじめに 1 | 題材の仕様確認 2 | sealed

    classを使う 3 | JavaからみたKotlin 4 | まとめ
  10. 16 注⽂の実装_2 | sealed classを使う 催促作成の前提条件になる、注⽂を⽤意します。注⽂は、IDで識別するエンティティです。 data class Order (

    val id: String, val paymentState: PaymentState, val amount: Amount, val items: Map<Item, Quantity>, val orderedAt: LocalDateTime ) { fun needToRequest() = this.paymentState.needToRequest() fun getRemainedAmount(): Amount = when (paymentState) { is PaymentState.NotPayed -> amount is PaymentState.UnderPayment -> { val v = amount.value - paymentState.payedAmount.value Amount(v) } is PaymentState.OverPayment -> Amount(0) is PaymentState.Done -> Amount(0) } } 簡易ドメインモデル上の、 「注⽂」を実装する。
  11. 17 注⽂の実装_2 | sealed classを使う 催促作成の前提条件になる、注⽂を⽤意します。注⽂は、IDで識別するエンティティです。 data class Order (

    val id: String, val paymentState: PaymentState, val amount: Amount, val items: Map<Item, Quantity>, val orderedAt: LocalDateTime ) { fun needToRequest() = this.paymentState.needToRequest() fun getRemainedAmount(): Amount = when (paymentState) { is PaymentState.NotPayed -> amount is PaymentState.UnderPayment -> { val v = amount.value - paymentState.payedAmount.value Amount(v) } is PaymentState.OverPayment -> Amount(0) is PaymentState.Done -> Amount(0) } } 簡易ドメインモデル上の、 「注⽂」を実装する。 sealed classである PaymentState をフィール ドに持つ。 PaymentState を使った計 算ロジックを持つ。
  12. 19 ⽀払い状況の準備_2 | sealed classを使う 催促作成判断の元になる、⽀払い状況を作成します。⽀払い状況には複数の状態があり、かつ個別の状態を 持たせたいため、sealed classで型の縛りを設けます。 sealed class

    PaymentState { abstract fun needToRequest(): Boolean object NotPayed : PaymentState() { override fun needToRequest() = true } data class UnderPayment( val payedAmount: Amount ) : PaymentState() { override fun needToRequest() = true } data class OverPayment( val payedAmount: Amount ) : PaymentState() { override fun needToRequest() = false } object Done : PaymentState() { override fun needToRequest() = false } } ⽀払い状況を、抽象と具象 をそれぞれ実装。
  13. 20 ⽀払い状況の準備_2 | sealed classを使う 催促作成判断の元になる、⽀払い状況を作成します。⽀払い状況には複数の状態があり、かつ個別の状態を 持たせたいため、sealed classで型の縛りを設けます。 sealed class

    PaymentState { abstract fun needToRequest(): Boolean object NotPayed : PaymentState() { override fun needToRequest() = true } data class UnderPayment( val payedAmount: Amount ) : PaymentState() { override fun needToRequest() = true } data class OverPayment( val payedAmount: Amount ) : PaymentState() { override fun needToRequest() = false } object Done : PaymentState() { override fun needToRequest() = false } } ⽀払い状況を、抽象と具象 をそれぞれ実装。 注⽂から使うメソッド を具象クラスで実装さ せる PaymentState を 継承している。
  14. •Javaのenum classと⾔語仕様は概ね同じ。 単⼀のインスタンスを列挙する⽬的で⽤いる。 •列挙されたクラスは、すべて単⼀のインス タンスで、コンストラクタ以外に個別の状態 を定義できない。 21 enumとsealed classの違い_2 |

    sealed classを使う 単にクラスを列挙したいだけであれば、Kotlinにも備わっているenum classを使うことができるのですが、 意図や得意なケースは異なるものであります。 enum classの特徴 •継承先を限定するabstract class。 •個別にメソッドを定義できるし、状態を持 つこともできるが、継承先は同⼀パッケージ に収める必要がある。 •継承先のクラスは、objectを指定すること も可能。 sealed classの特徴
  15. 22 注⽂エンティティのメソッド_2 | sealed classを使う 注⽂エンティティでは、when式と組み合わせて網羅的に検査したのちに値を返すメソッドを定義します。 enumのように扱いたいクラス群をチェックする場合、複雑なif分岐が必要なところ、sealed classとwhen式 を組み合わせることで簡潔に表現できます。 fun

    getRemainedAmount(): Amount = when (paymentState) { is PaymentState.NotPayed -> amount is PaymentState.UnderPayment -> { val v = amount.value - paymentState.payedAmount.value Amount(v) } is PaymentState.OverPayment -> Amount(0) is PaymentState.Done -> Amount(0) } 注⽂エンティティから、⽀ 払い状況に応じて催促する 残額を計算する。
  16. 23 注⽂エンティティのメソッド_2 | sealed classを使う 注⽂エンティティでは、when式と組み合わせて網羅的に検査したのちに値を返すメソッドを定義します。 enumのように扱いたいクラス群をチェックする場合、複雑なif分岐が必要なところ、sealed classとwhen式 を組み合わせることで簡潔に表現できます。 fun

    getRemainedAmount(): Amount = when (paymentState) { is PaymentState.NotPayed -> amount is PaymentState.UnderPayment -> { val v = amount.value - paymentState.payedAmount.value Amount(v) } is PaymentState.OverPayment -> Amount(0) is PaymentState.Done -> Amount(0) } sealed classを継承したインスタンスは、when式を介する ことで網羅的なチェックを実現できます。 when式の中では、 is PaymentState.Type -> の構⽂で簡潔に記述することができます。 注⽂エンティティから、⽀ 払い状況に応じて催促する 残額を計算する。
  17. 24 おまけ: ユースケースを組み⽴てる_2 | sealed classを使う 最後に、本セッションの本筋ではないですが、ここまでで⽤意したオブジェクトを使って、簡単にユースケ ースクラスを実装して本節を締めます。 class CreateRequest(

    private val orderRepository: OrderRepository, private val requestRepository: RequestRepository ) { fun execute(req: CreateRequestRequest) { val order = orderRepository.findById(req.orderId) // elseの条件ないのがちょっと気持ち悪いけど... 条件逆にして適当にロギングでもOK if (order.needToRequest()) { val requestMethod = Request.fromTelOrEmail(req.tel, req.email) val remainedAmount = order.getRemainedAmount() val request = Request.of(requestMethod, remainedAmount) requestRepository.save(request) } } } 催促を作成するのにあたって、注⽂や催促の情報は外部サ ービスや永続化層とのやりとりをするものとし、サンプル ではリポジトリも⽤意しました。 本セッションの主題からは外れるので説明を割愛します。
  18. 25 おまけ: sealed interface_2 | sealed classを使う 継承先を制限するinterfaceとして sealed interface

    を利⽤することもできます。 今回の例では、親に状態がないのでsealed interfaceでも代替可能でした。 sealed interface PaymentState { fun needToRequest(): Boolean object NotPayed : PaymentState { override fun needToRequest() = true } data class UnderPayment( val payedAmount: Amount ) : PaymentState { override fun needToRequest() = true } data class OverPayment( val payedAmount: Amount ) : PaymentState { override fun needToRequest() = false } object Done : PaymentState { override fun needToRequest() = false } } sealed class PaymentState { abstract fun needToRequest(): Boolean object NotPayed : PaymentState() { override fun needToRequest() = true } data class UnderPayment( val payedAmount: Amount ) : PaymentState() { override fun needToRequest() = true } data class OverPayment( val payedAmount: Amount ) : PaymentState() { override fun needToRequest() = false } object Done : PaymentState() { override fun needToRequest() = false } }
  19. ⽬次 0 | はじめに 1 | 題材の仕様確認 2 | sealed

    classを使う 3 | JavaからみたKotlin 4 | まとめ
  20. 27 逆アセンブルする準備_3 | Javaから⾒たKotlin Kotlinのsealed classが、Java(バイトコード)ではどのように取り扱われるか、Javaからも確かめます。 • Kotlinのソースコードをコンパイルするには、直に kotlinc を使っ

    てもよいですが、ここでは⾯倒なので Gradle に頼っています。 ◦ 左図はIntellij IDEA上のキャプチャです。 • ビルドはIDE上で実⾏してもいいですし、CLIから実⾏しても同じ ような結果を得られると思います。
  21. 28 クラスファイルになったsealed class_3 | Javaから⾒たKotlin user$ pwd tiny-payer/build/classes/kotlin/main/io/github/simonnozaki/tinypayer/domain user$ ls

    -la total 48 drwxr-xr-x 10 user group 320 10 11 13:00 . drwxr-xr-x 6 user group 192 10 11 13:00 .. -rw-r--r-- 1 user group 2199 10 11 13:00 Amount.class -rw-r--r-- 1 user group 1011 10 11 13:00 PaymentState$Done.class -rw-r--r-- 1 user group 1023 10 11 13:00 PaymentState$NotPayed.class -rw-r--r-- 1 user group 3083 10 11 13:00 PaymentState$OverPayment.class -rw-r--r-- 1 user group 3090 10 11 13:00 PaymentState$UnderPayment.class -rw-r--r-- 1 user group 1478 10 11 13:00 PaymentState.class 親である PaymentState と同じパッケージに、ネストされたサブクラスのクラスファイルができることが わかります。 ※以下はKotlinソースから、コンパイルした結果を端末上で ls した結果です。
  22. 29 sealed classをJavaから覗く_3 | Javaから⾒たKotlin $ javap PaymentState.class Compiled from

    "PaymentState.kt" public abstract class io.github.simonnozaki.tinypayer.domain.PaymentState { public abstract boolean needToRequest(); public io.github.simonnozaki.tinypayer.domain.PaymentState(kotlin.jvm.internal.DefaultConstructorMark er); } PaymentState は public abstract class となっていることが確認できます。 ※シェルから持ってきた出⼒ですが、わかりやすさのためキーワードはシンタックスハイライトを⼊れています。
  23. 30 sealed classをJavaから覗く_3 | Javaから⾒たKotlin sealed classを継承した具象クラスは、Javaの extends で継承し、sealed classは暗黙のうちにabstract

    classである、という⾔語仕様を確認できます。 Done は object で宣⾔していますが、シ ングルトンクラスとして扱われるので、 sealed classを継承可能なのでした。 Kotlin…
  24. $ javap PaymentState¥$Done.class Compiled from "PaymentState.kt" public final class io.github.simonnozaki.tinypayer.domain.PaymentState$Done

    extends io.github.simonnozaki.tinypayer.domain.PaymentState { public static final io.github.simonnozaki.tinypayer.domain.PaymentState$Done INSTANCE; public boolean needToRequest(); static {}; } 31 sealed classをJavaから覗く_3 | Javaから⾒たKotlin sealed classを継承した具象クラスは、Javaの extends で継承し、sealed classは暗黙のうちにabstract classである、という⾔語仕様を確認できます。 Done は object で宣⾔していますが、シ ングルトンクラスとして扱われるので、 sealed classを継承可能なのでした。 Java クラスファイル Kotlin…
  25. $ javap PaymentState¥$Done.class Compiled from "PaymentState.kt" public final class io.github.simonnozaki.tinypayer.domain.PaymentState$Done

    extends io.github.simonnozaki.tinypayer.domain.PaymentState { public static final io.github.simonnozaki.tinypayer.domain.PaymentState$Done INSTANCE; public boolean needToRequest(); static {}; } 32 sealed classをJavaから覗く_3 | Javaから⾒たKotlin sealed classを継承した具象クラスは、Javaの extends で継承し、sealed classは暗黙のうちにabstract classである、という⾔語仕様を確認できます。 Done は object で宣⾔していますが、シ ングルトンクラスとして扱われるので、 sealed classを継承可能なのでした。 Java クラスファイル Kotlin… object 宣⾔で定義したインスタンスは、Javaではシングルトンになっているので、逆アセンブルすると public static finalな INSTANCE になっているのが⾒えます。
  26. 33 おまけ: Javaでswitchする_3 | Javaから⾒たKotlin Javaにも、sealed classおよびrecord機能が⼊りました。sealed interfaceをrecordで実装する例です。 public sealed

    interface PaymentState permits PaymentState.Done, PaymentState.NotPayed, PaymentState.OverPayed, PaymentState.UnderPayed { boolean needToRequest(); record NotPayed() implements PaymentState { @Override public boolean needToRequest() { return true; } } record UnderPayed(Amount payedAmount) implements PaymentState { @Override public boolean needToRequest() { return true; } } // 以下略 }
  27. 34 ⽬次 0 | はじめに 1 | 題材の仕様確認 2 |

    sealed classを使う 3 | JavaからみたKotlin 4 | まとめ
  28. 35 振り返り_4|おわりに 本セッションでは、催促を作るユースケースを題材に、 sealed classを⽤いた簡潔かつ強⼒な、Kotlinによる実装を⽰しました。 また、Kotlin上では便利なシンタックスも、Javaから眺めると普段使っている⾔語仕様に なっていることが確認できました。 充実した標準ライブラリやビルドツールとの連携、IDEとのシームレスな統合など、 Kotlinには⽇々の開発に便利な多くの特徴や機能が、進化し続けています。 KotlinはJetBrains社が開発をリードしており、Intellij

    IDEAやGradleとの連携が 極めてシームレスで使いやすいことは、Kotlinを選択する魅⼒の⼀つでもあります。 静的型付けの安定感と、簡潔かつ体験の良いシンタックスは、 サーバサイドの⾔語選択におけるKotlinの良さを後押しするものであります。 もし使ったことがなければ、この機会に使ってみてはいかがでしょうか。
  29. 36 参考_4|おわりに Kotlin language spesification - https://kotlinlang.org/spec/introduction.html - https://kotlinlang.org/spec/inheritance.html#sealed-classes-and-interfaces Sealed

    classes - https://kotlinlang.org/docs/sealed-classes.html JEP 409: Sealed Classes - https://openjdk.org/jeps/409 JEP 433: Pattern Matching for switch (Fourth Preview) - https://openjdk.org/jeps/433 Creating The Best Programming Language: The Story of Kotlin - https://youtu.be/uE-1oF9PyiY Google's Journey from Java to Kotlin for Server Side Programming by James Ward , Brad Hawkes , John - https://youtu.be/o14wGByBRAQ