Slide 1

Slide 1 text

1 ©2024 Loglass Inc. ビジネスロジックを「型」で表現するOOPのため の関数型DDD Object-Oriented Conference 2024 2024.3.24 佐藤有斗(@Yuiiitoto) 株式会社ログラス

Slide 2

Slide 2 text

2 2 ©2024 Loglass Inc. 自己紹介 株式会社ログラス 開発部 エンジニア 佐藤有斗(X: @Yuiiitoto) 2020年12月にソフトウェアエンジニアとしてログラスに入社。 KotlinとTypeScriptを使って経営管理クラウドLoglassを開発して いる。 母国語はScala。 KotlinのOSSをちょこちょこ開発・保守しています。

Slide 3

Slide 3 text

3 ©2024 Loglass Inc. 自己紹介

Slide 4

Slide 4 text

4 ©2024 Loglass Inc. 型と自動テストの力を探求する者です

Slide 5

Slide 5 text

5 5 ©2024 Loglass Inc. ログラスについて 企業価値を向上する
 経営管理クラウド


Slide 6

Slide 6 text

6 ©2024 Loglass Inc.

Slide 7

Slide 7 text

7 ©2024 Loglass Inc. 今日のテーマ ビジネスロジックを「型」で表現する OOPのための関数型DDD

Slide 8

Slide 8 text

8 ©2024 Loglass Inc. 一番お伝えしたいこと 関数型のエッセンスを適度に取り入れ、 DDDをよりタイプセーフにする。 そしてソフトウェアの品質を上げる。

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

11 ©2024 Loglass Inc. 関数型DDDとは? - 定義済みの言葉としては存在しないように思える - 原著としてはおそらく Scott Wlaschin氏の『Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F#』

Slide 12

Slide 12 text

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とは?

Slide 13

Slide 13 text

13 ©2024 Loglass Inc. 関数型DDDとは? 個人的意訳 関数型のエッセンスを加えて ビジネスロジックを 型の力で柔軟に表現したDDD

Slide 14

Slide 14 text

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ぽい書き方は意図的に避けています。

Slide 15

Slide 15 text

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() ); - 実装ミスがコンパイルフェーズで検知できるようになる - 例)完了ステータスのタスクのみ完了日時を持つ

Slide 16

Slide 16 text

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 } - 実装ミスがコンパイルフェーズで検知できるようになる - 例)完了ステータスのタスクのみ完了日時を持つ

Slide 17

Slide 17 text

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() );

Slide 18

Slide 18 text

18 ©2024 Loglass Inc. 実装ミスを見つける3ステップ 1. コンパイル 2. 自動テスト 3. 手動テスト

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

22 ©2024 Loglass Inc. ここまで 型の力が強化されたDDDで ビジネスロジックを型で表現して 実装ミスをコンパイルフェーズで弾く!

Slide 23

Slide 23 text

23 ©2024 Loglass Inc. 関数型? ウチはオブジェクト指向の言語なんですが? 🤔🤔🤔 (ログラスもソウダヨ)

Slide 24

Slide 24 text

24 ©2024 Loglass Inc. OOP × DDDを行っている 既存のプロジェクトに関数型エッセンスを 追加することは十分可能

Slide 25

Slide 25 text

25 ©2024 Loglass Inc. そもそも関数型のエッセンスを取り入れ始めているプログラミング言語たち - ついに代数的データ型をサポートした Java https://openjdk.org/jeps/440 https://openjdk.org/jeps/441

Slide 26

Slide 26 text

26 ©2024 Loglass Inc. そもそも関数型のエッセンスを取り入れ始めているプログラミング言語たち - オブジェクト指向か関数型か一つのカテゴリに絞れない Rust 『The Rust Programming Language 日本語版』より引用

Slide 27

Slide 27 text

27 ©2024 Loglass Inc. オブジェクト指向か?関数型か?は 近年では曖昧になりつつある。 多くの言語で関数型のテクニックを 効果的に扱うことが可能になっている。

Slide 28

Slide 28 text

28 ©2024 Loglass Inc. オブジェクト指向か?関数型か? という観点より 良いプロダクトになるものなら 積極的に取り入れていきたい

Slide 29

Slide 29 text

29 ©2024 Loglass Inc. OOP × DDDのプロジェクトに 小さく導入可能 & 強力なものに 絞って話します。 アーキテクチャレベルで変える話は しないです。 そしてログラスで一部のチームで 実際に使っているテクニックです。 より純粋な関数型DDDに興味があるなら こちらの本を読みましょう そのため今日は、

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

31 ©2024 Loglass Inc. 2. DDDってなんだっけ?

Slide 32

Slide 32 text

32 ©2024 Loglass Inc. DDD (Domain-Driven Design) とは モデリングによってソフトウェアの価値を 高める開発手法

Slide 33

Slide 33 text

33 ©2024 Loglass Inc. DDD (Domain-Driven Design) とは モデリング 実装パターン

Slide 34

Slide 34 text

34 ©2024 Loglass Inc. DDDのモデリング モデリング - ドメインエキスパートと会話してドメインモデルを考える ドメインエキスパート

Slide 35

Slide 35 text

35 ©2024 Loglass Inc. DDDの実装パターン 実装パターン - DBや外部サービスに依存しないモデルをそのまま表現するための 独立したレイヤー(=ドメイン層)を作る 『新卒にも伝わるドメイン駆動設計のアーキテクチャ説明 (オニオンアーキテクチャ )[DDD]』より引用

Slide 36

Slide 36 text

36 ©2024 Loglass Inc. 3. 実践とテクニック

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

38 ©2024 Loglass Inc. 今回のお題 - 発注システム 注文を作成する 注文を確定する 発送を開始する 注文をキャンセルする

Slide 39

Slide 39 text

39 ©2024 Loglass Inc. 発注システムのユースケース図 ※詳細はこちら『 ドメイン駆動設 計 サンプルコード &FAQ』

Slide 40

Slide 40 text

40 ©2024 Loglass Inc. 発注システムのオブジェクト図

Slide 41

Slide 41 text

41 ©2024 Loglass Inc. 発注システムのドメインモデル図(≒オブジェクト図を抽象化したもの)

Slide 42

Slide 42 text

42 ©2024 Loglass Inc. ドメインモデル図にルールを加えていく

Slide 43

Slide 43 text

43 ©2024 Loglass Inc. nullableがたくさんあってややこしい

Slide 44

Slide 44 text

44 ©2024 Loglass Inc. 状態ごとの項目と遷移を整理する

Slide 45

Slide 45 text

45 ©2024 Loglass Inc. 状態ごとの項目と遷移を整理する

Slide 46

Slide 46 text

46 ©2024 Loglass Inc. これらのルールの実装ミスを コンパイルで防げたら 嬉しくないですか?

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

50 ©2024 Loglass Inc. 注文の状態をEnumで実装すると class Order { val orderId: OrderId, val customerId: CustomerId, val shippingAddress: Address, val lines: List, 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, }

Slide 51

Slide 51 text

51 ©2024 Loglass Inc. class Order { val orderId: OrderId, val customerId: CustomerId, val shippingAddress: Address, val lines: List, 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で実装すると

Slide 52

Slide 52 text

52 ©2024 Loglass Inc. 注文の状態をEnumで実装すると val order = Order( ..., status = OrderStatus.SHIPPING, cancelReason = "壊れていた", ) 発送済みなのにキャンセル理由を持ててしまう

Slide 53

Slide 53 text

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で実装すると

Slide 54

Slide 54 text

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で実装すると

Slide 55

Slide 55 text

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で実装すると

Slide 56

Slide 56 text

56 ©2024 Loglass Inc. 実行時にしかミスに気付けない。 深刻なデータ不整合が起きる可能性がある。 発送済み商品をキャンセルできてしまって 返金処理完了したのに商品が届いたなど

Slide 57

Slide 57 text

57 ©2024 Loglass Inc. ではどうするか? - 状態ごとにモデルを分ければよい

Slide 58

Slide 58 text

58 ©2024 Loglass Inc. 完全に別クラスとして定義する class UnconfirmedOrder( val orderId: OrderId, val customerId: CustomerId, val shippingAddress: Address, val lines: NonEmptyList, ) 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, )

Slide 59

Slide 59 text

59 ©2024 Loglass Inc. 課題: ポリモーフィズムがなく使いづらい val order: Any = orderRepository.findBy(orderId) val orders: List = listOf( UnconfirmedOrder(...), ConfirmedOrder(...), CancelledOrder(...), ... )

Slide 60

Slide 60 text

60 ©2024 Loglass Inc. ここまでをまとめると - 注文の状態ごとに必須項目が違う - 未確定、確定済み、キャンセル済み、発送済みをまとめて注文と呼びたい

Slide 61

Slide 61 text

61 ©2024 Loglass Inc. ここまでをまとめると - 注文の状態ごとに必須項目が違う - 未確定、確定済み、キャンセル済み、発送済みをまとめて注文と呼びたい →代数的データ型を使おう

Slide 62

Slide 62 text

62 ©2024 Loglass Inc. 代数的データ型(ADT, Algebraic Data Type)とは? - 代数学に由来するデータ構造 - 直積集合と直和集合で表されるデータ構造

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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 {}

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

68 ©2024 Loglass Inc. 代数的データ型 = 直積集合と直和集合の掛け合わせ

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

70 ©2024 Loglass Inc. Enumと何が違うの? A B C = int ✖ String - Enumは個別の構造体が持てない - 直積の性質がない Z

Slide 71

Slide 71 text

71 ©2024 Loglass Inc. 代数的データ型を使って Orderを再実装 sealed interface Order { val orderId: OrderId val customerId: CustomerId val shippingAddress: Address val lines: List 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 }

Slide 72

Slide 72 text

72 ©2024 Loglass Inc. 代数的データ型を使って Orderを再実装 sealed interface Order { val orderId: OrderId val customerId: CustomerId val shippingAddress: Address val lines: List 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 } データ不整合がなくなった

Slide 73

Slide 73 text

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を持っていないので コンパイルエラー

Slide 74

Slide 74 text

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を再実装

Slide 75

Slide 75 text

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説がいらない

Slide 76

Slide 76 text

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を再実装 分岐が足りなければコンパイルエラー

Slide 77

Slide 77 text

77 ©2024 Loglass Inc. 代数的データ型でモデルの状態を表現し、 データ不整合を防ぐことができた!

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

79 ©2024 Loglass Inc. 状態遷移を型で表現したい

Slide 80

Slide 80 text

80 ©2024 Loglass Inc. 状態遷移を型で表現したい

Slide 81

Slide 81 text

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("発送済みの注文はキャンセルできません ") } } }

Slide 82

Slide 82 text

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ケースのテストが必要 普通に書くと

Slide 83

Slide 83 text

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, ) } } } 実装ミスしてしまう

Slide 84

Slide 84 text

84 ©2024 Loglass Inc. どうするか? - そもそも ConfirmedOrderのメソッドとして定義すればよい class ConfirmedOrder(...) : Order { fun cancel(cancelReason: String?, now: LocalDateTime): CancelledOrder { return CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) } }

Slide 85

Slide 85 text

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 }

Slide 86

Slide 86 text

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("発送済みの注文はキャンセルできません") } } }

Slide 87

Slide 87 text

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メソッドがないので コンパイルエラー

Slide 88

Slide 88 text

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("発送済みの注文はキャンセルできません") } } } 結局呼び出し元に押し付けただけでは? 🤔 これ意味あるの?

Slide 89

Slide 89 text

89 ©2024 Loglass Inc. ルールを型で表現できた という観点で意味があります

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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("発送済みの注文は ~") }

Slide 92

Slide 92 text

92 ©2024 Loglass Inc. ルールとハンドリングを分けて考える ルールの適用 ハンドリング

Slide 93

Slide 93 text

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("発送済みの注文は ~") }

Slide 94

Slide 94 text

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("発送済みの注文は ~") } ルール違反を どう伝えるか?

Slide 95

Slide 95 text

95 ©2024 Loglass Inc. ルールとハンドリングを分けて考える ルールの適用 ハンドリング - どっちが大事?

Slide 96

Slide 96 text

96 ©2024 Loglass Inc. ルールとハンドリングを分けて考える ルールの適用 ハンドリング - どっちが大事?→ルールの適用の方が大事

Slide 97

Slide 97 text

97 ©2024 Loglass Inc. ルールとハンドリングを分けて考える ルールの適用 ハンドリング - どう守る? 可能な限り 型で守る テストで守る

Slide 98

Slide 98 text

98 ©2024 Loglass Inc. 関数の全域性から考えてみる

Slide 99

Slide 99 text

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著 訳)数学の関数は、可能性のある各入力を出力に結びつけます。関数型プログラミングで は、すべての入力が対応する出力を持つ ように、同じように関数を設計しようとします。この ような関数は全域関数と呼ばれます。

Slide 100

Slide 100 text

100 ©2024 Loglass Inc. X × 2 全域関数の例: 掛け算 - ある整数を受け取って 2倍にする関数→全域関数 1 2 3 6 3 6 12 入力 出力 0 0 整数 整数

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

104 ©2024 Loglass Inc. ルールとハンドリングの考え方に当てはめる ルールの適用 ハンドリング 0で割ってはいけないというルールを 型で守る fun divide12By(denominator: NonZeroInt): Int { return 12 / denominator.value }

Slide 105

Slide 105 text

105 ©2024 Loglass Inc. 結局これは何をやっているの? - 正常な値を返す範囲まで入力値を型で絞る - ルールを型で表現でき、ルールを型で守れた 12 / X 1 12 3 4 3 6 2 入力 出力 0ではない整数 整数 0

Slide 106

Slide 106 text

106 ©2024 Loglass Inc. 注文のキャンセルに話を戻す キャンセル 確定済み Exception 未確定 キャンセル済み キャンセル済み Exception 入力 出力 発送済 み Exception 注文 注文

Slide 107

Slide 107 text

107 ©2024 Loglass Inc. 注文のキャンセルに話を戻す キャンセル 確定済み 未確定 キャンセル済み キャンセル済み 入力 出力 発送済 み

Slide 108

Slide 108 text

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("発送済みの注文はキャンセルできません") } } }

Slide 109

Slide 109 text

109 ©2024 Loglass Inc. 注文のキャンセルに話を戻す - 全域関数ではなかった関数を全域関数にしていたということでした class ConfirmedOrder(...) : Order { fun cancel(cancelReason: String?, now: LocalDateTime): CancelledOrder { return CancelledOrder( ..., cancelledAt = now, cancelReason = cancelReason, ) } }

Slide 110

Slide 110 text

110 ©2024 Loglass Inc. 正常な値を返す範囲まで入力を絞り 全域関数にすることで ルールを型で守ることができた!

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

115 ©2024 Loglass Inc. ロジック内でDBアクセスしたいところ

Slide 116

Slide 116 text

116 ©2024 Loglass Inc. 例えばここ

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

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, getProduct: (ProductId) -> Product?, ): UnconfirmedOrder { … } } }

Slide 123

Slide 123 text

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問題発生するのでは?

Slide 124

Slide 124 text

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内の処理を どう注入するかは自由

Slide 125

Slide 125 text

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のリストを取得

Slide 126

Slide 126 text

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で 今回使いたい商品だけを先に取得

Slide 127

Slide 127 text

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アクセスは発生しない

Slide 128

Slide 128 text

128 ©2024 Loglass Inc. もちろん先読みできない場合もある - その時は最初に余分に読み込み or 都度DBアクセスになる。 - どちらを選択したとしてもドメインモデルのメソッドとしてインターフェースが 崩れないのがメリット class UnconfirmedOrder(...) { companion object { fun create( customerId: CustomerId, shippingAddress: CreateAddressParam, lines: List, getProduct: (ProductId) -> Product?, ): UnconfirmedOrder { … } } }

Slide 129

Slide 129 text

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

Slide 130

Slide 130 text

130 ©2024 Loglass Inc. 悪用禁止 - 利用はあくまで外部システム、 DBの責務でドメイン層に対して隠蔽したいことに制限す る - ドメイン層の責務を 関数で注入しないこと class UnconfirmedOrder(...) { companion object { fun create( customerId: CustomerId, shippingAddress: CreateAddressParam, lines: List, calculatePrice: (ProductId, OrderQuantity) -> Price, ): UnconfirmedOrder { … } } } NG

Slide 131

Slide 131 text

131 ©2024 Loglass Inc. ドメインモデルで外部の依存を 扱いたい場合は高階関数で依存を注入しよう

Slide 132

Slide 132 text

132 ©2024 Loglass Inc. 最後にまとめ 関数型のエッセンスを適度に取り入れ、 DDDをよりタイプセーフにする。 そしてソフトウェアの品質を上げる。

Slide 133

Slide 133 text

133 ©2024 Loglass Inc. 最後にまとめ nullableたくさんで ややこしい

Slide 134

Slide 134 text

134 ©2024 Loglass Inc. 最後にまとめ 代数的データ型で モデルの状態を 型で表現

Slide 135

Slide 135 text

135 ©2024 Loglass Inc. 最後にまとめ 全域関数を使って 状態遷移を型で表現

Slide 136

Slide 136 text

136 ©2024 Loglass Inc. 最後にまとめ 高階関数を使うことで ドメインモデルのロジック内で 簡潔にDBアクセス

Slide 137

Slide 137 text

137 ©2024 Loglass Inc. 最後にまとめ 関数型のエッセンスを適度に取り入れ、 DDDをよりタイプセーフにする。 そしてソフトウェアの品質を上げる。

Slide 138

Slide 138 text

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

Slide 139

Slide 139 text

139 ©2024 Loglass Inc. ありがとうございました!質問は X でも受け付けます!直接の質問も大歓迎 󰢐 https://twitter.com/Yuiiitoto - ブースがあるので遊びにきてください 🐳

Slide 140

Slide 140 text

140