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

ビジネスロジックを「型」で表現するOOPのための関数型DDD / Functional And Type-Safe DDD for OOP

ビジネスロジックを「型」で表現するOOPのための関数型DDD / Functional And Type-Safe DDD for OOP

Object-Oriented Conference 2024で発表した資料です。
https://fortee.jp/oocon-2024/proposal/b31c9818-3cb8-4350-adfe-cbc839cdf829

ビジネスの専門知識(ドメイン)を中心に据えたドメイン駆動設計に代数的データ型などの関数型のパラダイムを加えたよりタイプセーフな関数型DDDを紹介します。

本セッションではドメインモデリングによって発見したモデルやビジネスロジックをソフトウェアに反映する際により型を重視した設計を加えます。
型で表現する範囲が広がることでビジネスロジックをより明確にコードで表現できるようになります。
さらには型で表現されているためコンパイルフェーズで気付けるミスが増え、ソフトウェアの品質向上にもつながります。

関数型の考えをいれるといってもただ単にHaskellなどに代表される関数型言語を使ってドメイン駆動設計を行うことではありません。
関数型の知識を応用したタイプセーフな考え方やテクニックは小さく導入することが可能です。Javaなどの既存のオブジェクト指向の良さをそのままに明日からプロダクトションコードを改善する手法を紹介します。

YuitoSato

March 23, 2024
Tweet

More Decks by YuitoSato

Other Decks in Technology

Transcript

  1. 2 2 ©2024 Loglass Inc. 自己紹介 株式会社ログラス 開発部 エンジニア 佐藤有斗(X:

    @Yuiiitoto) 2020年12月にソフトウェアエンジニアとしてログラスに入社。 KotlinとTypeScriptを使って経営管理クラウドLoglassを開発して いる。 母国語はScala。 KotlinのOSSをちょこちょこ開発・保守しています。
  2. 9 ©2024 Loglass Inc. アジェンダ 1. はじめに a. 関数型DDDとは? b.

    ビジネスロジックを型で表現できると何がいいの? c. オブジェクト指向と関数型 2. DDDってなんだっけ? a. DDDのモデリング b. DDDの実装パターン 3. 実践と3つのテクニック a. モデルの状態を代数的データ型で表現する b. モデルの状態遷移を型と全域関数で表現する c. ロジック内のDBアクセスを高階関数で表現する 4. まとめ
  3. 10 ©2024 Loglass Inc. アジェンダ 1. はじめに a. 関数型DDDとは? b.

    ビジネスロジックを型で表現できると何がいいの? c. オブジェクト指向と関数型 2. DDDってなんだっけ? a. DDDのモデリング b. DDDの実装パターン 3. 実践と3つのテクニック a. モデルの状態を代数的データ型で表現する b. モデルの状態遷移を型と全域関数で表現する c. ロジック内のDBアクセスを高階関数で表現する 4. まとめ
  4. 11 ©2024 Loglass Inc. 関数型DDDとは? - 定義済みの言葉としては存在しないように思える - 原著としてはおそらく Scott

    Wlaschin氏の『Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F#』
  5. 12 ©2024 Loglass Inc. Domain-driven design (DDD) combined with functional

    programming is the innovative combo that will get you there. In this pragmatic, down-to-earth guide, you'll see how applying the core principles of functional programming can result in software designs that model real-world requirements both elegantly and concisely - often more so than an object-oriented approach. —『Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F#』Scott Wlaschin著 訳)ドメイン駆動設計 (DDD)と関数型プログラミングを組み合わせることで、革新的なソフト ウェア設計を実現することができます。この実用的で実践的なガイドでは、 関数型プログラミ ングの中核となる原則を適用することで、実世界の要求をエレガントかつ簡潔にモデル化す るソフトウェア設計が、オブジェクト指向のアプローチよりも実現できることを説明します。 関数型DDDとは?
  6. 14 ©2024 Loglass Inc. ビジネスロジックを型で表現できると何がいいの? - 実装ミスがコンパイルフェーズで検知できるようになる - 例)完了ステータスのタスクのみ完了日時を持つ よくある書き方

    class Task { val title: String val status: TaskStatus val completedAt: LocalDateTime? // 中略 } enum class TaskStatus { InProgress, Completed } val task = Task("買い物", TaskStatus.InProgress, null) val task = Task( "買い物", TaskStatus.Completed, LocalDateTime.now(), ) ⚠以降のコードは基本Kotlinで記述します。 なるべく多くの人がわかるよう Kotlinぽい書き方は意図的に避けています。
  7. 15 ©2024 Loglass Inc. ビジネスロジックを型で表現できると何がいいの? class Task { val title:

    String val status: TaskStatus val completedAt: LocalDateTime? // 中略 } enum class TaskStatus { InProgress, Completed } よくある書き方 val task = Task("買い物", TaskStatus.InProgress, null) val task = Task( "買い物", TaskStatus.Completed, LocalDateTime.now(), ) // NG: 進行中なのに完了時刻持ち val task = Task( "買い物", TaskStatus.InProgress, LocalDateTime.now() ); - 実装ミスがコンパイルフェーズで検知できるようになる - 例)完了ステータスのタスクのみ完了日時を持つ
  8. 16 ©2024 Loglass Inc. ビジネスロジックを型で表現できると何がいいの? constructor(title: String, status: TaskStatus, completedAt:

    LocalDateTime?) { if (status == TaskStatus.InProgress && completedAt != null) { throw IllegalArgumentException("進行中のタスクに完了時刻を持たせることはできません ") } this.title = title this.status = status this.completedAt = completedAt } - 実装ミスがコンパイルフェーズで検知できるようになる - 例)完了ステータスのタスクのみ完了日時を持つ
  9. 17 ©2024 Loglass Inc. ビジネスロジックを型で表現できると何がいいの? - 実装ミスがコンパイルフェーズで検知できるようになる - 例)完了ステータスのタスクのみ完了日時を持つ →constructorで縛りをつける

    class CompletedTask { val title: String val completedAt: LocalDateTime // 中略 } class InProgressTask { val title: String // 中略 } 完了済みタスクというクラスを作る val task = InProgressTask("買い物"); val task = CompletedTask( "買い物", LocalDateTime.now() ); // コンパイルエラー val task = InProgressTask( "買い物", LocalDateTime.now() );
  10. 19 ©2024 Loglass Inc. 実装ミスを見つける3ステップ 型で表現されている 範囲に限り 品質を担保 テストコードが 書かれている範囲

    に限り品質を担保 1. コンパイル 2. 自動テスト 3. 手動テスト 型で表現されている 範囲に限り 品質を担保
  11. 20 ©2024 Loglass Inc. 実装ミスを見つける3ステップ 型で表現されている 範囲に限り 品質を担保 テストコードが 書かれている範囲

    に限り品質を担保 1. コンパイル 2. 自動テスト 3. 手動テスト テストコードが全ケース 網羅できるわけではない。 テストコードもコード保守対象 型で表現されている 範囲に限り 品質を担保
  12. 21 ©2024 Loglass Inc. 実装ミスを見つける3ステップ テストコードが 書かれている範囲 に限り品質を担保 1. コンパイル

    2. 自動テスト 3. 手動テスト テストコードが全ケース 網羅できるわけではない。 テストコードもコード保守対象 ここでできるかぎり 実装ミスを洗い出す 型で表現されている 範囲に限り 品質を担保
  13. 29 ©2024 Loglass Inc. OOP × DDDのプロジェクトに 小さく導入可能 & 強力なものに

    絞って話します。 アーキテクチャレベルで変える話は しないです。 そしてログラスで一部のチームで 実際に使っているテクニックです。 より純粋な関数型DDDに興味があるなら こちらの本を読みましょう そのため今日は、
  14. 30 ©2024 Loglass Inc. アジェンダ 1. はじめに a. 関数型DDDとは? b.

    ビジネスロジックを型で表現できると何がいいの? c. オブジェクト指向と関数型 2. DDDってなんだっけ? a. DDDのモデリング b. DDDの実装パターン 3. 実践と3つのテクニック a. モデルの状態を代数的データ型で表現する b. モデルの状態遷移を型と全域関数で表現する c. ロジック内のDBアクセスを高階関数で表現する 4. まとめ
  15. 37 ©2024 Loglass Inc. アジェンダ 1. はじめに a. 関数型DDDとは? b.

    ビジネスロジックを型で表現できると何がいいの? c. オブジェクト指向と関数型 2. DDDってなんだっけ? a. DDDのモデリング b. DDDの実装パターン 3. 実践と3つのテクニック a. モデルの状態を代数的データ型で表現する b. モデルの状態遷移を型と全域関数で表現する c. ロジック内のDBアクセスを高階関数で表現する 4. まとめ
  16. 47 ©2024 Loglass Inc. アジェンダ 1. はじめに a. 関数型DDDとは? b.

    ビジネスロジックを型で表現できると何がいいの? c. オブジェクト指向と関数型 2. DDDってなんだっけ? a. DDDのモデリング b. DDDの実装パターン 3. 実践と3つのテクニック a. モデルの状態を代数的データ型で表現する b. モデルの状態遷移を型と全域関数で表現する c. ロジック内のDBアクセスを高階関数で表現する 4. まとめ
  17. 48 ©2024 Loglass Inc. 実践と3つのテクニック - 関数型の知識を使った 3つのテクニック - 1.

    モデルの状態を代数的データ型で表現する - 2. モデルの状態遷移を型と全域関数で表現する - 3. ロジック内のDBアクセスを高階関数で表現する
  18. 49 ©2024 Loglass Inc. 実践と3つのテクニック - 関数型の知識を使った 3つのテクニック - 1.

    モデルの状態を代数的データ型で表現する - 2. モデルの状態遷移を型と全域関数で表現する - 3. ロジック内のDBアクセスを高階関数で表現する
  19. 50 ©2024 Loglass Inc. 注文の状態をEnumで実装すると class Order { val orderId:

    OrderId, val customerId: CustomerId, val shippingAddress: Address, val lines: List<OrderLine>, val status: OrderStatus, val confirmedAt: LocalDateTime?, val cancelledAt: LocalDateTime?, val cancelReason: String?, val shippingStartedAt: LocalDateTime?, val shippedBy: ShipperId?, val scheduledArrivalDate: LocalDate?, } enum class OrderStatus { UNCONFIRMED, CONFIRMED, CANCELLED, SHIPPING, }
  20. 51 ©2024 Loglass Inc. class Order { val orderId: OrderId,

    val customerId: CustomerId, val shippingAddress: Address, val lines: List<OrderLine>, val status: OrderStatus, val confirmedAt: LocalDateTime?, val cancelledAt: LocalDateTime?, val cancelReason: String?, val shippingStartedAt: LocalDateTime?, val shippedBy: ShipperId?, val scheduledArrivalDate: LocalDate?, } enum class OrderStatus { UNCONFIRMED, CONFIRMED, CANCELLED, SHIPPING, } nullableなものが多くなる。 状態によっては必須な項目がある。 データ不整合が起きる可能性がある。 注文の状態をEnumで実装すると
  21. 52 ©2024 Loglass Inc. 注文の状態をEnumで実装すると val order = Order( ...,

    status = OrderStatus.SHIPPING, cancelReason = "壊れていた", ) 発送済みなのにキャンセル理由を持ててしまう
  22. 53 ©2024 Loglass Inc. constructor( orderId: OrderId, …, status: OrderStatus,

    cancelledAt: LocalDateTime?, cancelReason: String?, ... ) { if (status != OrderStatus.CANCELLED) { if (cancelledAt != null && cancelReason != null) { throw IllegalArgumentException("キャンセル理由、キャンセル日時は キャンセル済みの場合のみ持てます ") } } this.orderId = orderId … } データ不整合をガードするコードを 書かないといけない 注文の状態をEnumで実装すると
  23. 54 ©2024 Loglass Inc. constructor( orderId: OrderId, …, status: OrderStatus,

    cancelledAt: LocalDateTime?, cancelReason: String?, ... ) { if (status != OrderStatus.CANCELLED) { if (cancelledAt != null && cancelReason != null) { throw IllegalArgumentException("キャンセル理由、キャンセル日時は キャンセル済みの場合のみ持てます ") } } this.orderId = orderId … } ちなみにここ実装ミスしてます 注文の状態をEnumで実装すると
  24. 55 ©2024 Loglass Inc. constructor( orderId: OrderId, …, status: OrderStatus,

    cancelledAt: LocalDateTime?, cancelReason: String?, ... ) { if (status != OrderStatus.CANCELLED) { if (cancelledAt != null || cancelReason != null) { throw IllegalArgumentException("キャンセル理由、キャンセル日時は キャンセル済みの場合のみ持てます ") } } this.orderId = orderId … } 注文の状態をEnumで実装すると
  25. 58 ©2024 Loglass Inc. 完全に別クラスとして定義する class UnconfirmedOrder( val orderId: OrderId,

    val customerId: CustomerId, val shippingAddress: Address, val lines: NonEmptyList<OrderLine>, ) class ConfirmedOrder( val orderId: OrderId, // 中略 val confirmedAt: LocalDateTime, ) class CancelledOrder( val orderId: OrderId, // 中略 val confirmedAt: LocalDateTime, val cancelledAt: LocalDateTime, val cancelReason: String?, ) class ShippingOrder( val orderId: OrderId, // 中略 val confirmedAt: LocalDateTime, val shippingStartedAt: LocalDateTime, val shippedBy: ShipperId, val scheduledArrivalDate: LocalDate, )
  26. 59 ©2024 Loglass Inc. 課題: ポリモーフィズムがなく使いづらい val order: Any =

    orderRepository.findBy(orderId) val orders: List<Any> = listOf( UnconfirmedOrder(...), ConfirmedOrder(...), CancelledOrder(...), ... )
  27. 63 ©2024 Loglass Inc. 直積集合について - A × B =

    { (a,b) ∣ a ∈ A, b ∈ B } (=AかつB) - 要するに構造体(Class) (1, “あ”) (1, “い”) (1, “う”) (2, “あ”) (2, “い”) (2, “う”) (3, “あ”) (3, “い”) (3, “う”) A: 1, 2, 3, B: “あ”, “い”, ‘う“ A ✖ B class CancelledOrder( val orderId: OrderId, // 中略 val cancelledAt: LocalDateTime, val cancelReason: String?, ) CancelledOrder = OrderId × LocalDateTime × String
  28. 64 ©2024 Loglass Inc. 直和集合について - A ⊕ B= A

    ∪ B ただし A ∩ B = {0} (=AまたはBだけどAとBは被らない) - オブジェクト指向言語の一部では sealed節(継承できる範囲を限定)を使うことで実現 A B A ⊕ B sealed interface Order { class UnconfirmedOrder : Order class ConfirmedOrder : Order class CancelledOrder : Order class ShippingOrder : Order } Order = Unconfirmed ⊕ Confirmed ⊕ Cancelled ⊕ Shipping
  29. 65 ©2024 Loglass Inc. 直和集合について - A ⊕ B= A

    ∪ B ただし A ∩ B = {0} (=AまたはBだけどAとBは被らない) - TypeScriptとかの方が直感的かもしれない type Order = UnconfirmedOrder | ConfirmedOrder | CancelledOrder | ShippingOrder; class UnconfirmedOrder {} class ConfirmedOrder {} class CancelledOrder {} class ShippingOrder {}
  30. 66 ©2024 Loglass Inc. 代数的データ型 = 直積集合と直和集合の掛け合わせ Unconfirmed = (...)

    Confirmed = (...) Cancelled = (...) Order = Unconfirmed(...) ⊕ Confirmed(...) ⊕ Cancelled(...) ⊕ Shipping(...) Shipping = (...)
  31. 67 ©2024 Loglass Inc. 代数的データ型 = 直積集合と直和集合の掛け合わせ Unconfirmed = (...)

    Confirmed = (...) Cancelled = ( OrderId × … × LocalDateTime × String ) Order = Unconfirmed(...) ⊕ Confirmed(...) ⊕ Cancelled(...) ⊕ Shipping(...) Shipping = (...)
  32. 69 ©2024 Loglass Inc. 継承とは何が違うの? A B C = int

    ✖ String - 継承は範囲を閉じることができない(親クラスが子クラスを知らない) - 直和の性質がない D = boolean ✖ boolean Z 知らないところで 継承してるかも?
  33. 70 ©2024 Loglass Inc. Enumと何が違うの? A B C = int

    ✖ String - Enumは個別の構造体が持てない - 直積の性質がない Z
  34. 71 ©2024 Loglass Inc. 代数的データ型を使って Orderを再実装 sealed interface Order {

    val orderId: OrderId val customerId: CustomerId val shippingAddress: Address val lines: List<OrderLine> class UnconfirmedOrder( override val …, ) : Order class ConfirmedOrder( …, val confirmedAt: LocalDateTime, ) : Order class CancelledOrder( …, val confirmedAt: LocalDateTime, val cancelledAt: LocalDateTime, val cancelReason: String?, ) : Order class ShippingOrder( …, val confirmedAt: LocalDateTime, val shippingStartedAt: LocalDateTime, val shippedBy: ShipperId, val scheduledArrivalDate: LocalDate, ) : Order }
  35. 72 ©2024 Loglass Inc. 代数的データ型を使って Orderを再実装 sealed interface Order {

    val orderId: OrderId val customerId: CustomerId val shippingAddress: Address val lines: List<OrderLine> class UnconfirmedOrder( override val …, ) : Order class ConfirmedOrder( …, val confirmedAt: LocalDateTime, ) : Order class CancelledOrder( …, val confirmedAt: LocalDateTime, val cancelledAt: LocalDateTime, val cancelReason: String?, ) : Order class ShippingOrder( …, val confirmedAt: LocalDateTime, val shippingStartedAt: LocalDateTime, val shippedBy: ShipperId, val scheduledArrivalDate: LocalDate, ) : Order } データ不整合がなくなった
  36. 73 ©2024 Loglass Inc. val order = ShippingOrder( ..., cancelReason

    = "壊れていた", ) => コンパイルエラー 代数的データ型を使って Orderを再実装 class CancelledOrder( …, val confirmedAt: LocalDateTime, val cancelledAt: LocalDateTime, val cancelReason: String?, ) : Order class ShippingOrder( …, val confirmedAt: LocalDateTime, val shippingStartedAt: LocalDateTime, val shippedBy: ShipperId, val scheduledArrivalDate: LocalDate, ) : Order } ShippingOrderは cancelReasonを持っていないので コンパイルエラー
  37. 74 ©2024 Loglass Inc. - 網羅性も担保される fun cancel(cancelReason: String?, now:

    LocalDateTime): CancelledOrder { return when (this) { is UnconfirmedOrder -> throw Exception("未確定の注文はキャンセルできません ") is ConfirmedOrder -> CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) is CancelledOrder -> throw Exception("キャンセル済みの注文はキャンセルできません ") is ShippingOrder -> throw Exception("発送済みの注文はキャンセルできません ") } } 代数的データ型を使って Orderを再実装
  38. 75 ©2024 Loglass Inc. - 網羅性も担保される fun cancel(cancelReason: String?, now:

    LocalDateTime): CancelledOrder { return when (this) { is UnconfirmedOrder -> throw Exception("未確定の注文はキャンセルできません ") is ConfirmedOrder -> CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) is CancelledOrder -> throw Exception("キャンセル済みの注文はキャンセルできません ") is ShippingOrder -> throw Exception("発送済みの注文はキャンセルできません ") } } 代数的データ型を使って Orderを再実装 else説がいらない
  39. 76 ©2024 Loglass Inc. - 網羅性も担保される fun cancel(cancelReason: String?, now:

    LocalDateTime): CancelledOrder { return when (this) { is UnconfirmedOrder -> throw Exception("未確定の注文はキャンセルできません ") is ConfirmedOrder -> CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) is CancelledOrder -> throw Exception("キャンセル済みの注文はキャンセルできません ") // is ShippingOrder -> throw Exception("発送済みの注文はキャンセルできません ") } } 代数的データ型を使って Orderを再実装 分岐が足りなければコンパイルエラー
  40. 78 ©2024 Loglass Inc. 実践と3つのテクニック - 関数型の知識を使った 3つのテクニック - 1.

    モデルの状態を代数的データ型で表現する - 2. モデルの状態遷移を型と全域関数で表現する - 3. ロジック内のDBアクセスを高階関数で表現する
  41. 81 ©2024 Loglass Inc. 普通に書くと sealed interface Order { fun

    cancel(cancelReason: String?, now: LocalDateTime): CancelledOrder { return when (this) { is UnconfirmedOrder -> throw Exception("未確定の注文はキャンセルできません ") is ConfirmedOrder -> CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) is CancelledOrder -> throw Exception("キャンセル済みの注文はキャンセルできません ") is ShippingOrder -> throw Exception("発送済みの注文はキャンセルできません ") } } }
  42. 82 ©2024 Loglass Inc. sealed interface Order { fun cancel(cancelReason:

    String?, now: LocalDateTime): CancelledOrder { return when (this) { is UnconfirmedOrder -> throw Exception("未確定の注文はキャンセルできません ") is ConfirmedOrder -> CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) is CancelledOrder -> throw Exception("キャンセル済みの注文はキャンセルできません ") is ShippingOrder -> throw Exception("発送済みの注文はキャンセルできません ") } } } 4ケースのテストが必要 普通に書くと
  43. 83 ©2024 Loglass Inc. 普通に書くと sealed interface Order { fun

    cancel(cancelReason: String?, now: LocalDateTime): CancelledOrder { return when (this) { is UnconfirmedOrder -> throw Exception("未確定の注文はキャンセルできません ") is ConfirmedOrder -> CancelledOrder(...) is CancelledOrder -> throw Exception("キャンセル済みの注文はキャンセルできません ") is ShippingOrder -> CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) } } } 実装ミスしてしまう
  44. 84 ©2024 Loglass Inc. どうするか? - そもそも ConfirmedOrderのメソッドとして定義すればよい class ConfirmedOrder(...)

    : Order { fun cancel(cancelReason: String?, now: LocalDateTime): CancelledOrder { return CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) } }
  45. 85 ©2024 Loglass Inc. Orderクラス全体 sealed interface Order { ...

    class UnconfirmedOrder(...) : Order { fun confirm(now: LocalDateTime): ConfirmedOrder } class ConfirmedOrder(...) : Order { fun cancel(cancelReason: String?, now: LocalDateTime): CancelledOrder {...} fun startShipping(shipperId: ShipperId, now: LocalDateTime): ShippingOrder {...} } class CancelledOrder(...) : Order class ShippingOrder(...) : Order }
  46. 86 ©2024 Loglass Inc. 呼び出し元 class CancelOrderUseCase( private val orderRepository:

    OrderRepository, ) { fun execute(orderId: OrderId, cancelReason: String?) { val order = orderRepository.findById(orderId) ?: throw Exception("注文が見つかりませんでした。ID: ${orderId.value}") val now = LocalDateTime.now() when (order) { is UnconfirmedOrder -> throw Exception("未確定の注文はキャンセルできません") is ConfirmedOrder -> order.cancel(cancelReason, now) is CancelledOrder -> throw Exception("キャンセル済みの注文はキャンセルできません") is ShippingOrder -> throw Exception("発送済みの注文はキャンセルできません") } } }
  47. 87 ©2024 Loglass Inc. 呼び出し元 class CancelOrderUseCase( private val orderRepository:

    OrderRepository, ) { fun execute(orderId: OrderId, cancelReason: String?) { val order = orderRepository.findById(orderId) ?: throw Exception("注文が見つかりませんでした。ID: ${orderId.value}") val now = LocalDateTime.now() when (order) { is UnconfirmedOrder -> throw Exception("未確定の注文はキャンセルできません") is ConfirmedOrder -> order.cancel(cancelReason, now) is CancelledOrder -> throw Exception("キャンセル済みの注文はキャンセルできません") is ShippingOrder -> order.cancel(cancelReason, now) } } } ShippingOrderにはcancelメソッドがないので コンパイルエラー
  48. 88 ©2024 Loglass Inc. 呼び出し元 class CancelOrderUseCase( private val orderRepository:

    OrderRepository, ) { fun execute(orderId: OrderId, cancelReason: String?) { val order = orderRepository.findById(orderId) ?: throw Exception("注文が見つかりませんでした。ID: ${orderId.value}") val now = LocalDateTime.now() when (order) { is UnconfirmedOrder -> throw Exception("未確定の注文はキャンセルできません") is ConfirmedOrder -> order.cancel(cancelReason, now) is CancelledOrder -> throw Exception("キャンセル済みの注文はキャンセルできません") is ShippingOrder -> throw Exception("発送済みの注文はキャンセルできません") } } } 結局呼び出し元に押し付けただけでは? 🤔 これ意味あるの?
  49. 90 ©2024 Loglass Inc. ルールとハンドリングを分けて考える - 未確定の場合は「未確定の注文はキャンセルできません」と返す。 - 確定済みの場合のみ注文をキャンセルする。 -

    キャンセル済みの場合は「すでにキャンセル済みです」と返す - 発送済みの場合は「発送済みの注文はキャンセルできません」と返す
  50. 91 ©2024 Loglass Inc. ルールとハンドリングを分けて考える 確定済みの場合のみ注文をキャンセルできる - 未確定の場合は「未確定の注文はキャンセルできません」と返す。 - 確定済みの場合のみ注文をキャンセルする。

    - キャンセル済みの場合は「すでにキャンセル済みです」と返す - 発送済みの場合は「発送済みの注文はキャンセルできません」と返す ルールのみを抽出 ルールのみを抽出 ルール ハンドリング class ConfirmedOrder(...) : Order { fun cancel( cancelReason: String?, now: LocalDateTime ): CancelledOrder {...} } when (order) { is UnconfirmedOrder -> throw Exception("未確定の注文は ~") is ConfirmedOrder -> order.cancel(cancelReason, now) is CancelledOrder -> throw Exception("キャンセル済みの注文は ~") is ShippingOrder -> throw Exception("発送済みの注文は ~") }
  51. 93 ©2024 Loglass Inc. ルールとハンドリングを分けて考える ルールの適用 ハンドリング class ConfirmedOrder(...) :

    Order { fun cancel( cancelReason: String?, now: LocalDateTime ): CancelledOrder {...} } when (order) { is UnconfirmedOrder -> throw Exception("未確定の注文は ~") is ConfirmedOrder -> order.cancel(cancelReason, now) is CancelledOrder -> throw Exception("キャンセル済みの注文は ~") is ShippingOrder -> throw Exception("発送済みの注文は ~") }
  52. 94 ©2024 Loglass Inc. ルールとハンドリングを分けて考える ルールの適用 ハンドリング class ConfirmedOrder(...) :

    Order { fun cancel( cancelReason: String?, now: LocalDateTime ): CancelledOrder {...} } when (order) { is UnconfirmedOrder -> throw Exception("未確定の注文は ~") is ConfirmedOrder -> order.cancel(cancelReason, now) is CancelledOrder -> throw Exception("キャンセル済みの注文は ~") is ShippingOrder -> throw Exception("発送済みの注文は ~") } ルール違反を どう伝えるか?
  53. 99 ©2024 Loglass Inc. 関数の全域性 / 全域関数(Totality / Total Functions)とは?

    A mathematical function links each possible input to an output. In functional programming we try to design our functions the same way, so that every input has a corresponding output. These kinds of functions are called total functions. —『Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F#』Scott Wlaschin著 訳)数学の関数は、可能性のある各入力を出力に結びつけます。関数型プログラミングで は、すべての入力が対応する出力を持つ ように、同じように関数を設計しようとします。この ような関数は全域関数と呼ばれます。
  54. 100 ©2024 Loglass Inc. X × 2 全域関数の例: 掛け算 -

    ある整数を受け取って 2倍にする関数→全域関数 1 2 3 6 3 6 12 入力 出力 0 0 整数 整数
  55. 103 ©2024 Loglass Inc. 全域関数ではない関数の例 : 割り算 - ある0ではない整数を受け取ってその整数で 12を割る関数→全域関数

    - 0ではない整数という型を作る class NonZeroInt(val value: Int) { init { if (value == 0) { throw Exception("NonZeroInt は 0 以外の値を持つ必要があります ") } } } fun divide12By(denominator: NonZeroInt): Int { return 12 / denominator.value } val result = divide12By(0) => コンパイルエラー val result = divide12By(NonZeroInt(3)) => 4
  56. 108 ©2024 Loglass Inc. 注文のキャンセルに話を戻す - 全域関数ではなかった関数を sealed interface Order

    { fun cancel(cancelReason: String?, now: LocalDateTime): CancelledOrder { return when (this) { is UnconfirmedOrder -> throw Exception("未確定の注文はキャンセルできません") is ConfirmedOrder -> CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) is CancelledOrder -> throw Exception("キャンセル済みの注文はキャンセルできません") is ShippingOrder -> throw Exception("発送済みの注文はキャンセルできません") } } }
  57. 109 ©2024 Loglass Inc. 注文のキャンセルに話を戻す - 全域関数ではなかった関数を全域関数にしていたということでした class ConfirmedOrder(...) :

    Order { fun cancel(cancelReason: String?, now: LocalDateTime): CancelledOrder { return CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) } }
  58. 111 ©2024 Loglass Inc. 実践と3つのテクニック - 関数型の知識を使った 3つのテクニック - 1.

    モデルの状態を代数的データ型で表現する - 2. モデルの状態遷移を型と全域関数で表現する - 3. ロジック内のDBアクセスを高階関数で表現する
  59. 112 ©2024 Loglass Inc. ロジック内のDBアクセスを型で表現する - DBアクセスの理想 - ①最初に全て読み込み、②それらを使って処理を行い、③最後に書き込む 外部システム

    (DB、他システムな ど) アプリケーション ドメインモデル (ビジネスロジック) ①読み込み ③書き込み ②呼び出し
  60. 113 ©2024 Loglass Inc. ロジック内のDBアクセスを型で表現する - DBアクセスの理想 - ①最初に全て読み込み、②それらを使って処理を行い、③最後に書き込む -

    依存を減らしドメインモデルが簡潔になる。ユニットテストが書きやすくなる。 外部システム (DB、他システムな ど) アプリケーション ドメインモデル (ビジネスロジック) ①読み込み ③書き込み ②呼び出し
  61. 114 ©2024 Loglass Inc. ロジック内のDBアクセスを型で表現する - DBアクセスの理想 - ①最初に全て読み込み、②それらを使って処理を行い、③最後に書き込む -

    現実そう上手くいかない。複数回読み込みたい時も 外部システム (DB、他システムな ど) アプリケーション ドメインモデル (ビジネスロジック) 読み込み 書き込み 呼び出し 読み込み 呼び出し
  62. 117 ©2024 Loglass Inc. どうするか? class UnconfirmedOrder(...) { companion object

    { // Kotlinのstatic method記法 fun create( customerId: CustomerId, // ユーザー入力 shippingAddress: CreateAddressParam, // ユーザー入力 lines: List<CreateOrderLineParam>, // ユーザー入力 products: List<Product>, ): UnconfirmedOrder { ... } } } - 注文明細で参照している商品を作成時に渡す?
  63. 118 ©2024 Loglass Inc. どうするか? class UnconfirmedOrder(...) { companion object

    { // Kotlinのstatic method記法 fun create( customerId: CustomerId, // ユーザー入力 shippingAddress: CreateAddressParam, // ユーザー入力 lines: List<CreateOrderLineParam>, // ユーザー入力 products: List<Product>, ): UnconfirmedOrder { ... } } } - 注文明細で参照している商品を作成時に渡す? システム内の全ての商品を渡す? このproducts引数に今回欲しい商品が 全て渡されている保証はどうするか?
  64. 119 ©2024 Loglass Inc. どうするか? class UnconfirmedOrder( private val productRepository:

    ProductRepository // DI ) { companion object { // Kotlinのstatic method記法 fun create( customerId: CustomerId, // ユーザー入力 shippingAddress: CreateAddressParam, // ユーザー入力 lines: List<CreateOrderLineParam>, // ユーザー入力 ): UnconfirmedOrder { val products = productRepository.listBy(...) … } } - ProductRepositoryをDIする?
  65. 120 ©2024 Loglass Inc. どうするか? class UnconfirmedOrder( private val productRepository:

    ProductRepository // DI ) { companion object { // Kotlinのstatic method記法 fun create( customerId: CustomerId, // ユーザー入力 shippingAddress: CreateAddressParam, // ユーザー入力 lines: List<CreateOrderLineParam>, // ユーザー入力 ): UnconfirmedOrder { val products = productRepository.listBy(...) … } } - ProductRepositoryをDIする? ProductRepositoryの全てに依存してしま ユニットテストが書きづらくなる。
  66. 121 ©2024 Loglass Inc. どうするか? class UnconfirmedOrder(...) { companion object

    { // Kotlinのstatic method記法 fun create( customerId: CustomerId, // ユーザー入力 shippingAddress: CreateAddressParam, // ユーザー入力 lines: List<CreateOrderLineParam>, // ユーザー入力 getProduct: (ProductId) -> Product?, ): UnconfirmedOrder { … } } } - 商品を取得するロジックを関数で注入してしまう(高階関数) 依存を最小限にしつつ、注文作成時に 欲しい商品を確実時に取得できる
  67. 122 ©2024 Loglass Inc. どうするか? class CreateOrderUseCase( private val productRepository:

    ProductRepository, private val orderRepository: OrderRepository, ) { fun execute(param: CreateOrderParam) { val order = Order.UnconfirmedOrder .create( param.customerId, param.shippingAddress, param.lines, { productId -> productRepository.findById(productId) }, ) orderRepository.insert(order) } } - 呼び出し元 class UnconfirmedOrder(...) { companion object { fun create( customerId: CustomerId, shippingAddress: CreateAddressParam, lines: List<CreateOrderLineParam>, getProduct: (ProductId) -> Product?, ): UnconfirmedOrder { … } } }
  68. 123 ©2024 Loglass Inc. どうするか? class CreateOrderUseCase( private val productRepository:

    ProductRepository, private val orderRepository: OrderRepository, ) { fun execute(param: CreateOrderParam) { val order = Order.UnconfirmedOrder .create( param.customerId, param.shippingAddress, param.lines, { productId -> productRepository.findById(productId) }, ) orderRepository.insert(order) } } - 呼び出し元 注文に対して複数の商品が必要なので N+1問題発生するのでは?
  69. 124 ©2024 Loglass Inc. どうするか? class CreateOrderUseCase( private val productRepository:

    ProductRepository, private val orderRepository: OrderRepository, ) { fun execute(param: CreateOrderParam) { val targetProductIds = param.lines.map { line -> line.productId } val targretProducts = productRepository.listByIds(targetProductIds) val order = Order.UnconfirmedOrder .create( …, { productId -> targetProducts.find { product -> product.id == productId } } ) orderRepository.insert(order) } } - 呼び方を工夫すればよい - getProduct内の処理を どう注入するかは自由
  70. 125 ©2024 Loglass Inc. どうするか? class CreateOrderUseCase( private val productRepository:

    ProductRepository, private val orderRepository: OrderRepository, ) { fun execute(param: CreateOrderParam) { val targetProductIds = param.lines.map { line -> line.productId } val targretProducts = productRepository.listByIds(targetProductIds) val order = Order.UnconfirmedOrder .create( …, { productId -> targetProducts.find { product -> product.id == productId } } ) orderRepository.insert(order) } } - 呼び方を工夫すればよい - getProduct内の処理を どう注入するかは自由 ①: 今回の注文で参照している全ての 商品IDのリストを取得
  71. 126 ©2024 Loglass Inc. どうするか? class CreateOrderUseCase( private val productRepository:

    ProductRepository, private val orderRepository: OrderRepository, ) { fun execute(param: CreateOrderParam) { val targetProductIds = param.lines.map { line -> line.productId } val targretProducts = productRepository.listByIds(targetProductIds) val order = Order.UnconfirmedOrder .create( …, { productId -> targetProducts.find { product -> product.id == productId } } ) orderRepository.insert(order) } } - 呼び方を工夫すればよい - getProduct内の処理を どう注入するかは自由 ②: 上で取得した商品IDで 今回使いたい商品だけを先に取得
  72. 127 ©2024 Loglass Inc. どうするか? class CreateOrderUseCase( private val productRepository:

    ProductRepository, private val orderRepository: OrderRepository, ) { fun execute(param: CreateOrderParam) { val targetProductIds = param.lines.map { line -> line.productId } val targretProducts = productRepository.listByIds(targetProductIds) val order = Order.UnconfirmedOrder .create( …, { productId -> targetProducts.find { product -> product.id == productId } } ) orderRepository.insert(order) } } - 呼び方を工夫すればよい - getProduct内の処理を どう注入するかは自由 ③: ロジック中に欲しい商品は 先読みした商品リストから取得。 ここでDBアクセスは発生しない
  73. 128 ©2024 Loglass Inc. もちろん先読みできない場合もある - その時は最初に余分に読み込み or 都度DBアクセスになる。 -

    どちらを選択したとしてもドメインモデルのメソッドとしてインターフェースが 崩れないのがメリット class UnconfirmedOrder(...) { companion object { fun create( customerId: CustomerId, shippingAddress: CreateAddressParam, lines: List<CreateOrderLineParam>, getProduct: (ProductId) -> Product?, ): UnconfirmedOrder { … } } }
  74. 129 ©2024 Loglass Inc. 悪用禁止 - 利用はあくまで外部システム、DBの責務でドメイン層に対して隠蔽したいことに制限する - 商品IDで商品をDB内から取得したい -

    getProductId: (ProductId) -> Product? - 存在する住所なのか 外部サービスに確認したい - checkAddressExists: (Address) -> Boolean - 商品作成時に商品コードが DB内で重複しているか確認したい - checkProductCodeDuplicated: (ProductCode) -> Boolean
  75. 130 ©2024 Loglass Inc. 悪用禁止 - 利用はあくまで外部システム、 DBの責務でドメイン層に対して隠蔽したいことに制限す る -

    ドメイン層の責務を 関数で注入しないこと class UnconfirmedOrder(...) { companion object { fun create( customerId: CustomerId, shippingAddress: CreateAddressParam, lines: List<CreateOrderLineParam>, calculatePrice: (ProductId, OrderQuantity) -> Price, ): UnconfirmedOrder { … } } } NG
  76. 138 ©2024 Loglass Inc. 本日話せなかったこと(気になることがあれば質問してください 🙋) - Result、Eitherなどの異常値を型で表現する方法 - 便利だが、標準でサポートしていない言語がほとんどで、多くのプロジェクトでは言語選定から見直す必要があるの

    で本日は割愛。 - Kotlinは標準サポートしていないが良いライブラリがあるのでログラスは部分的に導入しています。 - 引数でとる関数が非同期処理だったらどうすんねん問題 - 先に結果を読み込めるならキャッシュして同期処理にして渡すけど、それが難しいなら関数で渡すのはやめるかも。 - 代数的データ型の永続化方法 - RDBMSなどのスキーマが決まっているタイプはENUMで表現して他テーブルやnullableな カラムを駆使して表現してます。DBのモデルとドメインモデルの変換はインフラ層で行います。 - “Domain Modeling Made Functional” のワークフローなどの関数型DDD特有の概念 - アーキテクチャレベルで変更する必要があるので本日は割愛。ログラスでもやっていません。気になる方はぜひ。
  77. 140