本章では、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 未知の型のモデリング
現時点では、CustomerInfo
や OrderLine
といった型がどのようなものか、詳細は不明である。未知の型は、とりあえずプレースホルダとして未定義型にしておいてもよい。
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 まとめ
関数型による設計はコードそのものである!