“2. Understanding the Domain” from “Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F#” 📚
一つのワークフローに着目して掘り下げていく
- 何がそれをトリガーするのか?
- どんなデータが必要?
- どんな境界づけられたコンテキストと協働する必要がある?
ここでは注意深く耳を傾ける能力が重要。 自分のメンタルモデルをドメインに押し付けないようにしたい。
ドメインエキスパートへの聞き取り
ドメインエキスパート(文中では受注部門のメンバー)に聞き取り調査をしていく。
ポイント:
- イベントストーミングで洗い出したコマンド・イベントを手がかりに、聞き取り調査をいくつかの小さい単位に分ける ✅
- 忙しいドメインエキスパートに時間をとってもらいやすい
- インタビューの初期は高次の視点を維持し、ワークフローの入出力だけに着目する ✅
- 詳細に囚われすぎずに済む
- 顧客がシステムをどう使うかの結論を急がず、人類学者のようにとにかく耳を傾ける ✅
- 文中では「顧客が注文フォームをオンラインで入力してくれたらいいのに」というドメインエキスパートの言葉に対して、開発者は一般的なカート & チェックアウトパターンのシステムを想像しかけた
- しかしドメインエキスパートが求めるのはそのパターンではなかった 💣
- 文中では「顧客が注文フォームをオンラインで入力してくれたらいいのに」というドメインエキスパートの言葉に対して、開発者は一般的なカート & チェックアウトパターンのシステムを想像しかけた
- 理想的には、設計の前にドメインエキスパートが働く様子を観察したりするとよい ✅
非機能要件を理解する
一歩下がって、ワークフローのスケールとコンテキストについて議論する。
文中では、後のアーキテクチャ決定に関わる下記の要件についてドメインエキスパートに尋ねている:
- 1 日あたりのリクエスト数
- リクエスト数の年間変動
- ユーザーの属性(ビギナー or プロフェッショナル)
- サービスに期待されているレスポンス
ワークフローの残りを理解する
ドメインエキスパートへの聞き取りによってドメインの知識を獲得していく。
文中では「受け取ったそれぞれの注文フォームで何をするのか?」という質問から始めて、下記のドメイン知識を発見した:
- 検証作業
- 協調すべき境界づけられたコンテキスト
- 集計作業
- 注文確認の返信作業
- 同じ入力を利用する別のワークフロー(見積もり作業)
入出力について考える
ワークフローの入出力について、聞き取りから学んだことを書き留める
- 入力: 明らかに注文フォーム ✅
- 出力:
- 「処理済みの注文」という概念があることはわかった。しかしそれ自体は特に出力として使い道がない ❌
- 注文確認はどうか?これは注文確定ワークフローの副作用であり、出力ではない ❌
- 出力は常に生成イベントであるべき ✅
- 他の境界づけられたコンテキストのアクションをトリガーするもの
- ここでは
注文が確定された
イベント
データベース駆動設計したくなる衝動と戦う ⚠️
📝 この節は目からウロコだった・・・! この節を読むべきなのは初学者よりむしろ、ビジネス要件から DB スキーマを設計することに慣れている人だと思う。
ある程度経験を積んだプログラマなら、もう詳細設計して実装に入りたくなるだろう。 テーブル間の関連について、下記のように設計を始めるかもしれない:
erDiagram Customers ||--o{ Orders : places Customers { string name string custNumber string sector } Orders ||--|{ OrderLines : contains Orders { string customerId string shippnigAddressId string billingAddressId bool isQuote } OrderLines { string orderId string productId float quantity } Addresses ||--|{ Orders : referred
・・・しかしこれをやってしまったらオシマイ 💣(マジか)
DDD ではドメインを正確にモデリングすることに集中するために「永続性の無視」というアプローチで設計する:
- ドメインに設計を駆動させる ✅
- DB スキーマではない ❌
- 実際、現実の紙ベースのシステムではデータベースなど存在しない
データベース
はユビキタス言語には入らない- ユーザーはデータがどう永続化されているかには興味がない
なぜこれが重要? → データベースの視点からデザインすると、データベースモデルに合うようにデータを歪めないといけなくなるから
例:
- 既に先ほどの ER 図では
注文
と見積り
の違いを無視してしまっている 💣- 同じ外部キー(
orderId
はquoteId
としても使われる)が二つのリレーションで責務を持つ状況は、データベースの機能ではサポートできない 💣(なるほど・・・!!)
- 同じ外部キー(
とにかく、技術的な先入観なしにドメインエキスパートの言葉に耳を傾けることに集中するのが重要。 ドメイン駆動で設計したモデルを関係データベースに永続化する方法は第 12 章で見ていく。
クラス駆動デザインへの衝動と戦う ⚠️
📝 本節のおかげで、私は個人開発でこの過ちを犯していたことを自覚した
オブジェクト指向の経験があるなら、特定の DB 実装に縛られないことの重要性は理解していることだろう。 しかし、ドメインではなくオブジェクトの観点で考えることによって、モデルに偏見を持ち込んでしまう(マジか)。
クラス駆動デザインも、要求をちゃんと聞いていないという点でデータベース駆動デザインと同じくらい危険 💣
こんなふうに設計するかもしれないが
classDiagram OrderBase <|-- Order OrderBase <|-- Quote OrderBase "n" -- "1" Customer Order "n" -- "1" Address : shipping address Order "n" -- "1" Address : billing address OrderBase "1" *-- "n" OrderLine OrderLine "1" -- "n" Product
注文
と見積り
の違いは表現できているできているものの、現実世界に存在しないOrderBase
という人工的なクラスを持ち込み、ドメインを歪めてしまった 💣
(まぁ"ドメインモデルと詳細設計は区別しましょう"で片付く話ではあるか)
心をオープンに保て。 技術的な思考をドメインに持ち込むな。
ドメインを文書化する
技術的な実装による偏見に陥らず、要求をどのように記述すべきだろう? UML のような可視化ダイアグラムを使うこともできるが、労力がかかる割にドメインの機微をとらえられるほどの表現力がない。
正確なドメインモデルをコードで表現する方法は後ほど学ぶとして、今は、ドメインモデルを捉えられる簡単なテキストベース言語を使う:
- ワークフローの表現法: 入出力を記述し、ビジネスロジックを記述するシンプルな疑似コードを使う
- データ構造の表現法
- 二つの部分が共に必要なときには
かつ
を使う - どちらかだけでいい時には
または
を使う
- 二つの部分が共に必要なときには
このミニ言語を使って、注文確定ワークフローを記述すると下記のようになる:
境界づけられたコンテキスト: 受注
ワークフロー: 注文確定
トリガー: "注文フォームを受け取った" イベント("見積もり" にチェックが入っていたとき)
初期入力:
注文フォーム
その他の入力:
商品カタログ
出力イベント:
"注文が確定した" イベント
副作用:
注文確定に伴って注文確認が顧客に送信された
データ構造は下記のように表現される:
境界づけられたコンテキスト: 受注
注文 データ =
顧客情報
かつ 配送先住所
かつ 請求先住所
かつ 注文明細 のリスト
かつ 請求金額
注文明細 データ =
商品
かつ 数量
かつ 価格
顧客情報 データ = ??? // まだ不明
請求先住所 データ = ??? // まだ不明
これはドメインを少し構造化された方法で捉えただけで、クラス構造もデータベーステーブルもない:
- ノンプログラマにとってもこわくない ✅
- ドメインエキスパートにも見てもらい、一緒に取り組んでもらえる
- ❓ コードもこれくらいシンプルにできるのか?
- → 第 5 章で学ぶ
受注ワークフローを掘り下げる
入出力を記述できたので、聞き取り調査を再開してワークフローを掘り下げていく。
下記のドメイン知識が明らかになった:
注文
か見積り依頼
かを判別見積り依頼
はあとで処理する- 売上げを生む
注文
のほうが重要だから
- 売上げを生む
- 検証作業におけるサードパーティサービスの利用
- 用途の異なる書類の山
- 実装ではキューになるだろうが、聞き取り調査では技術的詳細からは距離を保っておく
商品コード
のチェック- 技術的に言うとシンタックスチェック
商品コード
の照合- 技術的に言うとデータベースルックアップ
- 依存管理
- 「もし、商品部門のだれかがあなたの問い合わせに即座に答えられるとしても、商品カタログをやはり手元に持っておきたい?」「もちろん。電話が使えなかったり、彼らが忙しかったりしても、私の仕事が止まらずに済むから。」
- 数量単位の仕様
複雑さをドメインモデルに反映させる
ワークフローを掘り下げたことによって、ドメインモデルがかなり複雑になった
- これは良いこと ✅
- 実装する段になって複雑性が判明するのは大きな時間の無駄になる
- 初期段階で、ドメインの理解に時間をかけるべき
- 「数週間のコーディングによって設計にかかる時間を数時間節約できる」という皮肉がある
制約を表現する
聞き取り調査の結果、商品コードと数量が単なる文字列あるいは整数でないことがわかった。 これをドキュメントに反映する。
ウィジェットコード
、ギズモコード
、商品コード
の制約は下記のように表現される:
コンテキスト: 受注
ウィジェットコード データ = "W" から始まり 4 つの数字からなる文字列
ギズモコード データ = "G" から始まり 3 つの数字からなる文字列
商品コード データ = ウィジェットコード または ギズモコード
- ❓ この制約は厳しすぎるだろうか?
- ❓ 新しい商品コードを追加したくなったらどうする?
- 正しい答えはコンテキストによるが、設計をドメインエキスパートの視点からすることが重要
- ❓ 新しい商品コードを追加したくなったらどうする?
ここでは、この制約はドメインの設計に反映させるべきだろう。 なぜなら、商品コードのチェックは注文確認プロセスの重要な工程であることを学んだので、最終的には何らかの方法でドキュメント化が必要。 別のドキュメントに分散するより設計として書いた方が良い。
ちなみに、設計が厳しいからといって必ずしも実装まで厳しくする必要はない。 不正な入力があったときの実装例:
- 注文全体を即座に拒否(これは厳しすぎるだろう)
- ユーザーに「要確認」の通知を出す(よい落とし所かもしれない)
数量に関する制約もドキュメントにしておく:
単位数量 データ = 1 から 1000 までの整数
キログラム数量 データ = 0.05 から 100.00 までの小数
注文のライフサイクルを表現する
注文
(型)に着目し、学んだことを記述する。
初期のバージョンでは下記のように表現したが、これはドメインを表現し切れていないことがわかった。
境界づけられたコンテキスト: 受注
注文 データ =
顧客情報
かつ 配送先住所
かつ 請求先住所
かつ 注文明細 のリスト
かつ 請求金額
聞き取り調査の結果、下記を学んだ:
注文
にはライフサイクルがある注文
は初めは請求金額
を持たないが最後には必ず持つようになる
- 未検証の注文や未集計の注文は他と区別できる
- 紙ベースの現在の業務では進捗をフォームに記録しているため
これらをモデルにも反映する必要がある。
未検証の注文 データ =
未検証の顧客情報
かつ 未検証の配送先住所
かつ 未検証の請求先住所
かつ 未検証の注文明細 のリスト
未検証の注文明細 データ =
未検証の商品コード
かつ 未検証の注文数量
各情報のステータスがわかるようになった(DB のフラグで済ませない!!!!)。
注文が確認されると下記のようになる:
検証済の注文 データ =
検証済の顧客情報
かつ 検証済の配送先住所
かつ 検証済の請求先住所
かつ 検証済の注文明細 のリスト
検証済の注文明細 データ =
検証済の商品コード
かつ 検証済の注文数量
次は注文に金額を入力する:
集計済の注文 データ =
検証済の顧客情報
かつ 検証済の配送先住所
かつ 検証済の請求先住所
かつ 集計済の注文明細 のリスト // 知識が反映されている
かつ 請求金額 // 知識が反映されている
集計済の注文明細 データ =
検証済の商品コード
かつ 明細料金 // 知識が反映されている
ドメインモデルがビジネスロジックを反映するようになった 🎉 初めに考えていたよりも複雑になったが、逆に言えばもしモデルがこのように複雑でなかったとしたら、それは要件を正しく捉えられていなかったということ。
ワークフロー中のステップを肉付けする
ワークフローが小さいステップに分割されることがわかった:
- 確認作業
- 集計作業
- などなど
これらのステップにも入力出力アプローチを適用しよう
ワークフロー: 注文確定
入力: 注文フォーム
出力:
"注文が確定した" イベント(他のチームに送るための山に置く)
または "不正な注文" (それ用の山に置く)
// ステップ 1
do 注文を検証
if 注文 が 不正 then:
add 不正な注文 to 山
stop
// ステップ 2
do 注文集計
// ステップ 3
do 顧客に確認を送る
// ステップ 4
return 注文が確定された イベント(if エラーがないならば)
それぞれのサブステップもドキュメント化する
ワークフロー: 注文を検証
入力: 未検証の注文
出力: 検証済の注文 また 検証エラー
依存: 商品コードの存在チェック, 住所の存在チェック
顧客の名前を検証する
配送先住所と請求先住所が存在することを確認する
for each 注文明細:
商品コードをチェック
商品コードが商品カタログに存在することをチェック
if 全て OK ならば, then:
return 検証済の注文
else:
return 検証エラー
ほぼ丸写しになってしまうのであとのステップは割愛。
まとめ
次のモデリングフェーズでたくさんやることがあるので、要件の収集はここまで。
ここで学んだことをまとめる:
- 設計時には実装の詳細に立ち入らないことが重要
- 代わりに、先入観やコーディング手法にとらわれずにドメインを捉えることに集中しよう
本文中で例として使われている受注システムのようなはかなり単純化されているが、それでもドメインエキスパートの言葉に耳を傾けると、多くの複雑性が明らかになることがわかる。 例えば一口に注文といっても、ライフサイクルを通して、データや振る舞いの異なるのバリエーションがあった。
次にやることは?
受注ワークフローを F# の型システムを使ってどのようにモデリングするかをみていきたい。 次章ではその前に、一歩下がって全体像を見て、システム全体をどうソフトウェアアーキテクチャに翻訳するかを議論する。