起きた出来事から業務を始める
画面やデータベース設計から考えるのではなく、実際にあるべき業務の流れから考え始めます。
- ドメインイベント 予約がキャンセルされた
この出来事から、原因となるコマンドと、成功に必要だった条件を逆算します。
コマンド、確認する条件、集約を見つける
コマンドと条件を逆算する
- ドメインイベント
- 業務で起きた出来事(イベント)を書いてください。ドメインイベントは過去形(〇〇された)で表現します。
- コマンド
- ドメインイベントが発生する起点となる指示や操作を書いてください
- アクター
- コマンドを発する人、あるいは役割を書いてください
- 集約
- 状態の整合性を守るドメインモデルを書いてください。
- Hotspot
- 答えが揃わなかった点や、今後決める必要があることを書いてください。
班で次の3問を順に話しましょう。
- この出来事は、誰が、何を依頼したことによって発生しましたか。
- その実行者には、予約を管理する権限がありますか。
- 予約の状態とキャンセル理由は、どんな条件を満たす必要がありますか。
診察直前のキャンセルを認めるかなど、答えが揃わない点は無理に決めずHotspotとして残します。
予約を管理する担当者が、診察開始前の予約だけを、キャンセルできます。キャンセルする際には、キャンセル理由を記入する必要があります。
この条件は、コマンドを受けたユースケースが確認する条件としてまとめます。
ROPで失敗の経路を設計する
このセクションでは、EventStormingで見つけたコマンド、確認する条件、集約、ドメインイベントを、失敗を含むユースケースの流れへ写します。
2本のレールを考える
Railway Oriented Programmingは、処理の流れを成功と失敗の2本のレールで捉える考え方です。一方は処理に成功した場合の経路、もう一方は失敗した場合の経路を表します。
各工程が成功したら、その出力を次の工程へ渡します。失敗したら下のレールへ移り、後続の成功処理をスキップします。まずは図を左からたどり、どの条件で失敗のレールへ切り替わるかを確認してください。
なぜROPを利用するのか
それぞれの処理で発生する失敗を同じフローで扱い、その失敗を網羅的に検証することで、変更箇所とテスト観点を追いやすくなります。これにより、「キャンセル先の予約が存在しない」、「キャンセルする権限がない」などさまざまな理由で止まっても、それを適切にハンドリングでき、全体の処理のうち一部だけ処理されて不整合が発生することを防げます。
EventStormingをユースケースへ写す
先ほどの付箋を、ユースケースの要素へ一つずつ対応させましょう。コマンドは入力へ、集約はドメインロジックのAppointment.cancelへ、ドメインイベントは成功出力へ写します。実行者の権限、予約の存在、現在状態、理由は、それぞれ確認する条件として明示します。
- 入力
- 予約キャンセルのコマンドと、実行者・予約の現在状態。
- ユースケースの確認
- 実行者に予約を管理する権限があり、対象の予約が存在することを確認する。
- ドメインロジック
Appointment.cancelが予約の現在状態とキャンセル理由を確認し、成功イベントを作る。- 成功時の出力
AppointmentCanceled。
I/Oをドメインロジックの両端へ追い出す
このセクションでは、前のセクションで組み立てたユースケースからデータの取得と記録を分け、中心のドメインロジックを副作用のないpureな関数として扱える構成を学びます。
ビジネスユースケース、ドメインモデル、ユースケースを区別する
ビジネスユースケースとは、アクターが達成したい業務上の目的です。例えば「受付担当者が予約をキャンセルする」がこれにあたります。
ソフトウェアのユースケースは、Resolverから実行者と対象の予約を受け取り、権限の有無と予約の存在を確認します。
診察開始前で、かつ理由が記入されている場合にのみ、ドメインモデルのAppointment.cancelは成功イベントを返します。一つの集約と一つのユースケースが常に対応するわけではありません。
なぜResolverとStoreをドメインロジックの外へ置くのか
ResolverとStoreを外へ置く主な理由は、Appointment.cancelをpureに保つためです。データベースへの接続を含めず、入力された値だけで結果が決まるようにすると、業務ルールを値だけでテストできます。取得方法や記録先が変わっても、中心の判断を変更せずにResolverとStoreの実装を差し替えられます。
入力側:Resolverの役割
User ResolverとAppointment Resolverは、実行者と予約の現在状態を取得し、ユースケースへ値として渡します。取得を入力側へ分けることで、Appointment.cancelをデータベースのAPIから切り離して値だけでテストできます。図では、Resolverから受け取った値が確認する条件を通り、ドメインロジックへ渡るところを指でたどってみてください。
出力側:Storeの役割
Appointment.cancelは、条件を満たしたときだけAppointmentCanceledを返します。Storeはその成功イベントを受け取り、Event Storeへ追記します。失敗した場合はStoreを呼ばないため、不正なキャンセルや途中状態を記録しません。誰がいつどの予約をキャンセルしたかは、追記された出来事から確認できます。
S1のstartExaminationは、まだRepositoryからの取得、状態の記録、監査記録、時刻とIDの生成を一つのユースケースで行います。S2以降でドメインロジックを分離し、後続セッションでResolverとStoreを明示的な依存へ変えます。
S1で残す成果物
最後に、ここまでの議論を次の3点として班のボードへ残しましょう。後続セッションでは、この図と条件を見ながらTypeScriptのコードへ落とし込みます。
- アクター、コマンド、
Appointment集約、AppointmentCanceledを順に並べた図 - 権限、予約の存在、現在状態、理由の条件とHotspot
- 3か所で失敗へ切り替わるROP基本図と、ResolverとEvent Storeを外側へ置いたユースケース図
班ワークは docs/event/session-01-event-storming.excalidraw を使います。TAが開始前にボードを開きます。