最初のステップの実装
LT;DR
検証されていない型からドメイン型を作成する際には、ヘルパ関数を実装する
パイプラインを実装する際に発生する型の不一致は、 関数アダプタ を作成することで回避する 関数アダプタ は、List.map のようにある関数を別の関数に変換する関数である https://scrapbox.io/files/66a8b8f4dde555001c182782.png
hr.icon
検証のステップ
再掲
code:fsharp
// 依存関係
type CheckProductCodeExists = ProductCode -> bool
type CheckAddressExists = UnvalidatedAddress -> CheckedAddress
// 検証ステップ
type ValidateOrder =
CheckProductCodeExists // 依存関係
-> CheckAddressExists // 依存関係
-> UnvalidatedOrder // 入力
-> ValidatedOrder // 出力
実装は以下のようなフローになる
未検証の注文の OrderId 文字列を使って、OrderId ドメイン型を作成する
未検証の注文の CustomerInfo(UnvalidateCustomerInfo 型) フィールドを使って、CustomerInfo ドメイン型を作成する
未検証の注文の ShippingAddress (UnvalidatedAddress 型)フィールドを使って、Address ドメイン型を作成する
... 繰り返し ...
すべてのフィールドの準備ができたら、レコード(ValidatedOrder)を作成して返す
コード
code:fsharp
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
(プリミティブでない)任意の非ドメイン型を検証済みのドメイン型に変換するロジックは簡単
各フィールドについて、対応するフィールドを見つけてドメイン型に変換する
code:fsharp
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
}
customerInfo
toAddress
プリミティブ型をドメインオブジェクトに変換し、CheckAddressExists サービスを用いて住所が存在するかをチェックする必要があるので複雑
code:fsharp
let toAddress (checkAddressExists: CheckAddressExists) unvalidatedAddress =
// サービスを呼び出す
let checkedAddress = checkAddressExists unvalidatedAddress
// 内部値を抽出
let (CheckedAddress checkedAddress) = checkedAddress
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
String50.createOption
入力に null または 空文字を指定でき、その場合は None を返す
明細行(OrderLines)の作成
まず、1 つの UnvalidatedOrderLine を ValidatedOrderに変換するヘルパ関数を実装する
toValidatedOrderLine
code:fsharp
let toValidatedOrderLine checkProductCodeExists
(unvalidatedOrderLine: UnvalidatedOrderLine) =
let orderLineId =
unvalidatedOrderLine.OrderLineId
|> OrderLineId.create
let productCode =
unvalidatedOrderLine.ProductCode
|> toProductCode checkProductCodeExists
let quantity =
unvalidatedOrderLine.Quantity
|> toOrderQuantity productCode
let validatedOrderLine = {
OrderLineId = orderLineId
ProductCode = productCode
Quantity = quantity
}
validatedOrderLine
新しい 2 つのヘルパ関数
toOrderQuantity
code:fsharp
let toOrderQuantity productCode quantity =
match productCode with
| Widget _ ->
quantity
|> int // float を int に変換
|> UnitQuantity.create // ユニット数に変換
|> OrderQuantity.Unit // OrderQuantity 型に持ち上げる
| Gizmo _ ->
quantity
|> KilogramQuantity.create // キログラム量に変換
|> OrderQuantity.Kilogram // OrderQuantity 型に持ち上げる
OrderQuantity 型に 型を持ち上げる のは、戻り値の型を合致させるため toProductCode
パイプラインを用いて関数を書くと、以下のようになる
code:fsharp
let toProductCode (checkProductCodeExists: CheckProductCodeExists) productCode =
productCode
|> ProductCode.create
|> checkProductCodeExists // bool 値を返す
ProductCode を返すようにしたいが、bool を返してしまう
これと List.map を組み合わせて、UnvalidatedOrderLine list を ValidatedOrder list に一気に変換する
code:fsharp
let validateOrder: ValidateOrder =
fun checkProductCodeExists checkAddressExists unvalidatedOrder ->
let orderId = ...
let customerInfo = ...
let shippingAddress = ...
let orderLines =
unvalidatedOrder.Lines
|> List.map (toValidatedOrderLine checkAddressExists)
関数アダプタの作成
元の関数を入力とし、パイプラインで使用するのに適した「形」を持つ新しい関数
https://scrapbox.io/files/66a8b8f4dde555001c182782.png
List.map も関数アダプタの 1 種
'a -> 'b を 'a list -> 'b list に変換する
code:fsharp
let convertToPassthru checkProductCodeExists productCode =
if checkProductCodeExists productCode then
productCode
else
failwith "Invalid Product Code"
上記の関数のシグネチャはジェネリックと 型推論 される code:fsharp
val convertToPassthru :
checkProductCodeExists: ('a -> bool) -> productCode: 'a -> 'a
なので、より汎用的にする
code:fsharp
let predicateToPassthru errorMsg f x =
if f x then
x
else
failwith erroMsg
部分適用 でメッセージを組み込めるように 1 番目にしている toProductCode に適用する
code:fsharp
let toProductCode (checkProductCodeExists: CheckProductCodeExists) productCode =
let checkProduct productCode =
let errorMsg = sprintf "Invalid: %A" productCode
predicateToPassthru errorMsg checkProductCodeExists productCode
productCode
|> ProductCode.create
|> checkProduct