関数型ドメインモデリング
共通のモデルを作成するガイドライン
- データ構造ではなく、ビジネスイベントやワークフローに焦点を当てる
- ドメインをより小さなサブドメインに分割
- 各サブドメインのモデルを解決空間に作成する
- ユビキタス言語を開発する
- PJに関わる全ての人が共有し、コードのあらゆる場所で使用される共通言語
ビジネスイベントによるドメイン理解
データ自体がビジネスを生み出すのではなく、それを変換したイベントによってビジネスが生み出される
- これを基準にすることで開発者とドメインエキスパートで共通の理解が築かれる
- 外部からのトリガー、イベントを用いて設計を表現することを「ドメインイベント」と呼ぶ
- ドメイン内で重要な出来事や状態の変化を表すオブジェクト
- システム内の特定のアクションや条件が満たされたときに発生し、他の部分にその変化を通知するために使用
- イベントは変更できない事実
- 外部からのトリガー、イベントを用いて設計を表現することを「ドメインイベント」と呼ぶ
イベントストリーミングによるドメインの探索
ドメインのイベントを如何に発見するか
- ビジネスイベントとそれに関連するワークフローを発見するための共同プロセスをイベントストリーミングを呼ぶ
- ドメインの理解者を集めたワークショップ
受注システム例)
🔸イベントを見つける(OOされた OOした)
- ドメインの理解者を集めたワークショップ
- 注文書を受け取った
- 注文を
- 確定した
- 発送した
- 変更を依頼された
- キャンセルを依頼された
- 見積書を~
何が目的か? - ビジネスの共通モデル(視点を揃える)
- 全チームの把握(他面で見る)
- 要件のギャップの発見
- チーム間の連携
- レポーティング要件
🔸コマンドの文書化
コマンドは、システムに対して「何をすべきか」を指示するリクエスト
- ドメインイベントを引きおこしたのは何か
- 上司が何かをして欲しいといった
- 客が注文書を受け取って欲しいといった
特定のアクションや操作を実行するための意図を表現する
オブジェクトコマンドはユーザーインターフェースや他のシステムから発行され、システムの状態を変化させる
コマンドはOOするという現在系
フローは以下
- コマンド
- 成功
- ワークフロー開始
- ドメインイベント
- コマンド(注文を確定する)
- ドメインイベント(注文が確定された)
ドメインをサブドメインに分割
境界づけられたコンテキストを利用した解決手段の作成
「問題空間(現実)」=>「解決空間(ドメインモデル)」
- Bounded Context(境界づけられたコンテキスト)
- ドメインモデルが適用される明確な境界
- 現実世界のドメインの境界は曖昧だが、ソフトウェアではシステムを疎結合にし独立して進化できるようにしたい
- ユビキタス言語の境界でもあり、ドメインの特定部分を定義
- 異なるコンテキスト間での混乱を避け、一貫性を保つ
- ドメインモデルが適用される明確な境界
- 受注コンテキスト
- 発注コンテキスト
- 請求コンテキスト
概念要約
ドメイン
解決しようとしている問題に関連する知識の領域
ドメインモデル
あるドメインにおいて、特定の問題に関連した側面を単純化して表現したものの集合
ドメインモデルは解決空間の一部であり、ドメインモデルが表現するドメインは問題空間の一部
ユビキタス言語
ドメインに関する概念と語彙の集合
チームメンバーとコードの両方で共有
境界づけられたコンテキスト
解決空間内のサブシステムであり、明確な境界線で他のサブシステムと区別
境界づけられたコンテキスト 概念編 - ドメイン駆動設計用語解説 [DDD]
コンテキストマップ
境界づけられたコンテキストの集合体とそれらの関係を示す図
ドメインイベント
システム内で起こったことの記録
常に過去形で記述し、イベントは新たな活動のトリガーになる
コマンド
何らかの処理をするための要求、人や他のイベントによってトリガーされる
プロセスが成功すると、システムの状態が変化し、1つ以上のドメインイベントが記録される
イベントストリーミング後に、、
データベース駆動設計をしたくなる => わかる
▶️ ユビキタス言語にDBは含まれない。ユーザーはデータがどのように永続化されるかについて気にしていない。DDDは永続性非依存の原則。DB内のデータの表現を気にすることなくドメインを正確にモデル化する。
書籍の例)
注文書と見積書
持つデータが似ているので、同じテーブルで管理し、フラグでどちらのDocumentかを管理する体制にしたくなるが、ワークフローや詳細のデータが異なってくる可能性が多分にあるのでいずれ崩壊する
ドメインの文書化
①どうやって要件を記録する?
- ワークフローではインプットとアウトプットを文書化
- ビジネスロジックは簡単な疑似コードで表現
- データ構造はAND、ORで表現
Bounded Context: Order-Taking
Workflow: "Place Order"
triggered by:
"Order form received" event
primary input:
Product catalog
output events:
"Order Placed" event
side-effects:
An acknowledgment is send to the customer,
alog with the placed order
bounded context: Order-Taking
data Order =
CustomerInfo
AND ShippingAddress
AND BillingAddress
AND List of OrderLines
AND AbountToBill
data OrderLine =
Product
AND Quantity
AND Price
data CustomerInfo = ?
data BillingAddress = ?
ドメインエキスパートに見せて一緒に作業できるレベル
②ワークフローを深掘りする
ユビキタス言語の定義もここで発生する
③2をもとに制約条件を表現
context: Order-Taking
# 装置コード
data WidgetCode = string starting with "W" then 4 digits
# 機器コード
data GizmoCode = ~~~~
# 製品コード
data ProductCode = WidgetCode or GizmoCode
# 数量要件
data OrderQuantity = UnitQuantitiy OR KilogramQuantity
data UnitQuantity = interger between 1 and ?
data KilogramQuantity = decimal between ? and ?
設計が厳格で、実装は必ずしも厳格である必要はない
? についてドメインエキスパートに確認をおこない、制約を詳細にしていく
④ライフサイクルを表現
Orderは状態が変化する
- 検証前
- 検証後
- 価格反映後
各フェーズに名前付けして分離する(例:UnvalidatedOrder, ValidatedOrder, PricedOrder)
検証前
data UnvalidatedOrder =
UnvalidatedInfo
AND UnvalidatedShippingAddress
data UnvalidtedOrderLine =
UnvalidatedProduct
検証後
data ValidatedOrder =
validatedInfo
AND validatedShippingAddress
data ValidtedOrderLine =
ValidatedProduct
価格計算済み注文
data PricedOrder =
ValidatedCustomerInfo
AND ValidatedShippingAddress
AND ValidatedBillingAddress
AND List of PricedOrderLine
AND AmountToBill
data PricedOrderLine
ValidatedOrderLine
AND LinePrice
価格計算済み注文作成後に注文確認書が作成
data PlacedOrderAcknowledgment =
PricedOrder
AND AcknowledgmentLetter
ビジネスロジックとして以下がわかる
- 検証されていない注文には価格がない
- 検証された注文は全ての明細業が検証される必要がある
⑤ワークフローのステップを具体化
workflow: "Place Order" =
input: OrderForm
output:
OrderPlaced event
Or InvalidOrder
do ValidateOrder
If order is invalid then:
add InvalidOrder to pile
stop
do PriceOrder
do SendAcknowledgmentToCustomer
return OrderPlaced event
サブステップを追加
substep "ValidateOrder" =
input: UnvalidatedOrder
output: ValidatedOrder OR ValidatedError
dependenticies: CheckProductCodeExist, CheckAddressExists
validate ~~~~
ドメインモデルのパターン
- 単純な型
- プリミティブ型
- butドメインエキスパートと認識揃えたユビキタス言語の概念でとらえる
- ANDによる値の組み合わせ
- ORによる選択肢
- Order OR Quote
- UnitQuantity OR KilogramQuantity
- ワークフロー
- input and output
単純な値のモデリング
type CardNumber = CardNumber of string
type CheckNumber = CheckNumber of int
type CustomerId = CustomerId of int
type UnitQuantity = UnitQuantity of int
type KilogramQuantity = KilogramQuantity of decimal
型の名前とケースラベルを含む
ドメインエキスパートとの会話にラグがなくなるのはわかった
あとは何が嬉しいの?
▶︎単純なプリミティブではなく意味を持った型になるので、意味の混合が起きない(コンパイル時)
値オブジェクト (Value Object)
概念
値オブジェクトは、その属性によって同一性が定義されるオブジェクトです。つまり、値オブジェクトは一度生成されたら変更されず、同じ属性を持つ値オブジェクトは同一とみなされます。
特徴
不変性: 値オブジェクトは一度作成されたらその状態が変わることはありません。変更が必要な場合は、新しいインスタンスを作成します。
同一性の定義: 属性が同じであれば、別々のインスタンスでも同一とみなされます。
小さなオブジェクト: 通常、住所や通貨などの小さなオブジェクトとして使用されます。
例
住所を表す値オブジェクトの例です。
type Address = {
Street: string
City: string
ZipCode: string
}
// 値オブジェクトの使用例
let address1 = { Street = "123 Main St"; City = "Anytown"; ZipCode = "12345" }
let address2 = { Street = "123 Main St"; City = "Anytown"; ZipCode = "12345" }
// address1 と address2 は属性が同じなので同一とみなされる
address1 = address2 // true
エンティティ (Entity)
概念
エンティティは、一意の識別子によって同一性が定義されるオブジェクトです。属性が同じでも、異なる識別子を持つ場合は別のエンティティとして扱われます。
特徴
識別子: エンティティは一意の識別子(ID)を持ちます。この識別子によってエンティティが区別されます。
変化: エンティティはそのライフサイクルの中で状態が変化することがあります。
長期的なライフサイクル: エンティティは通常、システム内で長期的に存在し続けます。
例
顧客を表すエンティティの例です。
type CustomerId = CustomerId of int
type Customer = {
Id: CustomerId
Name: string
Address: Address
}
// エンティティの使用例
let customer1 = { Id = CustomerId 1; Name = "John Doe"; Address = address1 }
let customer2 = { Id = CustomerId 2; Name = "Jane Doe"; Address = address2 }
// customer1 と customer2 は同じ属性を持っていても、異なるIDを持つため別のエンティティとみなされる
customer1 = customer2 // false
値オブジェクトとエンティティの使い分け
不変性が重要な場合は値オブジェクト:
例えば、金額、通貨、住所など、属性によって同一性が定義され、変更されないデータを扱う場合は値オブジェクトを使用します。
一意の識別が重要な場合はエンティティ:
例えば、顧客、注文、商品など、システム内で一意の識別子が必要であり、状態が変化する可能性があるデータを扱う場合はエンティティを使用します。
ドメイン駆動設計における集約と集約ルート
集約(Aggregate)
集約とは、関連するエンティティや値オブジェクトを一つのまとまりとして扱う設計パターンです。集約はシステムの一貫性を保つための単位であり、その中で発生するすべての操作が整合性を保つように設計されています。集約を使うことで、システム全体の複雑さを管理しやすくなります。
集約ルート(Aggregate Root)
集約ルートは、集約の中で最も重要なエンティティであり、集約全体へのエントリーポイントです。集約ルートを通じてのみ、外部から集約内部のエンティティや値オブジェクトにアクセスできるようにします。これにより、集約内の状態を保護し、整合性を維持することができます。
実際の例
オンラインショップを例にして説明します。ここでは、注文(Order)が集約であり、その中には注文の詳細(Order Line)や配送先住所(Shipping Address)が含まれます。注文自体が集約ルートとなります。
コード例
// 値オブジェクトの定義
type Address = {
Street: string
City: string
ZipCode: string
}
// エンティティの定義
type OrderLine = {
ProductId: int
Quantity: int
UnitPrice: decimal
}
// 集約ルートの定義
type Order = {
Id: int
CustomerId: int
OrderLines: List<OrderLine>
ShippingAddress: Address
}
// 集約ルートによる操作の実装
let addOrderLine order productId quantity unitPrice =
let newOrderLine = { ProductId = productId; Quantity = quantity; UnitPrice = unitPrice }
{ order with OrderLines = newOrderLine :: order.OrderLines }
let updateShippingAddress order newAddress =
{ order with ShippingAddress = newAddress }
// 集約ルートの使用例
let order = {
Id = 1
CustomerId = 123
OrderLines = []
ShippingAddress = { Street = "123 Main St"; City = "Anytown"; ZipCode = "12345" }
}
let updatedOrder = addOrderLine order 456 2 19.99M
let finalOrder = updateShippingAddress updatedOrder { Street = "456 Elm St"; City = "Othertown"; ZipCode = "67890" }
完全性(Completeness)
完全性とは、システムが対象とするドメイン内で必要とされるすべての要件や機能が正しく網羅されている状態を指します。これは、ドメインモデルがビジネスのあらゆる側面を適切に表現し、すべてのビジネスルールや要件を反映していることを意味します。
整合性(Consistency)
整合性とは、システム内のデータや状態が常に矛盾なく正しい状態に保たれていることを指します。これには、同一のデータが異なる部分で異なる値を持たないことや、ビジネスルールが常に適用されていることが含まれます。
上記のためのアプローチ
スマートコンストラクタアプローチ
ドメインモデルを作成する際に、安全な状態のみを作り出すための方法。
通常のコンストラクタではなく、スマートコンストラクタを使う。
スマートコンストラクタ内でバリデーションを行い、ルールに違反するデータはオブジェクト化しない。
// 型定義
type CardNumber = CardNumber of string
// スマートコンストラクタ
module CardNumber =
let create (input: string) =
if isValidCardNumber input then
Some (CardNumber input)
else
None
// バリデーション関数
let isValidCardNumber (input: string) : bool =
input.Length = 16 && input |> Seq.forall Char.IsDigit