TL;DR

  • 外部とデータをやり取りする処理は純粋でなく、テストしにくい。そのため、ワークフローの中では I/O や永続化を行わない。
  • ワークフローを、ビジネスロジックを含む中心部分と、I/O を扱う端の部分に分ける。この構成ならリポジトリパターンは要らない。
  • 純粋なコードの途中で I/O が必要になったら、小さなワークフローに分割できないかを考える。

内容

第12章 Persistenceのガイドラインの 1 つである。理想はすべての関数を純粋関数にすることだが、外部とデータをやり取りする処理は純粋でなく、テストしにくい。そこで、ワークフローを次の 2 つに分ける。

  • ビジネスロジックを含む、ドメインの中心部分
  • I/O 関連のコードを含む、端の部分

これはI/O を端に追いやると同じ考え方である。

請求書の支払いの例

次の処理を考える。

  1. DB から請求書を読み込む
  2. 支払いを行う
  3. 全額支払われたら、DB で「支払い済み」とマークし、「請求書が支払われた」イベントを送る。そうでなければ、DB で「一部支払い済み」とマークし、イベントは送らない

F# でそのまま書くと次のようになる。

let payInvoice invoiceId payment =
    // 1.
    let invoice = loadInvoiceFromDatabase invoiceId
 
    // 2.
    invoice.ApplyPayment payment
 
    // 3.
    if invoice.IsFullyPaid then
        markAsFullyPaidInDb invoiceId
        postInvoicePaidEvent invoiceId
    else
        markAsPartiallyPaidInDb invoiceId

この関数は純粋でないので、テストしにくい。

改善する

まず、純粋なビジネスロジックを関数に切り出す。これでテストが簡単になる。

let applyPaymentToInvoice unpaidInvoice payment : InvoicePaymentResult =
    let updatedInvoice = unpaidInvoice |> applyPayment payment
 
    if isFullyPaid updatedInvoice then
        FullyPaid
    else
        PartiallyPaid

次に、境界づけられたコンテキストの境界で、この関数をコマンドハンドラの一部として使う。

type PayInvoiceCommand =
    { InvoiceId: ...
      Payment: ... }
 
let payInvoice payInvoiceCommand =
    // DB から読み込む(I/O)
    let invoiceId = payInvoiceCommand.InvoiceId
    let unpaidInvoice =
        loadUnpaidInvoiceFromDatabase invoiceId
 
    // 純粋関数の呼び出し
    let payment = payInvoiceCommand.Payment
    let paymentResult = applyPaymentToInvoice unpaidInvoice payment
 
    // 結果を処理する(I/O)
    match paymentResult with
    | FullyPaid -> markAsFullyPaidInDb invoiceId
    | PartiallyPaid -> markAsPartiallyPaidInDb invoiceId

I/O が端に、ビジネスロジックが中心に来る構造になっている。永続化のロジックは単純なので、この関数を単体でテストする必要はなく、統合テストでカバーすればよい。

それでもテストしたい場合は、I/O を行う関数をすべて依存関係としてパラメータで受け取るようにする(依存関係の注入)。

let payInvoice
    loadUnpaidInvoiceFromDatabase // 依存関係
    markAsFullyPaidInDb           // 依存関係
    markAsPartiallyPaidInDb       // 依存関係
    payInvoiceCommand =           // コマンド
    ...

クエリの結果で分岐したいとき

純粋なコードの途中で、データベースから読み込んだ値に基づいて処理を分岐させたいこともある。純粋な関数を不純な I/O 関数で挟む構成も取れる。しかし、長時間稼働するワークフローで見たように、ワークフローを小さなワークフローに分割すれば、I/O が端にある状態を保てる。

リポジトリパターンは要るか

エリック・エヴァンスのドメイン駆動設計は、DB にアクセスするためのパターンとしてリポジトリパターンを紹介している。しかし、関数型プログラミングではこのパターンを採用する必要はない。理由は次のとおり。

  • リポジトリパターンは、ミュータブルな状態を前提としたオブジェクト指向プログラミングで、永続化を隠蔽するためのパターンである
  • すべてを関数としてモデリングし、永続化を端に追いやれば、もう隠蔽する必要はない
  • 必要な関数だけをパラメータで受け取って使えるので、保守しやすい

キーワード

自分の理解

リポジトリパターンは、エリック・エヴァンスのドメイン駆動設計の第 6 章に書かれている。