永続化を端に追いやる
from DMMF: Persistence
LT;DR
理想としてはすべての関数を「純粋」にしたいが、外部とデータのやり取りをする処理は「純粋」でない
「純粋」でないと、テストしづらいなどの問題が生じる
そのため、ワークフロー 内では、I/O や 永続化 に関する処理を行わない ようにする
具体的には、ワークフロー を 2 つの部分に分ける
ビジネスロジック を含む ドメイン の中心的な部分
I/O 関連コードを含む 端 の部分
I/O を端に追いやる
https://scrapbox.io/files/6690960a4edb40001c7d7b30.png
この構成にすることで、リポジトリパターン は不要になる
永続化を端に追いやる#66b58a7e75d04f000003f2b8
具体例: 永続化を端に追いやる#66b575b475d04f0000b96e95
純粋なコードの途中で I/O が必要になった場合、小さなワークフローに分割できないか検討する
永続化を端に追いやる#66b586eb75d04f000003f283
hr.icon
請求書を支払うロジックの例
処理フロー
1. DB から請求書をロード
2. 支払いをする
3.
完全に支払いに成功した場合
DB で「完全に支払われた」ことをマークし、「請求書が支払われた」イベントを作成する
それ以外
DB で「部分的に支払われた」ことをマークし、イベントは作成しない
F# での実装例
code:fsharp
let paymentInvoice invoiceId payment =
// 1.
let invoice = loadInvoiceFromDatabase invoiceId
// 2.
invoice.ApplyPayment payment
// 3.
if invoice.IsFullyPaid then
markAsFullyPaidInDb invoiceId
postInvoicePaidEvent invoiceId
else
markAsPartiallyPaidInDb invoiceId
問題点
純粋でないため、テストするのが難しい
改善案
1. 純粋なビジネスロジックを関数に切り出す
code:fsharp
let applyPaymentToInvoice unpaidInvoice payment : InvoicePaymentResult =
let updatedInvoice = unpaidInvoice |> applyPayment payment
if isFullyPaid updatedInvoice then
FullyPaid
else
PartiallyPaid
テストが簡単に
2. 境界づけられたコンテキスト 上の境界線上で、1. の関数を コマンドハンドラ の一部として利用する
このとき、テストができるように I/O 処理を行う関数はすべて 依存関係としてパラメータで受け取るように する
DMMF: 依存関係の注入#66aa0ef275d04f0000588695
code:fsharp
type PayInvoiceCommand =
{ InvoiceId: ...
Payment: ... }
let paymentInvoice
paymentInvoiceCommand =
// DB から読み込む (I/O)
let invoiceId = paymentInvoiceCommand.InvoiceId
let unpaidInvoice =
loadUnpaidInvoiceFromDatabase invoiceId
// 純粋関数の呼び出し
let payment = paymentInvoiceCommand.Payment
let paymentResult = applyPaymentToInvoice unpaidInvoice payment
// 結果を処理 (I/O)
match paymentResult with
| FullyPaid -> markAsFullyPaidInDb invoiceId
| PartiallyPaid -> markAsPartiallyPaidInDb invoiceId
永続化ロジックは単純なので、この関数をテストする必要は無い
必要なら 3. へ
統合テストでカバーしたほうが良い
I/O が端に、ビジネスロジックが中心に来る構造になっている
https://scrapbox.io/files/66b585b279cd47001d8158fa.png
3. (任意: テストがしたい場合)I/O 処理を行う関数をすべて依存関係としてパラメータで受け取るようにする
code:fsharp
let paymentInvoice
loadUnpaidInvoiceFromDatabase // 依存関係
markAsFullyPaidInDb // 依存関係
markAsPartiallyPaidInDb // 依存関係
paymentInvoiceCommand = // コマンド
クエリに基づく意思決定
「純粋」なコードの途中で、データベースからの読み込みに基づいて処理を分岐したいケースもある
この場合、不純な I/O 関数の間に挟み込む選択が取ることもできる
https://scrapbox.io/files/66b58756091b99001cf3c3a2.png
しかし、長時間稼働するワークフロー で見たように ワークフロー を 小さいワークフローに分割することで、I/O が端にある状況を維持することが可能である
リポジトリパターンは?
エリック・エヴァンスのドメイン駆動設計 では、DB にアクセスするためのパターンとして リポジトリパターン を紹介している
第 6 章 に記載 radish-miyazaki.icon
FP の場合、このパターンを採用する必要はない
理由
リポジトリパターンは、ミュータブルを前提とした OOP において、永続化 を隠蔽するためのパターン
すべてを関数として モデリング し、永続化を端に追いやる ともはや隠蔽する必要がない
必要な関数だけをパラメータで受け取って使用できるので、保守しやすい