『Domain Modeling Made Functional』を読んで、ドメインのモデリングに関するメモ
関数の理解
関数は入力と出力を持つ一種のブラックボックス
変換トンネルに何かが入って、何らかの形で変形して、出てくる(とてもシンプル)
apple -> Banana
という記述(型シグネチャと呼ぶ)で表される
関数の入力または出力として使用できる値の名称を『型』と呼ぶ
int -> string(int型からstring型に変換する関数)
プリミティブ型じゃなくても、例えば、Person と呼ぶ一連のオブジェクトを操作する関数でもいい
Person -> 何かしらの出力
関数を出力する関数もあり
何かしらの入力 -> (apple -> banana)
OOPではほとんどのものを「オブジェクト」と呼ぶが、FPでは「値」と呼ぶ
「値」は「変数」じゃないので不変だが、「オブジェクト」はデータ構造とそれに関連する動作 (メソッド) をカプセル化したもので、状態をもって変更される可能性がある
型の構成
- 関数型プログラミングでは型をANDやORを使って組み合わせる(合成)
- すべての型を合成を使って構成することを代数型システムと呼ぶ
型を合成してドメインモデルを構築する
支払い請求 = 未払い請求書 -> 支払い -> 支払済み請求書
この時、「支払い」とは以下のフィールドを持つレコードとする
支払い =
金額 : お支払い金額
通貨: 通貨
方法: 支払い方法
この時、
・「お支払い金額」は10 進数の金額
・「通貨」は EUR か USD
・「支払い方法」は 現金 か カード
であるが、さらに金額とは?現金とか?カードとは?というようにドメインを探索し、型を作ってドメインモデルを構築する
任意の値
指定の型か値無しの型を返すOption型を用意する
この時、指定の型をジェネリックの型として渡せるようにしている(Option型はF#なら標準である)
type Option<指定の型> = [指定の型] OR [値なし]
使用例
PersonalName = { FirstName: string MiddleInitial: Option<string> LastName: string }
エラーの可能性がある場合
検証の成功と失敗を表すためにはResult型を用意する
この時、成功の場合の値の型と、失敗の場合の値の型を渡せるようにしておく
type Result<成功の型, 失敗の型> = [成功の型] OR [失敗の型]
使用例
type PayInvoice = UnpaidInvoice -> Payment -> Result<PaidInvoice,PaymentError>
PaymentError型のとりうるの値の型もいろいろある
・カードタイプが認識されません
・支払いが拒否されました
・決済プロバイダーオフライン
入力なしや、何も返さない場合
いわゆるvoidの話
出力値のvoidはDBへの保存時など、入力値のvoidは中で乱数を生成している関数など
これらは副作用があることを強く示しているのでなるべく回避する
ファイルとプロジェクト内の型の整理の仕方
すべてのドメインの型を 1 つのファイル ( Types.tsまたはDomain.tsなど) に配置し、それらに依存する関数をコンパイル順序の後ろに配置する
種類が多くて、複数のファイルに分割する必要がある場合は、Common.tsを最初に置き、サブドメイン固有のものを後に置く
- Common.Types.fs
- Common.Functions.fs
- MyContext.Types.fs
- MyContext.Functions.fs
ファイル内でも、依存関係の順に単純な型をまず先頭に置き、より複雑な型 (それに依存する型) をその下に置く方が良いが、トップダウンにした方が読みやすい場合もあるのでそのように書いても良い
ただ、設計が固まって、本番にあげる前には正しい依存関係の順序にしておくほうが良い
未知の型のモデリング
設計プロセスの初期段階では、モデリングに関するいくつかの質問に対して明確な答えが得られないことがよくある
ユビキタス言語のおかげで、モデル化する必要がある型の名前はわかるが、その内部構造はわからない場合
それはそれで構わないので、unkown型を駆使して未知の型としてモデル化しておき、後半で明確になったら置き換えるようにする
複数の入力がある場合
各入力値を個別のパラメータとして受け取るようにするか、
type CalculatePrices = OrderForm -> ProductCatalog -> PricedOrder
もしくは、二つを包括した型を用意して、それ1つを入力とするか、
type CalculatePricesInput = { OrderForm ProductCatalog } type CalculatePrices = CalculatePricesInput -> PricedOrder
もし、ProductCatalog が「実際の」入力ではなく依存関係である場合、別のパラメーターのアプローチを使用した方が良い(依存注入と同等の機能)
非同期の場合
非同期の場合は結果(Result)と非同期を表す型(Async)を使ってこう表現される
type ValidateOrder = UnvalidatedOrder -> Async<Result<ValidatedOrder,ValidationError list>>
毎回これを書くのは読みにくいので型エイリアスを作って
type ValidationResponse<指定の型> = Async<Result<指定の型, ValidationError list>>
こうする
type ValidateOrder = UnvalidatedOrder -> ValidationResponse<ValidatedOrder>
エンティティ
関数型プログラミング言語の値はデフォルトで不変で、定義された値(オブジェクト)はいずれも初期化後に変更できない
永続的な ID を持つエンティティはビジネスプロセスによって状態が変わるためオブジェクトのプロパティを更新したくなるが、関数型プログラミングの原則にのっとり更新するのではなく、変更されたデータを含むエンティティのコピーを作成することで不変性を保つ
集約
集約は一緒に更新すべきエンティティをまとめたもので、一貫性と不変性を強制する
永続性、データベーストランザクション、およびデータ転送の最小単位になる
ドメイン内の整合性と一貫性のとり方が重要になる
ドメイン内の整合性と一貫性のとり方
- 現実世界のドメインで無制限の整数や文字列が存在することは非常にまれで、型を「整数」や「文字列」と定義することを避け、文字列は特定の文字で始まるであったり、数字は 1~10000 の範囲であったりと制約を確認する
- F#の機能で「測定単位」というものがあり、
5.0<kg>
のような記述で、ただの5.0
ではなく「kg」であることを明示して他の単位との誤った計算を防ぐ - 型システムで不変条件を強制する
例えば、以下のような型を作って0件ではない少なくとも 1 つの要素が存在するリストを強制できるtype NonEmptyList<'a> = {
First: 'a
Rest: 'a list
} - 「顧客は電子メールまたは住所を持っている必要があります。」をモデリングすると以下になりがち
type Contact = {
Name: Name
Email: Option
Address: Option
} - そうではなく、持ちうる状況分の型を作る
・メールアドレスのみ
・郵便番号のみ
・電子メールアドレスと住所の両方type BothContactMethods = {
Email: EmailContactInfo
Address : PostalContactInfo
}
type ContactInfo = {
| EmailOnly of EmailContactInfo
| AddrOnly of PostalContactInfo
| EmailAndAddr of BothContactMethods
}
type Contact = {
Name: Name
ContactInfo : ContactInfo
}
ワークフローの入力
- ワークフローの実際の入力はドメインオブジェクトではなくコマンドである
- 1つのコンテキストの中で複数のワークフローがある場合、それぞれのコマンドを用意するのではなく、コマンドを1つの型に結合する
- コンテキストの外側に結合したコマンドを受けるコマンドハンドラーを設け、コマンドハンドラーが次に流すコマンドをディスパッチする
状態変化
おさらいになるが、オブジェクトの状態が変化するというのを関数型プログラミングでは、ある状態から別の状態への「遷移」が何らかのコマンドによってトリガーされると考える
この状態を遷移させる処理をステートマシンと呼ぶ
例えば、メールアドレスの「未検証」から「検証済み」への状態変化は、メールアドレスが「未検証」から「検証済み」になったと考えるのではなく、ステートマシンによって「未検証アドレス」から「検証済みアドレス」に遷移したと考える
これによる利点
- 各状態で許容される動作を変えることができる
- すべての状態が明示的に文書化される
- 起こり得るあらゆる可能性について考えることを強いることができる