软糖

fp-ddd chapter 7

Domain Modeling Made Functional - Chapter 5

Modeling Workflows as Pipelines

Transformation-oriented programming

We’ll create a “pipeline” to represent the business process, which in turn will be built from a series of smaller “pipes.” Each smaller pipe will do one transformation, and then we’ll glue the smaller pipes together to make a bigger pipeline.

  • The Workflow Input

  • Commands as Input

    • The command should contain everything that the workflow needs to process the request

    • Sharing common structures using Generics

Modeling an Order as a Set of States

A much better way to model the domain is to create a new type for each state of the order. This allows us to eliminate implicit states and conditional fields.

1
2
3
4
typeOrder =
​ | UnvalidatedofUnvalidatedOrder
​ | ValidatedofValidatedOrder
​ | PricedofPricedOrder

State Machines

we could encode that requirement directly in the function signature, using the compiler to ensure that that business rule was complied with.

A much better approach is to make each state have its own type, which stores the data that is relevant to that state (if any).

1
2
3
4
5
6
7
8
​​typeItem = ...
​​typeActiveCartData = { UnpaidItems: Itemlist​ }
​​typePaidCartData = { PaidItems: Itemlist​; Payment: ​float​ }
​​typeShoppingCart =
​ | EmptyCart ​// no data
​ | ActiveCartofActiveCartData
​ | PaidCartofPaidCartData

A command handler is then represented by a function that accepts the entire state machine (the choice type) and returns a new version of it (the updated choice type).

Modeling Each Step in the Workflow with Types

We’ve been talking about modeling processes as functions with inputs and outputs. But how do we model these dependencies using types? Simple, we just treat them as functions, too. The type signature of the function will become the “interface” that we need to implement later.

1
2
3
typeCheckProductCodeExists =
ProductCode -> ​bool​
​// ^input ^output

We have put the dependencies first in the parameter order and the input type second to last, just before the output type. The reason for this is to make partial application easier (the functional equivalent of dependency injection).

Booleans are generally a bad choice in a design, though, because they are very uninformative. It would be better to use a simple Sent/NotSent choice type instead of a bool.

1
2
3
4
typeSendResult = Sent | NotSent
​​typeSendOrderAcknowledgment =
OrderAcknowledgment -> SendResult

In summary

1
2
3
4
5
6
7
8
9
10
11
// Event
typeOrderAcknowledgmentSent = {
OrderId : OrderId
EmailAddress : EmailAddress
}
typeAcknowledgeOrder =
CreateOrderAcknowledgmentLetter ​// dependency​
-> SendOrderAcknowledgment ​// dependency​
-> PricedOrder ​// input​
-> OrderAcknowledgmentSent option ​// output

Creating the events to return

the workflow returns a list of events, where an event can be one of OrderPlaced, BillableOrderPlaced, or OrderAcknowledgmentSent.

1
2
3
4
5
6
7
typePlaceOrderEvent =
​ | OrderPlacedofOrderPlaced
​ | BillableOrderPlacedofBillableOrderPlaced
​ | AcknowledgmentSentofOrderAcknowledgmentSent
​​typeCreateEvents =
PricedOrder -> PlaceOrderEvent ​list