DMMFを読む
DDDも勉強途中であるが
Domain Modeling Made Functional
を読んで気になった点、調べたことなど書いていく。
全体の構成
- Part1. Understanding the domain (ch1-3)
- Part2. Modeling the domain (ch4-7)
- Part3. Implementing the model (ch8-13)
Chapter1
shared mental model 作る際のDDDコミュニティによるガイドラインが紹介されている。
- データ構造よりも、ビジネスイベントとワークフローに注目する
- ドメインをサブドメインに分割する
- 各サブドメインに対してモデルを作る
- ユビキタス言語をを作る
なぜデータ構造よりビジネスイベントに注目するのか?ビジネスとは単なるデータではなくそれを変換するプロセスであり、そこに価値がある。だからその変換プロセスがどのように機能するかを理解することが重要なのだと。何がプロセスのトリガーになるのかを設計の中で捉えることが重要であり、これをドメインイベントと呼ぶ。
イベントストーミングで、ドメインイベントをタイムラインに沿って整理していく。
なおイベント名はOrderPlaced
のように過去形でつける。それが既に起こって変わることのない事実だからだ。
次にCommandの話になる。Command名は命令形でつける。
コマンドは失敗することもあるが、成功するとワークフローを始動させる。
ワークフローの結果ドメインイベントが発生する。
このようにパイプラインで考えると関数型プログラミングと合うのだと。
コマンドのトリガーになるのは別のイベントの場合もあるし、スケジューラーやモニタリングシステムのこともあると。
個人的にコマンドの理解が十分ではないので以下のあたり整理してみてもいいかも
probrem space(real world)
* (sub?)domains
solution space(domain model)
* bounded contexts
bounded contextは実装で何らかのソフトウェアコンポーネントになる(DLL, サービス、名前空間、etc)
このあたりは実践ドメイン駆動設計の内容と概ね同じか。
bounded contextの境界を正しく定めることが非常に重要であり(this is an artだと)、そのガイドラインが挙げられている。いくつか気になったものもある。
境界を定めるヒントとして、既存のチームや部門の境界を参考にできる、とある。
コンウェイの法則を思い出した。
Design for friction-free business workflows. ワークフローが複数のbounded contextsと作用しているとワークフローがブロックされたり遅延が起こり得るので、見直せ(autonomyにしておけ)(例え醜い設計になったとしても)、とある。
Chapter2
ワークフローのoutputはeventであるべきだという。
あと、ここでの顧客へのメール送信はワークフローの副作用であってoutputではないと。
Place Orderワークフローの図
上記改行できなくてつらい。live editorだとできたんだけど
ドメインの要求を集めたら、
永続化やオブジェクト指向のクラス設計に進むのではなく (persistence ignorance )
それらをテキストでまとめる。
Bounded context: Order-Taking
Workflow: Place Order
triggered by:
"OrderFormReceived" event (when Quote checkbox is not checked)
primary input:
an order form
other input:
product catalog
output events:
"OrderPlaced" event
side effects:
an acknowledgment is sent to the customer,
along with the placed order
ワークフローに関連するデータ構造について記載してもよい。(疑似コード)
data Order =
CustomerInfo
AND ShippingAddress
AND BillingAddress
AND list of OrderLines
AND AmountToBill
data OrderLine =
Product
AND Quantity
AND Price
data CustommerInfo = ??? // don't know yet
data WidgetCode = string starting with "W" then 4 digits
data GizmoCode = string starting with "G" then 3 digits
data ProductCode = WidgetCode OR GizmoCode
data OrderQuantity = UnitQuantity OR KilogramQuantity
data UnitQuantity = integer between 1 and 1000
data KilogramQuantity = decimal between 0.05 and 100.00
これぐらい最低限の構造化とテキストベースの記載なら開発者以外(domain experts)に共有できるだろうと。
ライフサイクルのあるOrderについては各フェーズについて別々の名前で定義していく。
before validation
data UnvalidatedOrder =
UnvalidatedCustomerInfo
AND UnvalidatedShippingAddress
AND UnvalidatedBillingAddress
AND list of UnvalidatedOrderLine
data UnvalidatedOrderLine =
UnvalidatedProductCode
AND UnvalidatedOrderQuantity
after validation
data ValidatedOrder =
ValidatedCustomerInfo
AND ValidatedShippingAddress
AND ValidatedBillingAddress
AND list of ValidatedOrderLine
data ValidatedOrderLine =
ValidatedProductCode
AND ValidatedOrderQuantity
after placed order
data PricedOrder =
ValidatedCustomerInfo
AND ValidatedShippingAddress
AND ValidatedBillingAddress
AND list of PricedOrderLine // different from ValidatedOrderLine
AND AmountToBill // new
data PricedOrderLine =
ValidatedOrderLine
AND LinePrice // new
to create acknowledgment
data PlacedOrderAcknowledgment =
PricedOrder
AND AcknowledgmentLetter
最終的にワークフローをサブステップに分割して
テキストベースで記載する。
workflow "Place Order" =
input: OrderForm
output:
OrderPlaced event (put on a pile to send to other teams)
OR InvalidOrder (put on appropriate pile)
// step 1
do ValidateOrder
If order is invalid then:
add InvalidOrder to pile
stop
// step 2
do PriceOrder
// step 3
do SendAcknowledgementToCustomer
// step 4
return OrderPlaced event (if no errors)
substep "ValidateOrder" =
input: UnvalidatedOrder
output: ValidatedOrder OR ValidationError
dependencies: CheckProductCodeExists, CheckAddressExists
validate the customer name
check that the shipping and billing address exist
for each line:
check product code syntax
check that product code exists in ProductCatalog
if everything is OK, then:
return ValidatedOrder
else:
return ValidationError
chapter3
ドメインモデルがどのようにして関数型プログラミングで実装されるか、について考える。
Simon Brown's C4 approach: ソフトウェアアーキテクチャを構成する4つのレベル。上位レベルは下位レベルから構成されている。
- system context: システム全体を表す最上位レベル
- containers: デプロイ可能な単位 (website, web service, database, etc)
- components:
- classes, modules: that contains methods or functions
分散モノリスについて。
It’s tricky to create a truly decoupled microservice architecture – if you switch one of the microservices off, and anything else breaks, you don’t really have a microservice architecture, you just have a distributed monolith!
各サービスは自律的に機能するものであるべきで、他に依存しているようならそれはただの分散させたモノリスか。
Bounded contexts間の通信はイベントによって行い、イベントは必要なデータを含んでいる。
このデータは単なるドメインオブジェクトとは異なり、シリアライズされコンテクスト間で共有されるために設計されている。(DTO, Data Transfer Object)
Chapter4
In fact, a type is just the name given to the set of possible values that can be used as inputs or outputs of a function
型とは単に、可能な値の集合のことなのか
Composition of Types
AND types, Product(積) types, records
type FruitSalad = {
Apple: AppleVariety
Banana: BananaVariety
Cherries: CherryVariety
}
OR types, Sum(和) types, Tagged unions
type FruitSnack =
| Apple of AppleVariety
| Banana of BananaVariety
| Cherries of CherryVariety
type AppleVariety =
| GoldenDelicious
| GrannySmith
| Fuji
関数表記(引数、戻り値)でunitが見えたら、その関数には副作用がある確率が高い。
Chapter 5. Domain modeling with types
immutabilityが重要な関数型プログラミングにおいて、エンティティの更新はどのように扱うのか。
type Person = { PersonalId: int; Name: string }
let initialPerson = { PersonalId = 1; Name = "Joseph" }
let updatedPerson = { initialPerson with Name = "Joe" }
reduxのstateの扱いと同じ気がする。
元のrecordのフィールドを書き換えるのではなく、元のフィールド値をコピーして新しいrecordを作る。
Chapter 6. Integrity and Consistency
IntegrityというのはValidityのことらしい。
total function 入力に対し必ず対応する出力がある。
全域写像?
ドメインロジックはステートレスな純粋関数として実装すると、テストしやすくなる。
外部サービス呼び出しや副作用を伴う処理をdependencyパラメータとして明示的に渡せるように設計する。
起こり得るエラーは、コンパイラでチェックできるように関数戻り値として明示的に記述したい。(total functionにしたい) => Result型
エラーを三つに分類
- Domain error: ドメインルールの一部として想定されているエラー
- Panic:
- Infrastructure error: アーキテクチャから想定されるエラー。ネットワークタイムアウトなど。
Result<Ok, Error>の戻り値を持つ関数をパイプラインとして繋げるにはどうするか。
Resultをそのまま次の関数のinputにはできない。
アダプターを使って、
'a -> Result<'b, 'c>
を Result<'a, 'c> -> Result<'b, 'c>
に変換する。
このアダプターは関数型プログラミングでbind, flatMapというらしい。
let bind f aResult =
match aResult with
| Ok success -> f success
| Error failure -> Error failure
// カリー化した実装
let bind f =
fun aResult ->
match aResult with
| Ok success -> f success
| Error failure -> Error failure
もう一つの便利なのが 'a -> 'b
を Result<'a, 'c> -> Result<'b, 'c>
に変換するアダプターであり、
これは関数プログラミングではmapと呼ばれるらしい。
let map f aResult =
match aResult with
| Ok success -> Ok(f success)
| Error failure -> Error failure
ちなみに以下はエラーの方をmapする関数
let mapError f aResult =
match aResult with
| Ok success -> Ok success
| Error failure -> Error(f failure)