はじめに
Domain Modeling Made Functionalの日本語訳版である関数型ドメインモデリングが今年の夏に発売されました。日本語訳版が発売開始された時期に、知り合いのエンジニアから誘われて輪読会を週一で実施することになり、ようやく90%くらい読み切ったので、本記事では具体的な題材を取り上げてワークフローのモデリングを実践してみたいと思います。
題材
業務フローについてある程度知っているものが良いと思い、私が個人的に大変お世話になった大学内の英会話セッションの予約確定までのフローを取り上げたいと思います。具体的には以下のフローです。
※下記シーケンス図に登場するSA(Student Assistant)
は、英会話の相手をしてくれる人です。自分が在学中の時は、2割ほどが日本人、残りは外国人留学生でした。
後半の方に登場する教務課ですが、英会話セッションに予約すると授業の成績に加点される取り決めとなっているため記述しています。教務課の方では、成績付けという別コンテキストのアクションがあると考えられます。
今回は上記のシーケンス図に記されている事務局の業務(予約確定)のモデリングをしていきます。
文書によるモデリング
まずは文書によるモデリングを行なっていきます。ER図やクラス図ではなく、シンプルなテキストでモデリングをすることで、以下のような利点があります。
- 技術的なインフラストラクチャによって、モデリングが歪められることを防げる
- 技術的な知見が無いドメインエキスパートも一緒にモデリングについて議論ができる
書籍に倣って、以下のように記してみました。
workflow "Place Booking"
input: UnvalidatedBooking
output: (on success):
BookingConfirmationSent
AND BookingPlaced
output: (on error):
ValidationError
do ValidateBooking
if booking is invalid then:
return with ValiationError
do CheckBooking
if booking is not acceptable
return UnacceptableBookingError
do SendBookingConfirmationToStudent
return the events
次に各サブステップの詳細をテキストで表現していきます。
入力と出力に加えて、サブステップの依存関係も明示します。
substep: "ValidateBooking"
input: UnvalidatedBooking
output: ValidatedBooking OR ValidationError
dependencies: CheckSudentCodeExists, CheckBookingLimitExceeds
check the student code syntax
check that student code exists in CollegeSystem
if everything is OK, then:
return ValidatedBooking
else:
return ValidationError
substep: "CheckBookingDetails"
input: ValidatedBooking
output: AcceptedBooking OR UnacceptableBookingError
ask SA that the booking is acceptable or not
if booking is accepted, then:
return AcceptedBooking
else:
return UnacceptableBookingError
substep: "SendBookingConfirmationToStudent"
input: AcceptedBooking
output: None
send booking confirmation email to the student
一見するとGithub Actionsのworkflowのyamlファイルに似ている気がしますが、特定の技術に依存しない自然言語で記されているので、ドメインエキスパートも確認することが可能です。
型によるモデリング
文書によるモデリングを実施した後は、静的型付け言語の型システムを利用したモデリングを行なっていきます。
今回は書籍と同じくF#を用いて型を記述していきます。
入力
まずはワークフローの入力部分であるコマンドについて見ていきます。
今回は単一のワークフローしか扱いませんが、別のワークフローが増えた場合のことを考慮して、ジェネリクスを用いて汎用的なCommand型を定義し、それを利用します。必要に応じてCommandに持たせるフィールドは増減させても良いかもしれません。
type UnvalidatedBooking = {
BookingId: BookingId
StudentInfo: UnvalidatedStudent
BookingDetails: BookingDetails
}
and UnvalidatedStudent = {
StudentCode: string
Name: string
Email: string
}
type Command<'data> = {
Data: 'data
Timestamp: DateTime
UserId: string
}
type PlaceBookingCommand = Command<UnvalidatedBooking>
出力
次にワークフローの出力部分を見ていきます。ワークフローはドメインイベントを返し、そのドメインイベントが別のコマンドをトリガーし、別のワークフローを開始され、...といった具合に続いていきます。
成功時の出力と失敗時の出力をそれぞれ直積型と直和型で定義しています。
// 予約確定ワークフローの成功出力
type BookingConfirmationSent = {
BookingId: BookingId
EmailAddress: EmailAddress
}
type AcceptedBooking = { //SAによる確認完了時に生成される予約状態
...
}
type BookingPlaced = AcceptedBooking
type PlaceBookingEvents {
BookingConfirmationSent: BookingConfirmationSent
AcceptedBooking: AcceptedBooking
}
type PlaceBookingError =
| ValidationError
| UnacceptableBookingError
トップレベル
最後に予約確定ワークフローのトップレベルの関数を定義します。
前述のモデリングで見てきた通り、予約の検証やSAによる予約内容のチェックの際にエラーの発生する可能性があります。そこで、このワークフローがエラーエフェクトを持つ可能性があることを型シグネチャから分かるようにしたいためResultを用いています。またプロセスが非同期であることを型シグネチャで示すために、AsyncResult
というエイリアスを定義します。
こうすることで、関数の実装の詳細を覗かずとも、関数の型シグネチャからどのような出力が得られるのかが分かります。
type AsyncResult<'success, 'failures> = Async<Result<'success, 'failures>>
type PlaceBookingWorkflow =
PlaceBookingCommand -> AsyncResult<PlaceBookingEvents, PlaceBookingError>
おわりに
以上が英会話セッションの予約システムのワークフローのモデリングです。各サブステップの実装では、カリー化や部分適用、モナドなどの関数型プログラミングのテクニックを使って実現していきますが、後続記事に譲りたいと思います。今回はF#で型を記述していきましたが、今後はKotlinやTypeScriptなど、私が業務でよく用いる言語ではどのように記述していけば良いかについても探究していきたいと思います。