Slide 1

Slide 1 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 1/53 Scala コードとともに考えるドメインモデリング Scala 福岡 2019 かとじゅん(@j5ik2o) Scala 福岡 2019 1 / 53

Slide 2

Slide 2 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 2/53 Chatwork テックリード github/j5ik2o scala­ddd­base scala­ddd­base­akka­http.g8 reactive­redis reactive­memcached 翻訳レビュー エリックエヴァンスのドメイン駆動設計 Akka 実践バイブル 自己紹介 Scala コードとともに考えるドメインモデリング Scala 福岡 2019 2 / 53

Slide 3

Slide 3 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 3/53 最近の発表ネタ 1. ドメインモデリングの始め方 ­ AWS Dev Day Tokyo 2018 ドメインオブジェクトの発見・実装・リファクタリングの方法論をカバー 2. Scala でのドメインモデリングのやり方 ­ Scala 関西Summit 2018 1. のスライドと同様の観点だが、より実装技法寄りの議論をカバー Scala コードとともに考えるドメインモデリング Scala 福岡 2019 3 / 53

Slide 4

Slide 4 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 4/53 アジェンダ 同じネタはやりません 過去のネタについて議論したいなら、懇親会で捕まえてください! 1. ドメインイベントを使ったモデリングと実装 2. 集約を跨がる整合性の問題 Scala コードとともに考えるドメインモデリング Scala 福岡 2019 4 / 53

Slide 5

Slide 5 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 5/53 対象のドメイン ウォレットサービス ユーザが電子マネーをウォレットという概念で管理できるサービス 某Kyash さんのようなサービスをイメージしてもらえば… API サーバを開発する想定で考える Scala コードとともに考えるドメインモデリング Scala 福岡 2019 5 / 53

Slide 6

Slide 6 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 6/53 ウォレットサービスの概要 主な機能 ユーザがサインアップすると電子マネーを管理するウォレットが一つ作成される ユーザがクレジットカードなどからウォレットにチャージできる ユーザ間で請求や支払いができる(飲み会の割り勘のときに使える) 料金プランはパーソナルプランとファミリプランがあり、プラン変更もできる Scala コードとともに考えるドメインモデリング Scala 福岡 2019 6 / 53

Slide 7

Slide 7 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 7/53 ドメインモデルの輪郭を捉える Scala コードとともに考えるドメインモデリング Scala 福岡 2019 7 / 53

Slide 8

Slide 8 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 8/53 想定するユースケース アクター ユーザ ユースケース ユーザが、ウォレットにチャージできる(クレジットカードなど) ユーザが、他のウォレットに請求する ユーザが、他のウォレットに支払う ユーザが、他のウォレットからの請求を受け取る ユーザが、他のウォレットからの支払を受け取る ユーザが、ウォレットの残高を確認できる ユーザが、支払履歴を確認する ユーザが、請求履歴を確認する ユーザが、プランをパーソナルプランもしくはファミリプランに切り替える ユーザが、ウォレットを追加/ 削除/ 一覧確認できる Scala コードとともに考えるドメインモデリング Scala 福岡 2019 8 / 53

Slide 9

Slide 9 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 9/53 ユーザが、他のウォレットに支払う 晴れの日コース ユーザは、支払ボタンをクリックする システムは、支払画面を表示する ユーザは、支払画面上に支払( 支払元ウォレットID, 支 払先ウォレットID 、名目、金額) を入力し、支払ボタ ンをクリックする システムは、受け取った支払から以下を行う 支払元ウォレットID をユーザが所有しているか確 認し、ストレージからウォレットを読み出す 支払元から支払先への支払を、支払元ウォレット に履歴として残す(To 側) 支払元から支払先への支払を、支払先ウォレット に履歴として残す(From 側) 雨の日コース 残高がマイナスになる請求はどうするのか? 雨の日コースから重要なビジネスルールを見つけることができ る FYI: ユースケース記述 Scala コードとともに考えるドメインモデリング Scala 福岡 2019 9 / 53

Slide 10

Slide 10 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 10/53 ビジネスルールに注目する ユースケース記述を書き起こしながら、ビジネスルールについても議論する 支払・請求は、誰から誰へ、名目、いくらかが分かる必要がある 支払には請求があるものとないものがある。請求がなくても支払はできる 残高が0 になる支払は行えるのか?行えないのか? プランでできること・できないこととは? パーソナルは1 ウォレットのみ、ファミリは10 ウォレットのみ 契約ってどう表現するの?そもそも契約とは? Scala コードとともに考えるドメインモデリング Scala 福岡 2019 10 / 53

Slide 11

Slide 11 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 11/53 Part­1 ドメインイベントを使ったモデリングと実装 Scala コードとともに考えるドメインモデリング Scala 福岡 2019 11 / 53

Slide 12

Slide 12 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 12/53 ドメインモデルと集約を明らかにする Scala コードとともに考えるドメインモデリング Scala 福岡 2019 12 / 53

Slide 13

Slide 13 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 13/53 FYI: 関心事の分離 ヒト 個人、企業、担当者、など モノ 商品、サービス、店舗、場所、権利、など コト( ドメインイベント) 予約、注文、支払、出荷、キャンセル、など ( 出典: 現場で役立つシステム設計の原則) Scala コードとともに考えるドメインモデリング Scala 福岡 2019 13 / 53

Slide 14

Slide 14 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 14/53 コトからモノを整理する コトに注目することで次の関係も明らかになる コトはヒトとモノとの関係として出現する( だれの何についての行動か) コトは時間軸に沿って明確な前後関係を持つ ( 出典: 現場で役立つシステム設計の原則) Greg Young 氏考案のEvent Sourcing もモノではなくコトをモデリングの主役と位置づけている Scala コードとともに考えるドメインモデリング Scala 福岡 2019 14 / 53

Slide 15

Slide 15 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 15/53 コト= ドメインイベント Scala コードとともに考えるドメインモデリング Scala 福岡 2019 15 / 53

Slide 16

Slide 16 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 16/53 FYI: ドメインイベントの現れ方 「… するときに」や「… した場合」という用語法に現れる XXX するときに YYY だったら気にしないが、もしXXX だったら注意が必要 XXX の場合は、把握しておく必要がる XXX が発生した場合に 過去形で表現される関心時がそのままドメインイベントとして表現される Scala コードとともに考えるドメインモデリング Scala 福岡 2019 16 / 53

Slide 17

Slide 17 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 17/53 ドメインイベントから集約を見つける手順 1. ユースケースからドメインイベントを洗い出す 2. そのドメインイベントを発生させる元となるコマンドを洗い出す 3. コマンドを発行するアクターを洗い出す( またそのときの入力となるリードモデルや条件も洗い出す) 4. そのコマンドを受け取って、副作用を起こし、ドメインイベント発行する集約の名前を付ける ( 画像出典:https://en.wikipedia.org/wiki/Event_storming) Scala コードとともに考えるドメインモデリング Scala 福岡 2019 17 / 53

Slide 18

Slide 18 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 18/53 ドメインイベントを洗い出す ユースケース( 記述含む) からドメインイベントを見つける ユーザが、ウォレットにチャージできる = MoneyDeposited ユーザが、他のウォレットに請求する = MoneyRequested ユーザが、他のウォレットに支払う = MoneyPaid ユーザが、他のウォレットからの請求を受け取る = MoneyRequestReceived ユーザが、他のウォレットからの支払を受け取る = MoneyPaymentReceived Scala コードとともに考えるドメインモデリング Scala 福岡 2019 18 / 53

Slide 19

Slide 19 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 19/53 タイムライン状にドメインイベントを並べて抜け漏れがな いか確認 前後のイベントに依存関係も洗い出す(" 申請した" のあと に" 承認した" があるかなど) ドメインイベントの重複が起きないか( 要件によっては、 集約の不変条件が壊れる) ドメイン上のストーリを語る Scala コードとともに考えるドメインモデリング Scala 福岡 2019 19 / 53

Slide 20

Slide 20 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 20/53 チャージイベント(MoneyDeposited) チャージ元口座( クレジットカードなど) チャージ金額 チャージ日時 請求イベント(MoneyRequested) 請求元のウォレットID 請求先のウォレットID 名目(Optional) 金額 請求日時 支払イベント(MoneyPaid) 支払元のウォレットID 支払先のウォレットID 名目(Optional) 金額 支払日時 請求ID(Optional) ドメインイベントの例 Scala コードとともに考えるドメインモデリング Scala 福岡 2019 20 / 53

Slide 21

Slide 21 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 21/53 ドメインイベントが分かればコマンドも分かる場合が多い チャージコマンド(DepositMoney) チャージ元口座 金額 日時 請求コマンド(RequestMoney) 請求元のウォレットID 請求先のウォレットID 名目(Optional) 金額 請求日時 支払コマンド(PayMoney) 支払元のウォレットID 支払先のウォレットID 名目(Optional) 金額 支払日時 請求ID(Optional) コマンドは以下の実装に対応する 集約アクターに対するメッセージ 集約クラスのメソッド コマンドの例 Scala コードとともに考えるドメインモデリング Scala 福岡 2019 21 / 53

Slide 22

Slide 22 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 22/53 アクターを洗い出す ユースケースを書いていたらすぐにわかる。今回の場合はすべてユーザ ワークフローがあるようなドメインでは、コマンドを発行する、複数のアクターを考慮する Scala コードとともに考えるドメインモデリング Scala 福岡 2019 22 / 53

Slide 23

Slide 23 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 23/53 コマンド発行条件とは? アクターはどんなときに、そのコマンドを発行するのか? リードモデルが判断条件に一致したときに発行される アクターがシステムでも、人でも同じ Scala コードとともに考えるドメインモデリング Scala 福岡 2019 23 / 53

Slide 24

Slide 24 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 24/53 FYI: リードモデルの例 発生したイベントからDTO を作る 取引DTO 取引ID 発生日時 取引種別( チャージ/ 請求/ 支払) リソースID(Optional) From ウォレットID(Optional) From ユーザアカウント名(Optional) To ウォレット元ID To ユーザアカウント名 適用 金額 Scala コードとともに考えるドメインモデリング Scala 福岡 2019 24 / 53

Slide 25

Slide 25 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 25/53 コマンドを受け付けて、副作用を起こし、ドメインイベン トを発生させる集約をイメージする DepositMoney ­> 副作用 ­> MoneyDeposited ReuestMoney ­> 副作用 ­> MoneyRequested PayMoney ­> 副作用 ­> MoneyPaid コマンドはメソッド名になる 副作用を扱うオブジェクト( 集約) の名前を見つける Wallet 集約とする Wallet の責務も定義する 電子マネーのチャージ・請求・支払の管理 集約の名前を探す( 命名) Scala コードとともに考えるドメインモデリング Scala 福岡 2019 25 / 53

Slide 26

Slide 26 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 26/53 ドメインオブジェクトと集約を実装する Scala コードとともに考えるドメインモデリング Scala 福岡 2019 26 / 53

Slide 27

Slide 27 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 27/53 Wallet はドメインイベントを履歴として扱う Scala コードとともに考えるドメインモデリング Scala 福岡 2019 27 / 53

Slide 28

Slide 28 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 28/53 Event Message Pattern 出来事をPub/Sub を使って通知する仕組み Event Sourcing(ES) 状態を永続化するのではなく、発生する出来事をすべ て永続化( 追記のみ) する FYI: ドメインイベントの利用例 Scala コードとともに考えるドメインモデリング Scala 福岡 2019 28 / 53

Slide 29

Slide 29 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 29/53 簡易的なEvent Sourcing を実装する Scala コードとともに考えるドメインモデリング Scala 福岡 2019 29 / 53

Slide 30

Slide 30 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 30/53 全体象 ※ リードモデルアップデータの解説は省きます… 。 Scala コードとともに考えるドメインモデリング Scala 福岡 2019 30 / 53

Slide 31

Slide 31 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 31/53 Wallet(1/2) WalletEvent の集合はファーストクラスコレクション(WalletEvents) に委譲する 最新の状態(balance など) も必要。不変なイベントから導出する想定 コマンドをメソッドとして展開する trait Wallet { def events: WalletEvents // def id: WalletId def userAccountId: UserAccountId def balance: Money def createdAt: Timestamp // def deposit(from: MoneyResource, money: Money, createdAt: Timestamp = ZonedDateTime.now()): Result[Wallet] // 請求 def request(toId: WalletId, money: Money, createdAt: Timestamp = ZonedDateTime.now()): Result[(Wallet, WalletEventId)] // 支払 def pay(toId: WalletId, money: Money, requestEventId: Option[WalletEventId] = None, createdAt: Timestamp = ZonedDateTime.now()): Result[Wallet] // ... } Scala コードとともに考えるドメインモデリング Scala 福岡 2019 31 / 53

Slide 32

Slide 32 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 32/53 Wallet(2/2) 請求された側と支払れた側のメソッドも必要 request した後にreceiveRequest するのを忘れると問題… 。やり方はいくつかありそう。後述します trait Wallet { // ... // 請求 def receiveRequest(fromId: WalletId, money: Money, createdAt: Timestamp = ZonedDateTime.now()): Result[Wallet] // 支払 def receivePayment(fromId: WalletId, money: Money, requestEventId: Option[WalletEventId], createdAt: Timestamp = ZonedDateTime.now()): Result[Wallet] } Scala コードとともに考えるドメインモデリング Scala 福岡 2019 32 / 53

Slide 33

Slide 33 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 33/53 sealed trait WalletEvent extends Event { val id: WalletEventId val walletId: WalletId val userAccountId: UserAccountId val createdAt: Timestamp } // case class MoneyDeposited(id: WalletEventId, walletId: WalletId, userAccountId: UserAccountId, from: MoneyResource, money: Money, createdAt: Timestamp) extends WalletEvent // 請求 case class MoneyRequested(id: WalletEventId, walletId: WalletId, userAccountId: UserAccountId, toId: WalletId, money: Money, createdAt: Timestamp) extends WalletEvent // 支払 case class MoneyPaid(id: WalletEventId, walletId: WalletId, userAccountId: UserAccountId, toId: WalletId, money: Money, requestEventId: Option[WalletEventId], createdAt: Timestamp) extends WalletEvent WalletEvent(1/2) ケース漏れを防ぐためにsealed する Scala コードとともに考えるドメインモデリング Scala 福岡 2019 33 / 53

Slide 34

Slide 34 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 34/53 WalletEvent(2/2) 支払われた=MoneyPaymentReceived はMoneyDeposited に似ているが意味が異なるので別の型とした // 請求 case class MoneyRequestReceived(id: WalletEventId, walletId: WalletId, userAccountId: UserAccountId, fromId: WalletId, money: Money, createdAt: Timestamp) extends WalletEvent // 支払 case class MoneyPaymentReceived(id: WalletEventId, walletId: WalletId, userAccountId: UserAccountId, fromId: WalletId, money: Money, requestEventId: Option[WalletEventId], createdAt: Timestamp) extends WalletEvent Scala コードとともに考えるドメインモデリング Scala 福岡 2019 34 / 53

Slide 35

Slide 35 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 35/53 WalletEvents ファーストクラスコレクションの例 煩雑さを軽減する ユビキタス言語でメソッドを表現することで、実装を読まなくても設計が予測可能になる breachEncapsulationOfEvents は妥協して使えるが、乱用するとドメインロジックがドメイン層から流出するので注意 case class WalletEvents(breachEncapsulationOfEvents: Seq[WalletEvent]) { // Seq Stream private val values = breachEncapsulationOfEvents require(values.nonEmpty) def walletId: WalletEventId = values.head.id def userAccountId: UserAccountId = values.head.userAccountId def createdAt: Timestamp = values.head.createdAt def add(other: WalletEvents): WalletEvents = WalletEvents(values ++ other.values) def add(other: WalletEvent): WalletEvents = add(WalletEvents(Seq(other))) } Scala コードとともに考えるドメインモデリング Scala 福岡 2019 35 / 53

Slide 36

Slide 36 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 36/53 イベントから状態を導出する すべてのドメインイベントがコンストラクタに渡される。イベント列が大きいと導出の計算コストも大きくなる 必ずしもすべてのイベント列が必要ではない場合、最新のスナップショット+差分ドメインイベントの集合でよくなる case class WalletImpl(events: WalletEvents, snapshotBalance: Money = Money.zero(Money.JPY)) extends Wallet { override lazy val id: WalletEventId = events.walletId override lazy val contractId: ContractId = events.contractId override lazy val userAccountId: UserAccountId = events.userAccountId override lazy val createdAt: Timestamp = events.createdAt override lazy val balance: Money = { // FIXME: events.breachEncapsulationOfEvents.reverse.foldLeft(snapshotBalanace) { case (r, MoneyDeposited(_, _, _, _, _, money, _)) => r.plus(money) case (r, MoneyPaid(_, _, _, _, _, money, _, _)) => r.minus(money) case (r, MoneyPaymentReceived(_, _, _, _, _, money, _, _)) => r.plus(money) case (r, _) => r } } // ... } Scala コードとともに考えるドメインモデリング Scala 福岡 2019 36 / 53

Slide 37

Slide 37 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 37/53 振る舞いの実装 クライアントが契約を満たしているか確認する イベントを生成して、新しいインスタンスのイベントの最後に追加する override def pay(toId: WalletId, money: Money, requestEventId: Option[WalletEventId] = None, createdAt: Timestamp): Result[Wallet] = money match { case m if m.currency != balance.currency => Left(new InvalidCurrencyError("Invalid currency")) case m if balance.minus(money).isNegative => Left(new InvalidBalanceError(s"fromId: $id, toId: $toId, money: $money")) case _ => val event = MoneyPaid(ULID.random(), id, userAccountId, toId, money, requestEventId, createdAt) Right( copy( events = events.add(event) ) ) } Scala コードとともに考えるドメインモデリング Scala 福岡 2019 37 / 53

Slide 38

Slide 38 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 38/53 ドメインサービスの例 支払は、支払う側と支払われる側の状態がある。どちらが欠けることは許されない( 集約インスタンスを跨がる不変条件) 現状のWallet の実装はよくないが、二つのインスタンスに跨がる状態の制御は実装しにくいし、理解もしにくい 1. receivePayment メソッドをprivate 化し、payment メソッド内部でコールする方法( 読む側に負担が大きそう) 2. payment メソッドとreceiveRequest メソッドをパッケージプライベートにして、ドメインサービスとして実装する方 法 object PaymentService { case class FromTo(from: Wallet, to: Wallet) def execute(from: Wallet, to: Wallet, money: Money, requestEventId: Option[WalletEventId] = None, createdAt: ZonedDateTime = ZonedDateTime.now()): Result[FromTo] = { for { newFrom <- from.pay(to.id, money, requestEventId, createdAt) newTo <- to.wasPaid(from.id, money, requestEventId, createdAt) } yield FromTo(newFrom, newTo) } } Scala コードとともに考えるドメインモデリング Scala 福岡 2019 38 / 53

Slide 39

Slide 39 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 39/53 リポジトリの例 イベントの永続化はEventStore に委譲 イベントの永続化後にコールバックする class WalletRepositoryOnMemory extends WalletRepository[Task] { private val listeners: mutable.Seq[WalletEvent => Unit] = mutable.Seq.empty private val eventStore = new EventStoreOnMemory[WalletEvent]() override def addListeners(listeners: Seq[WalletEvent => Unit]): Unit = this.listeners ++ listeners override def store(aggregate: Wallet): Task[Unit] = eventStore.add(aggregate.id, aggregate.events.breachEncapsulationOfEvents).doOnFinish { case None => fireEvents(aggregate) // ... } override def resolveById(id: WalletId): Task[Wallet] = eventStore.iterator(id).map { events => Wallet(WalletEvents(events.toSeq)) // Stream } // ... } Scala コードとともに考えるドメインモデリング Scala 福岡 2019 39 / 53

Slide 40

Slide 40 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 40/53 EventStore の例 イベント列には順序を保証する概念が必要 class EventStoreOnMemory[E] extends EventStore[E] { private val partitions: mutable.Map[String, mutable.Queue[E]] = mutable.Map.empty override def add(aggregateId: String, event: E): Task[Unit] = Task { val queue = partitions.getOrElseUpdate(aggregateId, mutable.Queue.empty) queue.enqueue(event) } override def add(aggregateId: String, events: Seq[E]): Task[Unit] = Task.sequence(events.map(event => add(aggregateId, event))).map(_ => ()) override def fetch(aggregateId: String): Task[E] = Task { val queue = partitions.getOrElseUpdate(aggregateId, mutable.Queue.empty) queue.dequeue() } override def iterator(aggregateId: String): Task[Iterator[E]] = Task { val queue = partitions.getOrElseUpdate(aggregateId, mutable.Queue.empty) queue.iterator } } Scala コードとともに考えるドメインモデリング Scala 福岡 2019 40 / 53

Slide 41

Slide 41 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 41/53 集約のイベントストリームに対して、複数のスレッドから同 時にアクセスがあって、同時に読まれることもありえる。そ うなると、並行処理の衝突が発生する可能性が出てくる。こ れを無視して放置していると、集約が不正な状態になってし まう問題が発生する。二つのスレッドが、イベントストリー ムを同時に変更しようとした場合を考えてみよう。そのよう すを図A­10 に示す。 ( 実践ドメイン駆動設計より) 対策は、Evt4 の追加時に同じキーがあれば、Evt4,Evt5 の追加 を失敗する( しかもこれがアトミックにできないと意味がない) というものだが、ストレージの機能によって実装が難しい場合 がある。多分、Kafka は無理… 。 つまるところ、状態を一次的 にしか持たないアプリケーションでは、EventStore#add にロッ クが必要… 。 FYI: 集約の更新が競合するケース Scala コードとともに考えるドメインモデリング Scala 福岡 2019 41 / 53

Slide 42

Slide 42 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 42/53 集約をイベント列から再生して、クラスター上で唯一のア クター(= 集約アクター) として常時起動 イベントストリームに書き込むスレッドが他にいない ので競合問題が回避できる メッセージはShardRegion をプロキシーとしてルーティン グされる 集約がアクターが不変条件を維持する( 並行制御からのロ ックなども含む) コマンドに反応した集約アクターは、イベントを追加書き 込み 「Akka 実践バイブル」の「第15 章 アクターの永続化」あ たりを読むとよい FYI: なぜAkka ではES がうまくいくか Scala コードとともに考えるドメインモデリング Scala 福岡 2019 42 / 53

Slide 43

Slide 43 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 43/53 Part­2 集約を跨がる整合性問題について Scala コードとともに考えるドメインモデリング Scala 福岡 2019 43 / 53

Slide 44

Slide 44 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 44/53 集約を跨がる整合性の問題 ユースケース ユーザが、プラン(Plan) をパーソナルプランもしくはファミリプランに切り替える ユーザが、ウォレットを追加/ 削除/ 一覧確認できる 契約(Contract) に応じて、Wallet のインスタンス数を制限しなければならない(Wallet 上限ルール) ネタ元 2018­12­03 集約の境界と整合性の維持の仕方に悩んで2 ヶ月ぐらい結論を出せていない話 「集約の境界と整合性( 略」に対して頂いたアイデアの分類と現状での僕の回答らしきもの Scala コードとともに考えるドメインモデリング Scala 福岡 2019 44 / 53

Slide 45

Slide 45 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 45/53 Plan とContract // 料金 sealed trait Plan extends EnumEntry { def maxWallets: Int def minWallets: Int } object Plan extends Enum[Plan] { override def values: immutable.IndexedSeq[Plan] = findValues case object Personal extends Plan { override val minWallets: Int = 1 override val maxWallets: Int = 1 } case object Family extends Plan { override val minWallets: Int = 1 override val maxWallets: Int = 10 } } // 契約 表 集約 case class Contract(id: ContractId, ownerId: UserAccountId, plan: Plan, createdAt: Timestamp, updatedAt: Timestamp) { // ... } Scala コードとともに考えるドメインモデリング Scala 福岡 2019 45 / 53

Slide 46

Slide 46 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 46/53 Plan を超えてWallet が登録できる問題 こんなコードを書いてませんか?ダメですよ! 以下のロジックを複数スレッド( もしくは別のプロセス) で同時に実行すると、整合性が破綻します 異なる集約では異なるトランザクション境界を持つため、アトミックなI/O がそもそもできない( 結果整合) object WalletApplicationService { def addWallet(wallet: Wallet): Result[Unit] = { val contract = contractRepository.resolveById(contractId) val walletCount = walletRepository.countEnabledByContractId(contractId) if (contract.plan.maxWallets > walletCount) Right(walletRepository.store(wallet)) else Left(new WalletLimitOverError(s"contract = $contract, wallet = $wallet")) } } Scala コードとともに考えるドメインモデリング Scala 福岡 2019 46 / 53

Slide 47

Slide 47 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 47/53 解決策とそのつらさ… 1. 集約をマージして1 つのトランザクション境界にする たとえば、Contract とWallet をマージする。Contract has Wallets … Wallets の参照を得るのに、いちいちContract を特定しなければならない ロックが競合する頻度があがる DB I/O のパフォーマンス劣化 2. 集約をマージせずに、独立した集約間は結果整合性を用いる 今回の場合はこちら。どうにもならない場合もある… 。 3. アンチパターン: 集約間を跨がったトランザクション制御を用いる def addWallet(wallet: Wallet): Result[Unit] = DB.localTx { ... } 1 集約1 トランザクション境界のルールが破綻し、ユースケースによって集約の整合性境界がバラバラになる。デッド ロックの温床にもなりやすい 4. ユースケースやビジネスルールの見直し 利害関係者の合意が難しい場合も… 。 Scala コードとともに考えるドメインモデリング Scala 福岡 2019 47 / 53

Slide 48

Slide 48 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 48/53 どうするといいのか 1. 万能ではないが結果整合性でカバーしつつ、ある程度の不整合を許すようにする 2. 一次的な破綻を受け入れて、結果整合を利用する 無効なWallet を追加して、別プロセスで有効化する 今回は前者の例を示します Scala コードとともに考えるドメインモデリング Scala 福岡 2019 48 / 53

Slide 49

Slide 49 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 49/53 結果整合性をうまくつかう方法の一つ Contract が有効なWalletIds を持つ case class Contract(id: ContractId, ownerId: UserAccountId, plan: Plan, walletIds: WalletIds, createdAt: Timestamp, updatedAt: Timestamp) { def addWalletIds(walletIds: WalletIds): Result[Contract] = { if (!AddWalletSpec(this, walletIds)) // 仕様 Left(new WalletLimitOverError(s"walletIds = $walletIds")) else Right( copy( walletIds = this.walletIds.add(walletIds) ) ) } def removeWalletIds(walletIds: WalletIds): Result[Contract] = ??? } Scala コードとともに考えるドメインモデリング Scala 福岡 2019 49 / 53

Slide 50

Slide 50 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 50/53 FYI: 仕様パターン:WalletSpec 暗黙的な概念を明示的にするためのパターン 複雑なルールはor,and,not 演算子で合成して作れるようにするよい // 追加仕様 object AddWalletSpec extends ((Contract, WalletIds) => Boolean) { override def apply(contact: Contract, walletIds: WalletIds): Boolean = contact.plan.maxWallets > (contact.walletIds.size + walletIds.size) } // 削除仕様 object RemoveWalletSpec extends ((Contract, WalletIds) => Boolean) { override def apply(contact: Contract, walletIds: WalletIds): Boolean = contact.plan.minWallets < (contact.walletIds.size - walletIds.size) } Scala コードとともに考えるドメインモデリング Scala 福岡 2019 50 / 53

Slide 51

Slide 51 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 51/53 AddWalletUseCase 泥臭いテクニックが固まり… 。 case class AddWalletDto(userAccountId: UserAccountId) class AddWalletUseCase[M[_]](contractRepository: ContractRepository[M], walletRepository: WalletRepository[M])( implicit ME: MonadError[M, Error] ) { def execute(dto: AddWalletDto): M[Unit] = { for { contract <- contractRepository.resolveByUserAccountId(dto.userAccountId) // AUTO_INCREMENT 採番 --- (1) newWallet <- ME.pure(Wallet(ULID.random(), contract.id, contract.ownerId)) newContract <- contract.addWalletIds(newWallet.id) match { // 強制 --- (2) case Right(v) => ME.pure(v) case Left(e) => ME.raiseError(e) } _ <- contractRepository.store(newContract) // 楽観 --- (3) _ <- walletRepository.store(newWallet) // 失敗 (3) (3) 追加 walletId 無害化 --- (2) } yield () } } このようなトランザクションを跨がる整合性の問題を調停する役割をProcess Manager やSaga と呼ぶ Scala コードとともに考えるドメインモデリング Scala 福岡 2019 51 / 53

Slide 52

Slide 52 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 52/53 まとめ ドメインイベントは、ドメインの分析と実装の両方で使えるツール 集約を跨がる整合性の問題は難しいが、解決方法がないわけではない Scala コードとともに考えるドメインモデリング Scala 福岡 2019 52 / 53

Slide 53

Slide 53 text

2019/1/19 localhost:4100/presentation.html#1 http://localhost:4100/presentation.html#1 53/53 一緒に働くエンジニアを募集しています! http://corp.chatwork.com/ja/recruit/ Scala コードとともに考えるドメインモデリング Scala 福岡 2019 53 / 53