Posted on

『関数型ドメインモデリング』読書メモ

F#の環境を作る

source /etc/os-release
wget https://packages.microsoft.com/config/$ID/$VERSION_ID/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb
sudo apt update
sudo apt install dotnet-sdk-8.0
dotnet new sln -o DomainModelingMadeFunctional
cd DomainModelingMadeFunctional
dotnet new classlib -lang "F#" -o src/Library
dotnet sln add src/Library/Library.fsproj

dotnet new console -lang "F#" -o src/App
dotnet add src/App/App.fsproj reference src/Library/Library.fsproj
dotnet sln add src/App/App.fsproj

cd src/App
dotnet restore
dotnet run Hello World
Nice command-line arguments! Here's what System.Text.Json has to say about them:
Input: { args = [|"Hello"; "World"|] year = 2024 }
Output: {"args":["Hello","World"],"year":2024}
type CustomerId =
    | CustomerId of Int
// ↓
type CustomerId = CustomerId of Int

5章

  • エフェクトに主な出力以外を含める. ex. Result, Async
  • エンティティ
    • アイデンティティを持たない: 値オブジェクト
    • エンティティに対する等価性の実装
[<CustomEquality; NoComparison>]
// [<NoEquality; NoComparison>] で等価判定を未定義にする
type Contact = {
    ContactId : ContactId
    PhoneNumber : PhoneNumber
    EmailAddress: EmailAddress
    }
    with

    override this.Equals(obj) =
        match obj with
        | :? Contact as c -> this.ContactId = c.ContactId
        | _ -> false
    
    override this.GetHashCode() =
        hash this.ContactId

5.9

  • 境界付けられたコンテキストは名前空間で表現する

第8章 関数の理解

  • パイピング |> で関数を合成する
    • Rustだと pike というcrateがあるらしい

第9章 実装: パイプラインの合成

  • 依存(引数の関数)は部分適用させて単純な関数にしておく
  • 集約の最上位は「コンポジションルート」と呼ぶ

第10章 実装: エラーの取り扱い

  • エラー
    • ドメインエラー: ビジネスプロセスの一部として予測されるエラー
    • パニック: 不明なエラー
    • インフラストラクチャエラー: ドメイン外のエラー(ex. タイムアウト、認証失敗)

第11章 シリアライズ

11.5 ドメイン型をDTOに変換する方法

  • 単純型(ラッパ型): プリミティブ型
  • オプション型: None をnullに
  • レコード型:

12章 永続化

  • 関数型言語では無理にリポジトリパターンに結び付けなくても良い
    • 個別の関数として定義して個別に使えば良い

12.2 コマンドとクエリの分離

  • クエリ(read) はコマンド(insert, update, delete ...) と混在させるべきでない
    • クエリは副作用を持つべきでない
    • 副作用(=状態の更新)を持つ関数(コマンド)はデータを返すべきでない, Unit を返すべき
type DbError = ...
type DbResult<'a> = AsyncResult<'a, DbError>

type InsertData = Data -> DbResult<Unit>
type ReadData = Query -> DbResult<Data>
type UpdateData = Data -> DbResult<Unit>
type DeleteData = Key -> DbResult<Unit>
  • 書き込みのモデルと読み込みのモデルを分離する
    • → [[CQRS]]
      • イベントソーシングと関連するが割愛
  • 境界付けられた各コンテキストはデータストレージが「分離」されていなければいけない
    • DBが結合していると
    • 分離
      • 異なるDB、デプロイ単位も別 <-> 同じDB
    • 複数ドメインのデータを扱う時
      • レポーティングシステム自体を別ドメインとして扱い他コンテキストのデータをコピーして使う
        • ex. 他のシステムが発行するイベントを購読してBIシステムのデータストアに挿入
      • BI領域ではドメインモデルをちゃんと作るより柔軟なクエリ等をサポートする多次元DB(キューブ)を開発することが重要
  • DB
    • DocumentDB
      • モデルをDTOにしてそれをJSON等の形式にして保存
    • RDB
      • オブジェクトとDBにインピーダンスミスマッチがある
        • 関数型ではデータと関数が分離されるためRDBとの親和性がOOPより高い
      • 選択型の永続化
        • 基本的に単一テーブルにタグを持たせる
        • 共通カラムが少ないなら別テーブルでも良い
      • アイデンティティを持つなら別テーブル, 持たないならインラインに
  • トランザクション
    • サービス間のトランザクションは出来ないので代わりに不整合を修正するための補償処理をする
      • 補償トランザクション
        • [[Saga Pattern]] みたいな?

第13章 設計を進化させ、きれいに保つ

  • アクティブパターン
    • 分岐で enum のような値を返せる
  • 型を追加しワークフローのエラーを直せば安全にロジックを追加できる