TL;DR
- ワークフローの各ステップを、依存関係、入力、出力を並べた関数の型で表す。
- 依存関係は関数として扱い、その型シグネチャが後で実装するインタフェースになる。
- ワークフローが返すイベントは、選択型のリストで表す。
内容
Modeling Workflows as Pipelines の一節である。ステートマシンの考え方を使って、注文確定ワークフローの各ステップをモデリングする。
検証のステップ
substep "ValidateOrder" =
input: UnvalidatedOrder
output: ValidatedOrder OR ValidationError
dependencies: CheckProduceCodeExists, CheckAddressExistsこのステップには、入力に加えて 2 つの依存関係がある。製品コードが存在するかを調べる関数と、住所が存在するかを調べる関数である。
このような依存関係は、関数(サービス)として扱う。関数の型シグネチャは後で実装するインタフェースになるので、実装を差し替えられる。
type CheckProductCodeExists = ProductCode -> bool
type CheckedAddress = CheckedAddress of UnvalidatedAddress
type AddressValidationError = AddressValidationError of string
type CheckAddressExists =
UnvalidatedAddress -> Result<CheckedAddress, AddressValidationError>依存関係を定義できたので、検証のステップは次のように書ける。入出力の型は定義済みとする(ワークフローの入力)。
type ValidateOrder =
CheckProductCodeExists // 依存関係
-> CheckAddressExists // 依存関係
-> UnvalidatedOrder // 入力
-> Result<ValidatedOrder, ValidationError> // 出力依存関係を先頭に置くのは、部分適用をしやすくするためである。CheckAddressExists が Result を返すので、この関数全体の戻り値も Result にする必要がある。
価格設定のステップ
substep "PriceOrder" =
input: ValidatedOrder
output: PricedOrder
dependencies: GetProductPriceこのステップには、製品コードを受け取って価格を返す依存関係が 1 つある。
type GetProductPrice =
ProductCode -> Price
type PriceOrder =
GetProductPrice // 依存関係
-> ValidatedOrder // 入力
-> PricedOrder // 出力注文確認のステップ
このステップでは、確認書を作って顧客に送る。確認書には、メールで送る HTML 文字列を含める。HTML 文字列も単純型としてモデリングする。
type HtmlString = HtmlString of string
type OrderAcknowledgment = {
EmailAddress: EmailAddress
Letter: HtmlString
}Letter は、顧客情報と注文内容をもとにテンプレートのようなものから作られる。これも依存関係として扱う。
type CreateOrderAcknowledgmentLetter = PricedOrder -> HtmlString確認書を送る処理も依存関係として扱う。具体的な実装は考えず、必要なインタフェースに集中する。次の 3 つは良くない例である。
OrderAcknowledgment -> unit: 呼び出し元から、送られたかどうかを判別できない。OrderAcknowledgment -> bool:boolは情報量がとても少ないので避けるべきである。OrderAcknowledgment -> OrderAcknowledgmentSent option:OrderAcknowledgmentSentはドメインイベントなので、その型を通じてドメインとサービスが結合してしまう。
代わりに、送った(Sent)か送っていない(NotSent)かを表す選択型を使う。
type SendResult = Sent | NotSent
type SendOrderAcknowledgment =
OrderAcknowledgment -> SendResult出力としては、「送信した」イベントを返す。確認書が送られない可能性があるので、出力の型は option にする。
type OrderAcknowledgmentSent = {
OrderId: OrderId
EmailAddress: EmailAddress
}
type AcknowledgeOrder =
CreateOrderAcknowledgmentLetter // 依存関係
-> SendOrderAcknowledgment // 依存関係
-> PricedOrder // 入力
-> OrderAcknowledgmentSent option // 出力イベントの作成
ワークフローは、OrderAcknowledgmentSent のほかに OrderPlaced と BillableOrderPlaced のイベントも返す必要がある。
workflow: "Place order" =
input: UnvalidatedOrder
output (on successs):
OrderAcknowledgmentSent
AND OrderPlaced (to send to shipping)
AND BillableOrderPlaced (to send to billing)
output (on error):
ValidationErrorOrderPlaced(注文が確定したイベント)は発送用で、PricedOrder の別名にする。BillableOrderPlaced(請求可能な注文が確定したイベント)は請求用で、PricedOrder の一部のフィールドだけを持つ。
type OrderPlaced = PricedOrder
type BillableOrderPlaced = {
OrderId: OrderId
BillingAddress: Address
AmountToBill: BillingAmount
}返すイベントは、レコード型でまとめることもできる。
type PlaceOrderResult = {
OrderPlaced: OrderPlaced
BillableOrderPlaced: BillableOrderPlaced
OrderAcknowledgmentSent: OrderAcknowledgmentSent option
}しかしこの形では、ワークフローに新しいイベントを足すときなどに更新が難しくなる。そこで、ワークフローがイベントのリストを返すようにし、各要素を選択型で表す。こうすればワークフロー全体を壊さずに更新できる。受注ドメインのすべてのイベントをまとめた OrderTakingDomainEvent のような、より一般的な型を作ることもできる。
type PlaceOrderEvent =
| OrderPlaced of OrderPlaced
| BillableOrderPlaced of BillableOrderPlaced
| OrderAcknowledgmentSent of OrderAcknowledgmentSent
type CreateEvents =
PricedOrder -> PlaceOrderEvent listキーワード
自分の理解
返すイベントをレコード型でまとめると、イベントを足すたびに型を書き換えることになり、オープン・クローズドの原則に反すると考えた。