TL;DR
- 価格設定、注文確認、イベント作成のステップを、Domain Modeling Made Functional 第9章 最初のステップの実装と同じくヘルパ関数を組み合わせて実装する。
- 型の異なるイベントは、共通の選択型、さらにリストへと型を持ち上げることで 1 つの戻り値にまとめる。
- 未実装のステップは「未実装」の例外を投げておけば、コンパイルを通せる。
内容
価格設定のステップ
Domain Modeling Made Functional 第7章 型を使ったワークフローの各ステップのモデリングで定義した型は次のとおりである。ここでもエフェクトは取り除いている。
type GetProductPrice = ProductCode -> Price
type PriceOrder =
GetProductPrice // 依存関係
-> ValidatedOrder // 入力
-> PricedOrder // 出力実装では、明細行ごとに価格を付け、その合計を請求総額にする。
let priceOrder: PriceOrder =
fun getProductPrice validatedOrder ->
let lines =
validatedOrder.Lines
|> List.map (toPricedOrderLine getProductPrice)
let amountToBill =
lines
|> List.map (fun line -> line.LinePrice) // 価格のリストを作る
|> BillingAmount.sumPrices // 合計して請求総額にする
let pricedOrder: PricedOrder = {
OrderId = validatedOrder.OrderId
CustomerInfo = validatedOrder.CustomerInfo
ShippingAddress = validatedOrder.ShippingAddress
BillingAddress = validatedOrder.BillingAddress
Lines = lines
AmountToBill = amountToBill
}
pricedOrderBillingAmount.sumPrices は、Price のリストを合計し、BillingAmount 型に包んで返す。合計が範囲外なら例外を発生させる。合計を Price ではなく BillingAmount 型にするのは、Price とは異なる検証ルールを持つ可能性があるからである。
let sumPrices prices =
let total = prices |> List.map Price.value |> List.sum
create totaltoPricedOrderLine は 1 行だけを変換する。数量と単価を掛けるために、Price.multiply というヘルパ関数も用意する。
let toPricedOrderLine getProductPrice (line: ValidatedOrderLine) : PricedOrderLine =
let qty = line.Quantity |> OrderQuantity.value
let price = line.ProductCode |> getProductPrice
let linePrice = price |> Price.multiply qty
{
OrderLineId = line.OrderLineId
ProductCode = line.ProductCode
Quantity = line.Quantity
LinePrice = linePrice
}
// Price.multiply
let multiply qty (Price p) = create (qty * p)注文確認のステップ
定義した型は次のとおりである。
type HtmlString = HtmlString of string
type CreateOrderAcknowledgmentLetter = PricedOrder -> HtmlString
type OrderAcknowledgment = { EmailAddress: EmailAddress; Letter: HtmlString }
type SendResult = Sent | NotSent
type SendOrderAcknowledgment = OrderAcknowledgment -> SendResult
type OrderAcknowledgmentSent = { OrderId: OrderId; EmailAddress: EmailAddress }
type AcknowledgeOrder =
CreateOrderAcknowledgmentLetter // 依存関係
-> SendOrderAcknowledgment // 依存関係
-> PricedOrder // 入力
-> OrderAcknowledgmentSent option // 出力確認書を作って送り、送信できたらイベントを Some で、できなければ None を返す。依存関係である sendOrderAcknowledgment の中身は後で考えればよい。それがなくても、コードの大部分はビルドして組み立てられる。
let acknowledgeOrder: AcknowledgeOrder =
fun createOrderAcknowledgmentLetter sendOrderAcknowledgment pricedOrder ->
let letter = createOrderAcknowledgmentLetter pricedOrder
let acknowledgment = {
EmailAddress = pricedOrder.CustomerInfo.EmailAddress
Letter = letter
}
match sendOrderAcknowledgment acknowledgment with
| Sent ->
let event = {
OrderId = pricedOrder.OrderId
EmailAddress = pricedOrder.CustomerInfo.EmailAddress
}
Some event
| NotSent -> Noneイベントの作成
イベントの型は次のとおりである。
type OrderPlaced = PricedOrder
type BillableOrderPlaced = {
OrderId: OrderId
BillingAddress: Address
AmountToBill: BillingAmount
}
type PlaceOrderEvent =
| OrderPlaced of OrderPlaced
| BillableOrderPlaced of BillableOrderPlaced
| OrderAcknowledgmentSent of OrderAcknowledgmentSentここに「請求額が 0 より大きいときだけ請求イベントを送る」という仕様を足す。すると CreateEvents の定義は次のようになる。
type CreateEvents =
PricedOrder
-> OrderAcknowledgmentSent option
-> PlaceOrderEvent listBillableOrderPlaced イベントを作るヘルパ関数 createBillingEvent は、イベントを送らない場合もあるので option を返す。
let createBillingEvent (pricedOrder: PricedOrder) : BillableOrderPlaced option =
let billingAmount = pricedOrder.AmountToBill |> BillingAmount.value
if billingAmount > 0M then
Some {
OrderId = pricedOrder.OrderId
BillingAddress = pricedOrder.BillingAddress
AmountToBill = pricedOrder.AmountToBill
}
else
None戻り値を組み立てるには、Domain Modeling Made Functional 第8章 関数合成と同じく、すべての型を含む「最小」の型を考える。まず、すべてのイベントを選択型 PlaceOrderEvent に変換する。option の値は Option.map で変換する。次に、より一般的な型である list に変換する。そのために option を list に変換するヘルパ関数を用意する。
let listOfOption opt =
match opt with
| Some x -> [x]
| None -> []すべて同じ PlaceOrderEvent list 型になったので、1 つのリストに展開して返せる。
let createEvents: CreateEvents =
fun pricedOrder acknowledgmentEventOpt ->
let events1 =
pricedOrder
|> PlaceOrderEvent.OrderPlaced // 共通の選択型に変換する
|> List.singleton // リストに変換する
let events2 =
acknowledgmentEventOpt
|> Option.map PlaceOrderEvent.OrderAcknowledgmentSent
|> listOfOption
let events3 =
pricedOrder
|> createBillingEvent
|> Option.map PlaceOrderEvent.BillableOrderPlaced
|> listOfOption
[
yield! events1
yield! events2
yield! events3
]未実装のステップがあってもコンパイルを通す
パイプラインに未実装のステップが多くても、プロジェクトのコンパイルは通したい。その場合は「未実装」の例外を発生させておけばよい。
let priceOrder: PriceOrder =
fun getProductPrice validatedOrder ->
failwith "not implemented"キーワード
自分の理解
イベントを list に持ち上げるのは、CreateEvents の戻り値が list だからだと理解しているが、まだ自信がない。