関数型ドメインモデリング』勉強会 第5章 型によるドメインモデリング

124
関数型ドメインモデリング』勉強会 第5章 型によるドメインモデリング

本章では、F#の型システムを使用したドメインモデルの表現方法を学ぶ。これにより、開発者でなくても理解できるようになる(はず)。

5.1 ドメインモデルの見直し

下記のような型が必要となる。

  • WidgetCode のような単純型
  • 製品コードのような、単純型の選択肢型
  • 未検証注文のような、レコード型
  • ワークフローのような関数型
  • 出力にエラー含むようなケース (Option型)

5.2 ドメインモデルのパターンを見る

下記のようなパターンが繰り返し発生する。

  • 単純な値 (Primitive)
  • AND による値の組み合わせ(レコード型; 構造体)
  • OR による選択肢(選択型; 判別共用体(Discriminated Union))
  • ワークフロー (インプットとアウトプットを持つビジネスプロセス)

5.3 単純な値のモデリング

OrderId と ProductCode は、どちらも int で表されているからといって、互換性があるわけではない。型が異なることを明確にするために、単一ケースの共用体で「ラッパー型」を作る。

type CustomerId = CustomerId of int

通常、共用体のケースラベル(右辺)はその型の名前(左辺)と同じにする。

単純型を作ることで、異なる型を混同することがなくなる(混同するとコンパイラエラーになるので)。

構築: ケースラベルをコンストラクタ関数として使用

let customerId = CustomerId 42

分解: ケースラベルによるパターンマッチ

let (CustomerId innerValue) = customerId

単純型による値の制約については、6章で後述。あと、プリミティブをラップしていることによるパフォーマンス問題についても書いてある。

5.4 複雑なデータのモデリング

5.4.1 レコード型によるモデリング

構造体ですね。

type Order = {
  CustomerInfo : CustomerInfo
  ShippingAddress : ShippingAddress
  BillingAddress : BillingAddress
  OrderLines : OrderLine list
  AmountToBill : ...
  }

各フィールドに名前(:の左辺)と 型(:の右辺)を与えている。

5.4.2 未知の型のモデリング

現時点では、CustomerInfoOrderLine といった型がどのようなものか、詳細は不明である。未知の型は、とりあえずプレースホルダとして未定義型にしておいてもよい。

F#で未定義の型を表現したい場合は、例外型の exn を使い Undefinedという別名を定義するとよい。(exn型は、.NETでいうSystem.Exceptionクラスの省略記法)

type Undefined = exn

次のように使える。

type CustomerInfo = Undefined
type ShippingAddress = Undefined
...

5.4.3 選択型(共用体)によるモデリング

下は2章で出てきたテキストベースの形式化。

data ProductCode =
  WidgetCode
  OR GizmoCode

これは、F#の型では以下のように表現される。

type ProductCode =
  | Widget of WidgetCode
  | Gizmo of GizmoCode

ケースごとにタグof の前; ケースラベルとも言う)とデータ型of の後)を定義する必要がある。

ここまでは、ユビキタス言語の「名詞」のモデル化。

5.5 関数によるワークフローのモデリング

ここでは、ユビキタス言語の「動詞」のモデル化を行う。

動詞、すなわち入出力のあるワークフローは、関数型としてモデリングする。

type ValidateOrder = UnvalidatedOrder -> ValidatedOrder

これは UnvalidatedOrder(未検証の注文)を入力とし、ValidatedOrder(検証済みの注文)を出力とするワークフローを関数として表現したときの型である。これはそのままワークフローのドキュメントになっている。

5.5.1 複雑な入力と出力の処理

複雑な、と言っても入力や出力に、レコード型とか共用体を使うようなケースについて書いてある。

ただし、後述のパイプラインラインを使う場合は、あえて複数の引数を持つ関数を使用して、パイプラインラインの前段から渡される引数以外は外部からの「依存関係の注入」を用いることもある。

5.5.2 関数のシグネチャでエフェクトを文書化する

エフェクトとは、副作用のこと。

関数がエラーを返す可能性がある場合

4.6.2 で説明した Result型を用いるとよい。

type Result<'Success,'Failure> =
  | Ok of 'Success
  | Error of 'Failure

プロセスが非同期である場合

Async という型を使って、関数が「非同期エフェクト」を持つことを明示する。

type ValidateOrder =
  UnvalidatedOrder -> Async<Result<ValidatedOrder,ValidationError list>>

5.6, 5.7 アイデンティティの考察

値オブジェクトの等値性やエンティティの同一性についての議論。

値オブジェクトは、それに含まれるすべてのフィールドの値が等しければ等値として扱い、等値な値オブジェクトは互いに区別されない。

エンティティは、識別子を持たせて、それによって同一性を判定する。エンティティは、通常、変更不可(immutable)として扱い、中の値を変更したい場合は、その値だけが異なっているコピーを作成する。

5.8 集約

集約とは、ドメインオブジェクトのコレクションをまとめあげるもの。

  • 集約ルート: 集約内のオブジェクトは、集約ルートと呼ばれるトップレベルエンティティを起点として参照され、オブジェクトに対する変更は、集約ルートを起点とする
  • 整合性境界: 集約内のすべてのデータが同時に正しく更新されることを保証する整合性の境界として機能する
  • 処理の単位: 永続化、トランザクション、データ転送におけるアトミックな処理単位
  • 不変条件の適用: ビジネスルールに違反するような処理を行うと、集約がエラーを発生させる

5.10 まとめ

関数型による設計はコードそのものである!