Open7
読書メモ「Domain Modeling Made Functional」
会社で購入してもらったので読む。
想定読者
- 型(type)と関数(functions)のみを使って、どのようにモデリング、および実装を行うかを知りたい人。
- DDD、オブジェクト指向設計、データ指向設計のそれぞれの違いを知りたい人。
- DDDは経験済みで、なぜDDDには関数型プログラミングが適しているのか、それをどう関数型プログラミングにフィットさせるかを知りたい人。
- 関数型プログラミングの概要、セオリーを知りたい人。
構成
- ドメインの理解
- モデリング
- 実装
ドメインエキスパートへのインタビュー
イベントストーミングで作った、コマンド/イベントのアプローチは、ドメインエキスパートへのインタビュー時間を短縮するのに役立つ。彼らも忙しいので。
「このワークフローを始めるのに、どんな情報が必要ですか?」と、1インタビューに1ワークフローのように集中することができる。
Documenting the Domain
ドメインの簡単な文書化。UMLとかでも書けるが、ワークフローを書くためにはそこまで詳細である必要は無いので、簡単なものでいい。
ドメインエキスパートにインタビューしながら、以下のような簡易的なメモを取っていく。
ポイントは2つ。
- ワークフローの文書化について。input, outputとそれに関連するビジネスロジックをメモ。
- データ構造の文書化。両方とも必須ならAND、いずれか一つ必須ならORで。
サンプル
# ワークフロー
- Bounded context: 注文の取得
- Workflow: 注文する
- triggered by:
"注文フォームの受信"イベント("見積書"がチェックされていない場合)
- primary input:
注文フォーム (An order form)
- other input:
製品カタログ (Product catalog)
- output events:
"発注"イベント
- side-effects:
受領書は発注と同時に顧客に送信される
# データ構造
- Bounded context: 注文の取得
- data Order =
CustomerInfo
AND ShippingAddress
AND BillingAddress
AND list of OrderLines
AND AmountToBill
- data OrderLine =
Product
AND Quantity
AND Price
- data CustomerInfo = ??? // don't know yet
- data BillingAddress = ??? // don't know yet
ドメインの状態遷移を型で表現する
状態を型で表現することのメリットは、「状態の追加」が容易になること。
既存のコードを壊さずに、型を追加するだけで解決する。
// Orderの状態遷移を型で表現
// Unvalidated Order -> Validated Order -> Priced Order
type Order interface {
UnvalidatedOrder | ValidatedOrder | PricedOrder
}
type (
UnvalidatedOrderLine any // undefined yet
ValidatedOrderLine any // undefined yet
PricedOrderLine any // undefined yet
UnvalidatedOrder struct {
OrderID OrderID
CustomerInfo CustomerInfo
ShippingAddress ShippingAddress
BillingAddress BillingAddress
OrderLines []UnvalidatedOrderLine
}
ValidatedOrder struct {
OrderID OrderID
CustomerInfo CustomerInfo
ShippingAddress ShippingAddress
BillingAddress BillingAddress
OrderLines []ValidatedOrderLine
}
PricedOrder struct {
OrderID OrderID
CustomerInfo CustomerInfo
ShippingAddress ShippingAddress
BillingAddress BillingAddress
OrderLines []PricedOrderLine
AmountToBill BillingAmount
}
)
interfaceでgenericsでUNION型を表現するけど、実際これ要らなそう
type ShoppingCart interface {
EmptyCart | ActiveCart | PaidCart
}
type (
EmptyCart struct{
CartID int
}
ActiveCart struct {
CartID int
UnpaidItems []int
}
PaidCart struct {
CartID int
PaidItems []int
Payment int
}
)
// ショッピングカートに商品を追加するメソッド
func (c EmptyCart) Add(items []int) ActiveCart {
return ActiveCart{
CartID: c.CartID,
UnpaidItems: items,
}
}
func (c ActiveCart) Add(items []int) ActiveCart {
newItems := append(c.UnpaidItems, items...)
return ActiveCart{
CartID: c.CartID,
UnpaidItems: newItems,
}
}
外部依存の機能も「関数の一つ」として、それを関数のInputにすると考えることができる。
ValidateOrder =
CheckProductCodeExists // dependency
-> CheckAddressExists // dependency
-> UnvalidatedOrder // input
-> Result<ValidatedOrder, ValidationError> // output