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

“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 と Domain Modeling Made Functional の内容を

    中心に時間が限られているので以下に限定して発表します 1. 直和 を用いた状態の表現 2. 状態遷移図の実装 特に書籍の中では、DDD の精神から関数型のテクニックなども扱って いるので興味があればぜひ読んでみてください ※ 直和の説明は後ほどするのでお待ちを 3 / 37
  2. 発表の元となる書籍とブログの紹介 Designing with Types というブログシリーズ Domain Modeling Made Functional の著者で

    もある Scott Wlaschin さんが書いたもの ブログシリーズが 2013 年で、本が 2018 年に 出版されており、本の内容の中核はこのブロ グシリーズで話してるものを改変したもの ( 読んだ個人の感想) Scott Wlaschin さんが F# を好んで利用するよ うで、本もブログも F# での説明となっている 4 / 37
  3. 直和と直積 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
  4. 直和と直積 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
  5. 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
  6. まず簡単に考えると 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
  7. そこで直和を利用して型として 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
  8. 状態遷移図の実装 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
  9. 補足 : 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
  10. 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
  11. 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
  12. sealed class Cart object EmptyCart : Cart() data class ActiveCart(val

    unpaidItems: List<Item>) : Cart() data class PaidCart(val paidItems: List<Item>, val payment: Float) : Cart() -> 3 つの状態が明確に実装から読み取れる ※ field を持たないような Sealed Class の子は object で表現せよとい う lint の warning が出る 例2: EC サイトにおけるカート 23 / 37
  13. 状態遷移図の実装 (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
  14. 状態遷移図の実装 (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
  15. まとめ 2 つの例に沿って、以下の具体例を紹介した 1. 直和 を用いた状態の表現 2. 状態遷移図の実装 例 1:

    メールアドレスの認証状態 例 2: EC サイトにおけるカートの内容 26 / 37
  16. if (customer == null) { plan = BillingPlan.basic(); } else

    { plan = customer.plan; } doSomething(plan) 上のコード一見わかりやすいが customer が null の場合のビジネス 要求が正確に理解できない おそらく 「新規のお客様の場合にはデフォルトのプランが適用され る」 とかだと思う 余談1: Null Object Pattern 29 / 37
  17. 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
  18. 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
  19. data class ContactInfo( val emailContactInfo: EmailContactInfo, val postalContactInfo: PostalContactInfo )

    この状態で 「連絡情報は、 Email もしくは 住所どちらかが設定されて いれば良い」 という要求の変更が起こった際にはどうする? data class ContactInfo( val emailContactInfo: EmailContactInfo?, val postalContactInfo: PostalContactInfo? ) → どちらも任意となってしまった 余談2: 直和でしか表現できないデータ構造 33 / 37
  20. つまるところ 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
  21. 2 つの例に沿って、以下の具体例を紹介 1. 直和 を用いた状態の表現 2. 状態遷移図の実装 例 1: メールアドレスの認証状態

    例 2: EC サイトにおけるカートの内容 また余談として以下を紹介 Null Object Pattern 直和でしか表現できないデータ構造 35 / 37
  22. 参考 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