$30 off During Our Annual Pro Sale. View Details »

“Designing with types” に学ぶ Kotlin における型を利用したモデリング術

omuomugin
December 12, 2022

“Designing with types” に学ぶ Kotlin における型を利用したモデリング術

2022/12/12
Kotlin Fest Reject Conference 2022

https://henry.connpass.com/event/267081/

omuomugin

December 12, 2022
Tweet

More Decks by omuomugin

Other Decks in Technology

Transcript

  1. “Designing with types”
    に学ぶ
    Kotlin
    における型を利用したモデリング術
    by @omuomugin
    2022/12/12 Kotlin Fest Reject Conference 2022

    View Slide

  2. 自己紹介
    佐々木俊亮 (@omuomugin)
    Job:
    株式会社リクルート
    ソフトウェアエンジニア
    結婚領域で新規サービスの開発
    SpringBoot (Kotlin) + Next.js
    2 / 37

    View Slide

  3. 今日話すこと
    Designing with Types
    と Domain Modeling Made Functional
    の内容を
    中心に時間が限られているので以下に限定して発表します
    1.
    直和 を用いた状態の表現
    2.
    状態遷移図の実装
    特に書籍の中では、DDD
    の精神から関数型のテクニックなども扱って
    いるので興味があればぜひ読んでみてください

    直和の説明は後ほどするのでお待ちを
    3 / 37

    View Slide

  4. 発表の元となる書籍とブログの紹介
    Designing with Types
    というブログシリーズ
    Domain Modeling Made Functional
    の著者で
    もある Scott Wlaschin
    さんが書いたもの
    ブログシリーズが 2013
    年で、本が 2018
    年に
    出版されており、本の内容の中核はこのブロ
    グシリーズで話してるものを改変したもの
    (
    読んだ個人の感想)
    Scott Wlaschin
    さんが F#
    を好んで利用するよ
    うで、本もブログも F#
    での説明となっている
    4 / 37

    View Slide

  5. 前提知識編
    5 / 37

    View Slide

  6. 直和と直積
    data class Hoge(val a: A, val b: B)
    // A = {a_1, a_2, a_3, ..., a_n} (
    要素数 n)
    // B = {b_1, b_2, b_3, ..., b_m} (
    要素数 m)
    A

    B
    という型はそれぞれ取れる値の集合が定義されてる
    このとき
    Hoge
    の値としての等価を考えたときに
    Hoge

    n * m
    の値
    を取り得る
    このように取り得る値が 積 によって決まるような性質の型を
    代数的データ型 (a.k.a Algebraic Data Type , ADT )
    の世界では 直積型
    (a.k.a Product Type )
    と呼ぶ
    前提知識編
    6 / 37

    View Slide

  7. 直和と直積
    data class Hoge(val fuga: A | B) //
    もちろん Kotlin
    にこんな記法はないのでイメージ
    // A = {a_1, a_2, a_3, ..., a_n} (
    要素数 n)
    // B = {b_1, b_2, b_3, ..., b_m} (
    要素数 m)
    Typescript
    などを利用したことがある人にとっては馴染みのある
    |
    は、
    A

    B
    のうちのいずれかである という意味
    この場合
    Hoge
    がとり得る値は
    n + m
    となる
    このように取り得る値が 和 によって決まるような性質の型を
    代数的データ型 の世界では 直和型 (a.k.a Sum Type , Union ,
    Tagged Union Type )
    と呼ぶ
    前提知識編
    7 / 37

    View Slide

  8. Kotlin
    による直和の表現
    Sealed Class
    を利用するのが一般的
    サブクラスがコンパイルタイムで限定できるようにする言語機能
    以下の記述は、
    Hoge
    は HogeWithA
    と HogeWithB
    のいずれかである という
    ことを表現してる
    sealed class Hoge
    data class HogeWithA(val a: A) : Hoge()
    data class HogeWithB(val b: B) : Hoge()
    ※ Sealed Class
    は、実は Java 17
    で Java
    にも正式に言語機能として追
    加されている JEP 409 (JEP = JDK Enhancement Proposal)
    前提知識編
    8 / 37

    View Slide

  9. さて準備ができたので直和を使う例を見ていこう
    9 / 37

    View Slide

  10. 2
    つの例に沿って、以下の具体例を紹介
    1.
    直和 を用いた状態の表現
    2.
    状態遷移図の実装
    例 1:
    メールアドレスの認証状態
    例 2: EC
    サイトにおけるカートの内容
    10 / 37

    View Slide


  11. 1:
    メールアドレスの認証状態
    メールアドレスには、認証済みのものとそうでないものがある
    新規作成する場合には常に「未認証のメールアドレス」となる
    パスワードリセットのメールはセキュリティ観点で「認証済メール
    アドレス」にしか送信してはならない
    11 / 37

    View Slide

  12. ドメインオブジェクトのライフサイクル
    Domain Driven Design
    の文脈においても語られているようにビジネス
    ドメインにとって重要なモデルというのは様々な状態を持つ
    したがって「状態」というものをいかにモデリングしていくかが重要
    となる
    すべてのオブジェクトにはライフサイクルがある。オブジェクト
    は誕生した後、おそらく様々な状態を経て、最終的には死ぬ。死
    ぬと言っても、アーカイブされるか削除されるということだ。

    エリック・エヴァンスのドメイン駆動設計


    12 / 37

    View Slide

  13. (
    再掲
    )

    1:
    メールアドレスの認証状態
    メールアドレスには、認証済みのものとそうでないものがある
    新規作成する場合には常に「未認証のメールアドレス」となる
    パスワードリセットのメールはセキュリティ観点で「認証済メール
    アドレス」にしか送信してはならない
    13 / 37

    View Slide

  14. まず簡単に考えると
    isVerified
    のようなフラグでの制御が思いつく
    data class EmailAddress(
    val address: String
    val isVerified: Boolean
    )
    //
    パスワードリセットのメールの送信
    fun sendPasswordResetEmail(email: EmailAddress) {
    if(email.isVerified) { emailSendService.send(to = email.address, body = ....) }
    }
    しかしフラグだと「パスワードリセットのメールの送信」の際に
    isVerified
    の値を参照し忘れたり、
    isVerified
    を誤って更新・代入
    してしまうケースがある

    もっと安全かつ明確に状態を表現したい
    例1:
    メールアドレスの認証状態
    14 / 37

    View Slide

  15. そこで直和を利用して型として 2
    つの状態を表現する
    sealed class EmailAddress(abstract val address: String)
    data class UnverifiedEmailAddress(override val address: String): EmailAddress()
    data class VerifiedEmailAddress(override val address: String) : EmailAddress()
    //
    パスワードリセットのメールの送信
    //
    認証済みのメールアドレスに対してのみ呼び出し可能
    fun sendPasswordResetEmail(emailAddress: VerifiedEmailAddress) {
    emailSendService.send(to = emailAddress.address, body = ....)
    }

    安全かつ明確に状態を表現できている
    例1:
    メールアドレスの認証状態
    15 / 37

    View Slide

  16. 状態遷移図の実装
    fun create(emailAddress: String): EmailAddress {
    return UnverifiedEmailAddress(emailAddress)
    }
    fun verfied(email: EmailAddress): EmailAddress {
    // when
    には 網羅(exhaustive)
    性があるので else
    などが不要
    return when(email) {
    is UnverifiedEmailAddress -> VerifiedEmailAddress(email.address)
    is VerifiedEmailAddress -> email //
    そのまま返す
    }
    }
    例1:
    メールアドレスの認証状態
    16 / 37

    View Slide

  17. 補足
    : when

    exhaustive
    性について
    Sealed Class
    は、サブクラスが有限であることがわかっているため
    when
    の exhaustive
    性 の恩恵を受けることができる
    fun verfied(email: EmailAddress): EmailAddress {
    //
    例えば、 `WaitingVerifyEmailAddress`
    という状態が
    //
    加わった時には、 `when`
    がコンパイルエラーを起こしてくれる
    return when(email) {
    is VerifiedEmailAddress -> email //
    そのまま返す
    is UnverifiedEmailAddress -> VerifiedEmailAddress(email.address)
    is WaitingVerifyEmailAddress -> {} //
    ここの分岐がないとコンパイルエラーになる
    }
    }
    例1:
    メールアドレスの認証状態
    17 / 37

    View Slide

  18. Tips
    本書の実装からさらに発展させて自分たちのプロダクトで
    採用している実装方法を紹介します
    18 / 37

    View Slide

  19. Tips1:
    ファクトリを利用する
    // `create`
    はファクトリとして用意することでより新規作成する場合には常に「未認証のメールアドレス」となる
    //
    という仕様を明確にコードで表現することもできる
    data class UnverifiedEmailAddress private constructor(
    override val address: String
    ): EmailAddress() {
    companion object {
    fun create(emailAddress: String): UnverifiedEmailAddress {
    return UnverifiedEmailAddress(emailAddress)
    }
    }
    }
    //
    利用例
    val email = UnverifiedEmailAddress.create("[email protected]")
    例1:
    メールアドレスの認証状態
    19 / 37

    View Slide

  20. Tips2:
    ドメインモデルのアクションとして定義する
    // `verified`
    を `UnverifiedEmailAddress`
    のアクションとして定義することで
    //
    状態遷移がより明確に表現することもできる
    data class UnverifiedEmailAddress(
    override val address: String
    ) : EmailAddress() {
    fun verify(): VerifiedEmailAddress {
    return VerifiedEmailAddress(email.address)
    }
    }
    //
    利用例
    //
    それぞれの結果が Immutable
    になっていてテスタビリティも高い
    val unverifiedEmailAddress = UnverifiedEmailAddress.create("[email protected]")
    val verifiedEmailAddress = unverifiedEmailAddress.verify()
    例1:
    メールアドレスの認証状態
    20 / 37

    View Slide

  21. なんとなく雰囲気を掴めたと思うのでもう
    1

    の例でも直和を活用してみましょう
    21 / 37

    View Slide


  22. 2: EC
    サイトにおけるカート
    カートには、「空の状態」、「未購入」、「購入済み」の 3
    つの状
    態がある
    アクションとしては以下がある
    カートにアイテムを追加する
    購入する
    22 / 37

    View Slide

  23. sealed class Cart
    object EmptyCart : Cart()
    data class ActiveCart(val unpaidItems: List) : Cart()
    data class PaidCart(val paidItems: List, val payment: Float) : Cart()
    -> 3
    つの状態が明確に実装から読み取れる
    ※ field
    を持たないような
    Sealed Class
    の子は
    object
    で表現せよとい
    う lint
    の warning
    が出る
    例2: EC
    サイトにおけるカート
    23 / 37

    View Slide

  24. 状態遷移図の実装
    (add item)
    //
    もちろん Tips2
    のように EmptyCart
    や ActiveCart
    のアクションとして実装することも可能
    fun addItem(cart: Cart, item: Item): Cart {
    return when(cart) {
    is EmptyCart -> ActiveCart(listOf(item))
    is ActiveCart -> ActiveCart(cart.unpaidItems + item)
    is PaidCart -> throw Error()
    }
    }
    例2: EC
    サイトにおけるカート
    24 / 37

    View Slide

  25. 状態遷移図の実装
    (pay)
    //
    もちろん Tips2
    のように ActiveCart
    のアクションとして実装することも可能
    fun pay(cart: Cart: payment: Float): Cart {
    return when(cart) {
    is EmptyCart -> throw Error()
    is ActiveCart -> PaidCart(cart.unpaidItems, payment)
    is PaidCart -> throw Error()
    }
    }
    例2: EC
    サイトにおけるカート
    25 / 37

    View Slide

  26. まとめ
    2
    つの例に沿って、以下の具体例を紹介した
    1.
    直和 を用いた状態の表現
    2.
    状態遷移図の実装
    例 1:
    メールアドレスの認証状態
    例 2: EC
    サイトにおけるカートの内容
    26 / 37

    View Slide

  27. 余談
    時間が余れば発表する
    Null Object Pattern
    直和でしか表現できないデータ構造
    27 / 37

    View Slide

  28. 余談
    1: Null Object Pattern
    28 / 37

    View Slide

  29. if (customer == null) {
    plan = BillingPlan.basic();
    } else {
    plan = customer.plan;
    }
    doSomething(plan)
    上のコード一見わかりやすいが
    customer

    null
    の場合のビジネス
    要求が正確に理解できない
    おそらく 「新規のお客様の場合にはデフォルトのプランが適用され
    る」 とかだと思う
    余談1: Null Object Pattern
    29 / 37

    View Slide

  30. null

    non-null
    の 2
    つの状態を明確に状態として直和を用いて表現
    してみる
    sealed class Customer
    object NewRegisteredCustomer : Customer()
    data class ExistingCustomer(val plan: BillingPlan) : Customer()
    val plan = when(customer) {
    is NewRegisteredCustomer -> BillingPlan.basic()
    is ExistingCustomer -> customer.plan
    }
    doSomething(plan)

    「新規のお客様の場合にはデフォルトのプランが適用される」 がコ
    ードから明らか
    余談1: Null Object Pattern
    30 / 37

    View Slide

  31. Null Object Pattern
    null
    を利用せずに明確に型を作成して表現するデザインパターンのこ
    とを
    Null Object Pattern
    と呼ぶ
    特に Kotlin
    の場合には
    nullable
    にする必要がなくなるのでこのパタ
    ーンはコードに安全性をもたらしてくれる
    参考
    Introduce Null Object
    The Null Object Pattern by Bobby Woolf
    余談1: Null Object Pattern
    31 / 37

    View Slide

  32. 余談
    2:
    直和でしか表現できないデータ構造
    32 / 37

    View Slide

  33. data class ContactInfo(
    val emailContactInfo: EmailContactInfo,
    val postalContactInfo: PostalContactInfo
    )
    この状態で 「連絡情報は、
    Email
    もしくは 住所どちらかが設定されて
    いれば良い」 という要求の変更が起こった際にはどうする?
    data class ContactInfo(
    val emailContactInfo: EmailContactInfo?,
    val postalContactInfo: PostalContactInfo?
    )

    どちらも任意となってしまった
    余談2:
    直和でしか表現できないデータ構造
    33 / 37

    View Slide

  34. つまるところ 3
    パターンのみが許容できるということ
    住所
    /
    メールアドレス あり なし
    あり ○ ○
    なし ○ x
    sealed class ContactInfo
    data class EmailOnlyContactInfo(val emailContactInfo: EmailContactInfo): ContactInfo()
    data class PostalOnlyContactInfo(val postalContactInfo: PostalContactInfo): ContactInfo()
    data class EmailAndPostalContactInfo(
    val emailContactInfo: EmailContactInfo,
    val postalContactInfo: PostalContactInfo
    ): ContactInfo()
    -> 3
    つの状態が明確になった上に
    Nullable
    を利用せずに済んでいる
    余談2:
    直和でしか表現できないデータ構造
    34 / 37

    View Slide

  35. 2
    つの例に沿って、以下の具体例を紹介
    1.
    直和 を用いた状態の表現
    2.
    状態遷移図の実装
    例 1:
    メールアドレスの認証状態
    例 2: EC
    サイトにおけるカートの内容
    また余談として以下を紹介
    Null Object Pattern
    直和でしか表現できないデータ構造
    35 / 37

    View Slide

  36. 以上
    一緒に働く人を募集中なので気軽に
    Twitter
    の DM
    などで連絡をください
    @omuomugin
    36 / 37

    View Slide

  37. 参考
    Domain Modeling Made Functional by Scott Wlaschin
    Designing with Types by Scott Wlaschin
    Null Object
    The Null Object Pattern by Bobby Woolf
    Primitive Obsession
    エリック・エヴァンスのドメイン駆動設計
    37 / 37

    View Slide