TL;DR
内容
Modeling Workflows as Pipelines の一節である。
次のワークフローでは、注文(Order)がさまざまな状態に遷移する。
workflow: "Place order" =
input: OrderForm
output:
OrderPlaced event (put on a pile to send to other teams)
OR InvalidOrder (put on appropriate pile)
// ステップ 1
do ValidateOrder
If order is invalid then:
add InvalidOrder to pile
stop
// ステップ 2
do PriceOrder
// ステップ 3
do SendAcknowledgmentToCustomer
// ステップ 4
return OrderPlaced event (if no errors)注文は、未検証、検証済み、価格計算済み、と状態を変えていく。これをどうモデリングするかを考える。
単一のレコード型で表す
1 つのレコード型に、状態を表すフラグを持たせる方法がある。
type Order = {
...
IsValidated: bool // 検証時後に true
IsPriced: bool // 価格計算後に true
AmountToBill: float option // 価格計算後に設定
}この方法には 3 つの問題がある。
- 状態が暗黙的なので、処理するのに多くの条件分岐が必要になる。
- 状態によって必要なフィールドが異なるので、1 つのレコード型で表すと複雑になる。フィールドを
optionにして省略可能にしなければならない。 - どのフィールドがどのフラグに対応するのかが明確でない。
IsPricedが設定されていればAmountToBillも設定されている必要があるが、このルールは強制できず、コメントで促すしかない。
状態ごとに型を作る
状態ごとに新しい型を作る方法がよい。既存のコードを壊さずに新しい状態を追加できるので、オープン・クローズドの原則にも沿う。
// 検証済みの注文
type ValidatedOrder =
{ OrderId: OrderId
CustomerInfo: CustomerInfo
ShippingAddress: Address
BillingAddress: Address
OrderLines: ValidatedOrderLine list }
// 価格計算済みの注文
type PricedOrder =
{ OrderId: OrderId
CustomerInfo: CustomerInfo
ShippingAddress: Address
BillingAddress: Address
OrderLines: ValidatedOrderLine list
AmountToBill: BillingAmount }取りうる状態の型をすべて作ったら、そこから 1 つを選ぶトップレベルの型を作れる。
type Order =
| Unvalidated of UnvalidatedOrder
| Validated of ValidatedOrder
| Priced of PricedOrderこの型は、注文のライフサイクルのどの時点の状態でも表現できる。ストレージに永続化したり、他のコンテキストとやり取りしたりするのに使える。
キーワード
自分の理解
状態ごとに型を作るのは、言われてみれば当然のやり方だと感じた。