TL;DR
- 未検証の型からドメイン型を作るときは、フィールドごとにヘルパ関数を実装する。
- パイプラインで型が合わないときは、関数を別の形の関数に変換する関数アダプタを作って解決する。
内容
検証のステップ
Domain Modeling Made Functional 第7章 型を使ったワークフローの各ステップのモデリングで定義した最初のステップ「検証」を実装する。型の定義は次のとおりである。ここではエフェクトを意図的に取り除いている。
// 依存関係
type CheckProductCodeExists = ProductCode -> bool
type CheckAddressExists = UnvalidatedAddress -> CheckedAddress
// 検証ステップ
type ValidateOrder =
CheckProductCodeExists // 依存関係
-> CheckAddressExists // 依存関係
-> UnvalidatedOrder // 入力
-> ValidatedOrder // 出力実装の流れは単純である。未検証の注文の OrderId 文字列から OrderId ドメイン型を作る。同じように、CustomerInfo フィールドから CustomerInfo ドメイン型を、ShippingAddress フィールドから Address ドメイン型を作る(入力の型はDomain Modeling Made Functional 第7章 ワークフローの入力とDomain Modeling Made Functional 第7章 状態の集合による注文のモデリングを参照)。すべてのフィールドがそろったら、ValidatedOrder レコードを作って返す。
let validateOrder: ValidateOrder =
fun checkProductCodeExists checkAddressExists unvalidatedOrder ->
let orderId =
unvalidatedOrder.OrderId
|> OrderId.create
let customerInfo =
unvalidatedOrder.CustomerInfo
|> toCustomerInfo
let shippingAddress =
unvalidatedOrder.ShippingAddress
|> toAddress checkAddressExists
// unvalidatedOrder の各プロパティに対して同様の処理を行う
let billingAddress = ...
let lines = ...
{
OrderId = orderId
CustomerInfo = customerInfo
ShippingAddress = shippingAddress
BillingAddress = billingAddress
OrderLines = lines
}ヘルパ関数の実装
toCustomerInfo と toAddress は、未検証の型からドメイン型を作るヘルパ関数である。制約を満たさない値が来たらエラーを発生させる。
toCustomerInfo は簡単で、各フィールドについて対応するフィールドを見つけ、ドメイン型に変換するだけである。
let toCustomerInfo (customer: UnvalidatedCustomerInfo) : CustomerInfo =
// 無効な場合は例外を投げる
let firstName = customer.FirstName |> String50.create
let lastName = customer.LastName |> String50.create
let emailAddress = customer.EmailAddress |> EmailAddress.create
let name: PersonalName = { FirstName = firstName; LastName = lastName }
let customerInfo: CustomerInfo = { Name = name; EmailAddress = emailAddress }
customerInfotoAddress は少し複雑である。プリミティブ型をドメイン型に変換するだけでなく、CheckAddressExists サービスで住所が存在するかを確かめる必要がある。String50.createOption は、入力が null や空文字なら None を返す。
let toAddress (checkAddressExists: CheckAddressExists) unvalidatedAddress =
// サービスを呼び出し、内部値を取り出す
let (CheckedAddress checkedAddress) = checkAddressExists unvalidatedAddress
let addressLine1 = checkedAddress.AddressLine1 |> String50.create
let addressLine2 = checkedAddress.AddressLine2 |> String50.createOption
let addressLine3 = checkedAddress.AddressLine3 |> String50.createOption
let addressLine4 = checkedAddress.AddressLine4 |> String50.createOption
let city = checkedAddress.City |> String50.create
let zipCode = checkedAddress.ZipCode |> String50.create
let address: Address = {
AddressLine1 = addressLine1
AddressLine2 = addressLine2
AddressLine3 = addressLine3
AddressLine4 = addressLine4
City = city
ZipCode = zipCode
}
address明細行の作成
まず、1 つの UnvalidatedOrderLine を ValidatedOrderLine に変換するヘルパ関数 toValidatedOrderLine を実装する。
let toValidatedOrderLine checkProductCodeExists (unvalidatedOrderLine: UnvalidatedOrderLine) =
let orderLineId = unvalidatedOrderLine.OrderLineId |> OrderLineId.create
let productCode = unvalidatedOrderLine.ProductCode |> toProductCode checkProductCodeExists
let quantity = unvalidatedOrderLine.Quantity |> toOrderQuantity productCode
{ OrderLineId = orderLineId; ProductCode = productCode; Quantity = quantity }ここで新しいヘルパ関数が 2 つ必要になる。toOrderQuantity は、製品コードの種類に応じて数量を変換する。最後に OrderQuantity 型に型を持ち上げるのは、2 つの分岐の戻り値の型をそろえるためである。
let toOrderQuantity productCode quantity =
match productCode with
| Widget _ ->
quantity
|> int // float を int に変換
|> UnitQuantity.create // ユニット数に変換
|> OrderQuantity.Unit // OrderQuantity 型に持ち上げる
| Gizmo _ ->
quantity
|> KilogramQuantity.create // キログラム量に変換
|> OrderQuantity.Kilogram // OrderQuantity 型に持ち上げるtoProductCode をパイプラインで素直に書くと、ProductCode を返したいのに、checkProductCodeExists が bool を返してしまう。この問題は次の節の関数アダプタで解決する。
let toProductCode (checkProductCodeExists: CheckProductCodeExists) productCode =
productCode
|> ProductCode.create
|> checkProductCodeExists // bool 値を返してしまうtoValidatedOrderLine と List.map を組み合わせると、明細行のリストを一度に変換できる。
let orderLines =
unvalidatedOrder.Lines
|> List.map (toValidatedOrderLine checkProductCodeExists)関数アダプタの作成
関数アダプタは、元の関数を入力に取り、パイプラインで使いやすい「形」の新しい関数を返す関数である。List.map も関数アダプタの一種で、'a -> 'b の関数を 'a list -> 'b list の関数に変換する。
toProductCode の問題を解くには、述語関数を「条件を満たせば入力をそのまま返し、満たさなければ例外を投げる」関数に変換すればよい。
let convertToPassthru checkProductCodeExists productCode =
if checkProductCodeExists productCode then
productCode
else
failwith "Invalid Product Code"この関数のシグネチャは、ジェネリックな ('a -> bool) -> 'a -> 'a と推論される。そこで、より汎用的な形に書き直す。エラーメッセージを最初の引数にしたのは、部分適用でメッセージを組み込めるようにするためである。
let predicateToPassthru errorMsg f x =
if f x then x else failwith errorMsgこれを toProductCode に適用すると、パイプラインが ProductCode を返すようになる。
let toProductCode (checkProductCodeExists: CheckProductCodeExists) productCode =
let checkProduct productCode =
let errorMsg = sprintf "Invalid: %A" productCode
predicateToPassthru errorMsg checkProductCodeExists productCode
productCode
|> ProductCode.create
|> checkProduct