Open7

読書メモ「Domain Modeling Made Functional」

masafumi330masafumi330

想定読者

  • 型(type)と関数(functions)のみを使って、どのようにモデリング、および実装を行うかを知りたい人。
  • DDD、オブジェクト指向設計、データ指向設計のそれぞれの違いを知りたい人。
  • DDDは経験済みで、なぜDDDには関数型プログラミングが適しているのか、それをどう関数型プログラミングにフィットさせるかを知りたい人。
  • 関数型プログラミングの概要、セオリーを知りたい人。

構成

  1. ドメインの理解
  2. モデリング
  3. 実装
masafumi330masafumi330

ドメインエキスパートへのインタビュー

イベントストーミングで作った、コマンド/イベントのアプローチは、ドメインエキスパートへのインタビュー時間を短縮するのに役立つ。彼らも忙しいので。

「このワークフローを始めるのに、どんな情報が必要ですか?」と、1インタビューに1ワークフローのように集中することができる。

masafumi330masafumi330

Documenting the Domain

ドメインの簡単な文書化。UMLとかでも書けるが、ワークフローを書くためにはそこまで詳細である必要は無いので、簡単なものでいい。
ドメインエキスパートにインタビューしながら、以下のような簡易的なメモを取っていく。

ポイントは2つ。

  1. ワークフローの文書化について。input, outputとそれに関連するビジネスロジックをメモ。
  2. データ構造の文書化。両方とも必須なら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
masafumi330masafumi330

ドメインの状態遷移を型で表現する

状態を型で表現することのメリットは、「状態の追加」が容易になること。
既存のコードを壊さずに、型を追加するだけで解決する。

// 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
	}
)
masafumi330masafumi330

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,
	}
}
masafumi330masafumi330

外部依存の機能も「関数の一つ」として、それを関数のInputにすると考えることができる。

ValidateOrder =
    CheckProductCodeExists // dependency
    -> CheckAddressExists  // dependency
    -> UnvalidatedOrder    // input
    -> Result<ValidatedOrder, ValidationError> // output