前回記事「【まとめ】ドメイン駆動開発(実装編)」の続きです。
前提
参考資料
下記を参考に情報を自分なりにまとめたものです。学習中の身 かつ ドメイン駆動開発の難解さ(故に人によって意見も違う)もあり、絶対に正しい情報という訳ではない点はご理解ください。
- [入門]ドメイン駆動設計 基礎と実践・クリーンアーキテクチャ
- ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本
- 【実践ドメイン駆動設計】第3章 コンテキストマップ 読書メモ
- 境界づけられたコンテキスト 概念編 - ドメイン駆動設計用語解説 DDD
- DDDを実践するための手引き(ドメインイベント編)
まとめの流れ
「ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本」がわかりやすかったので、そちらに倣い、実装戦略→設計戦略の順にまとめていく予定です(今回は実装設計編)。
最終的に下記知識を網羅するものとなります。
<実装戦略>
- 値オブジェクト
- エンティティ
- 集約
- 仕様パターン
- ドメインサービス
- リポジトリパターン
- ファクトリーパターン
<設計戦略>
- ユビキタス言語
- ドメインモデル
- イベントストーミング
- 境界づけられたコンテキスト
- コンテキストマップ
といっても「ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本」は設計戦略についてはさわり程度にしか触れられていないので、今回の章は他の本やネットで収集した情報を集約したものとなります。
その点もご了承ください。
基本思想(設計観点)
ドメインとは
ソフトウェアが対象とする「領域」のこと。業務系アプリケーションであれば、事業領域=ドメイン。
「領域」という表現を用いるのは、ドメインが単一の事業活動を表す訳ではないことを意味する。複数の事業領域の集合が事業活動であり、それぞれの事業領域は動的なネットワークによって構成されている。
この独立性の高い構成要素の関係性によってひとつのシステムを表現するという考え方が、マイクロサービスや分散アーキテクチャの思想と重なることから、ドメイン駆動設計は近年注目を集めている。
他の開発手法との違い
アプリケーションの対象領域である事業活動や業務内容を理解することは、V字モデルにおいても要件定義工程などを通じて自然に行われる。
ただしドメイン駆動設計がV字モデルなどと異なるのは、ソフトウェア設計と直接的に関連づけることを目標としていること。
これは「プログラムが複雑化する要因はドメインそのもの。複雑な業務プロセスやビジネスロジックをソフトウェア設計にどう落とし込むかに焦点を当てるべき」という考えに基づく。
また、ビジネスロジックは環境や顧客動向に適用して変化し、常にルールの追加/調整を必要とするものであるため、それらの変化に対する容易性が必要となる。
この「複雑な業務プロセスやビジネスロジックをソフトウェア設計にどう落とし込むか」と「環境や顧客動向に適用することで発生する変化を取り込みやすくするにはどうすれば良いか」をまとめたものがドメイン駆動設計である。
ユビキタス言語
ドメイン駆動設計では、複雑な業務プロセスやビジネスロジックを深く理解していく必要がある。一般的な業務知識、業務マニュアル、顧客の文化やローカルルール、場合によっては現行システムの既存コードなど、様々な角度で情報を収集する必要がある。
ここで困るのは下記のようなケース。
- それぞれの情報源で用語や言い回しが異なる
- 知りたいこと以外の情報が大量に含まれている
この問題を解消するためにドメイン駆動設計ではユビキタス言語を定義する。
ユビキタスは「いつでも、どこでも」という意味の言葉で、ドメインエキスパート(業務の専門家=顧客)とソフトウェアの専門家(開発)が「同じ言葉を使ってソフトウェアを開発しよう」というもの。
全てを網羅的に辞書化するのは現実的には無理であるため、重要な言葉や表記揺れが発生するものを中心とし、表現方法の統一化を図る。それによって周辺の言葉や使い方にも整合性が生まれる、というのがユビキタス言語という考え方。
また、用語が確定することで、その用語間を繋ぐ重要な関係を抜き出すことができるようになり、大量の情報の中から必要な情報だけを拾いやすくするという効果もある。
こうした用語定義・関係性の定義から、ソフトウェアに対して重要な情報のみ集約・抽象化した登場人物(モデル)を浮かび上がらせ、それをそのままソフトウェア設計に用いる。
ユビキタス言語の定義は、システム理解のための入り口を開け放つ鍵のような存在と言えるだろう。
ドメインモデル
ドメイン駆動設計では、知識の整理や認識合わせに使う登場人物(モデル)をそのままプログラムの基本構造とする。そして業務ロジックに直接関係しないものを周辺的な関心事として切り分けて考える。
この「知識の整理や認識合わせに使うモデル」を独立させるための設計スタイルが「ドメインモデル」である。
このドメインモデルは下記のように、業務知識の整理・関係者間での意図伝達、そしてクラス設計と一貫して用いられる。
- 業務知識の要点の整理
- 関係者が意図を伝える時の基本語彙
- クラス設計の基本構造
イベントストーミング
出来事というものは、現実的にはただ過ぎ去った過去の状態に過ぎない。それは自分自身の記憶に自動的に記録され、いつでも振り返ることができる。
しかしながら、システムにおいて状態というものは、何もしなければ一切記録されることはない。
そこで、ドメインで発生する重要な出来事に着目し、ユースケースにおける出来事の連続を整理することで、システムに保存しなければならない状態を明確化しようというものがイベントストーミングと呼ばれる設計手法。
イベントストーミングは主に下記3つのフェーズに分かれる。
- ビッグピクチャー
ドメイン内で発生する様々なイベントを洗い出し、時系列に沿って並べる。これにより、ドメインの全体像を明確にして、その中でも重要なものやイベント間の関連性を明らかにする。 - ビジネスプロセスモデリング
イベントの構造を分解し、紐づく集約との関連性やイベントに付随する要素を整理する。 - ソフトウェアシステムモデリング
集約の深掘りや境界づけれられたコンテキストに基づき、実際にソフトウェア設計に適用する方法を検討する。
ビジネスプロセスモデリングでは、イベントを下記要素に分解する。
- ドメインイベント
- コマンド
- アクター
- ドメインモデル(集約)
- ポリシー
そしてその分解要素の多くは、ソフトウェアシステムモデリングにて実際のクラス定義などで用いられる。
ドメインイベント
イベントストーミングによって洗い出される、ドメインにおける重要な出来事の結果(=システムに状態として保存しておく、あるいは一時的に保持しておく必要がある)をドメインイベントという。
ドメインイベントは出来事そのものではなく出来事の結果(状態)であるため、常に過去形で表される。
- 本が予約された
- 本の予約がキャンセルされた などなど
一連のユースケースは複数のドメインイベントの連続によって表現することができ、それによってシステムが保持しておかなければならない状態が明らかになる。
- 本が予約された
- 本の予約がキャンセルされた
- 次の予約者に予約を割り当てた
- 予約本を貸出した
- ....
コマンド
ドメインイベントを発生させる行為のこと。
例えば本の予約であれば「予約コマンド」が「本が予約された」というドメインイベントを発生させ、「本の予約キャンセル」であれば「本の予約がキャンセルされた」というドメインイベントを発生させる。
アクター
ドメインイベント発生のトリガーとなるコマンドの実行者。誰がそのコマンドを実行するのかを整理することで、最終的にどんなことをその実行者に還元しなければならないのかを明確化することができる。
ドメインモデル(集約)
ドメインイベントを生み出すものは基本的に同一性を持つ。故にドメインイベントは基本的に集約のドメインモデルによって生み出される。
例えば「予約」は同じ人が同じ本を予約したとしても、それは別物として扱われることから同一性を持ち、システム上はIDという形で同一性が表現される。
ドメインイベントを整理することで、システムが表すべき集約が自ずと見えてくる。
- 集約「予約」
- 集約「貸出」
- ....
ポリシー
ドメインイベントが発生した時に、特定の処理を実行すべきか否かを表すビジネスルールやロジックを表すもの。
例えば下記の例で言えば、「3.次の予約者に予約を割り当てた」には、次の予約者がいればという条件が隠されている。
- 本が予約された
- 本の予約がキャンセルされた
- 次の予約者に予約を割り当てた
このポリシーは条件を満たした時、アクターを介さずに自動的かつ連鎖的に次のイベントコマンドを実行する。
- 図書館利用者(アクター)が本の予約をキャンセル(コマンド)
- 本の予約がキャンセルされた(ドメインイベント)
- 次の予約者がいるか?(ポリシー)
true→次の予約者に本を割り当てる(コマンド)
false→処理なし
ソフトウェア設計例
整理した結果は下記のような形でソフトウェア設計として表現される。
//ドメインイベント定義
data class BookReservationCancelledEvent(
val id: Int,
val userId: Int,
val bookId: Int,
val eventDatetime: LocalDateTime,
)
//予約状態
enum class ReservationStatus {
PENDING,
CONFIRMED,
CANCELLED,
COMPLETED
}
//集約定義
data class BookReservation(
val id: Int,
val userId: Int,
val bookId: Int,
var status: ReservationStatus
) {
fun cancel(): BookReservationCancelledEvent {
status = ReservationStatus.CANCELLED
return BookReservationCancelledEvent(
id = 1, //実際にはちゃんと設定ロジックがあるはずだが今回は形だけ
userId = userId,
bookId = bookId,
eventDatetime = LocalDateTime.now()
)
}
}
//ポリシーを定義
class AssignNextReservationPolicy(
private val bookReservationRepository: BookReservationRepository,
) {
suspend fun isFulfill(
event: BookReservationCancelledEvent
): Boolean {
//次の予約者がいるかどうかの問い合わせ処理
}
}
//コマンド表現
data class ReserveBookCommand(
val userId: Int,
val bookId: Int,
)
//ユースケースを扱うアプリケーションサービス
class ReserveBookApplicationService(
private val bookReservationRepository: BookReservationRepository,
private val assignNextReservationPolicy: AssignNextReservationPolicy,
private val eventPublisher: EventPublisher
) {
suspend fun execute(command: ReserveBookCommand) {
//予約という集約を取得する
val bookReservation = bookReservationRepository.getBy(
command.userId,
command.bookId
)
//予約キャンセル実行してドメインイベント発生
val bookReservationCancelledEvent = bookReservation.cancel()
//集約のデータ永続化
bookReservationRepository.save(bookReservation)
//ポリシーを使って次のコマンドイベントを実行
if (assignNextReservationPolicy.isFulfill(bookReservationCancelledEvent)) {
//pub/subパターンで次のApplicationServiceを実行
eventPublisher.publish(bookReservationCancelledEvent)
}
}
}
class AssignNextReservationApplicationService(
private val bookReservationRepository: BookReservationRepository,
) {
suspend fun execute(event: BookReservationCancelledEvent) {
//次の予約者に割り当て
}
}
境界づけられたコンテキスト
同じ物、事であってもケースバイケースで必要な情報は異なる。商品に必要な情報が、販売側と運送側では異なるように、語られる文脈(コンテキスト)によって物事の性質(システム化する際は属性となる)は変化する。
にもかかわらずそれを同一のものとして扱おうとすると、ドメインモデルがぐちゃぐちゃになってしまい、非常に扱いづらくなってしまう。
であれば、コンテキストに従って別物として扱うことにしよう。
これが「境界づけられたコンテキスト」という考え方。
これだけ聞くとモデリングの技法のように思えるが、大事なのはドメインモデルを別物として分けることではなく、コンテキスト毎にグルーピングして境界線を引くこと。
例えば販売コンテキストと運送コンテキストでそれぞれのドメインモデルがグルーピングされるのであれば、それはシステムを分割する単位になる。
1アプリケーション内であればパッケージ分けに繋がり、分散型アプリケーションであれば販売と運送でそれぞれ1アプリケーションとして独立させることができる。
つまり、境界付けられたコンテキストを見出していくことで、複雑なドメインをいくつかに分割して整理することができることを意味する。
コンテキストマップ
境界付けられたコンテキストによってシステムはいくつかに分割される。ただし、同じドメインに属することに変わりはないため、コンテキスト同士の関係性をきちんと見出す必要がある。
その関係性をまとめたものがコンテキストマップである。
重要なのは、これが単に理解を深めたり状況を整理するためだけの知識ではなく、どういう体制で開発を進めて行かなければならないかという開発体制を明確化するための手段であるということ。
コンテキストマップはコンテキスト同士の関係性をもとに、実際の開発でどのように協働関係を築かなければならないかを示す。下記の組織パターンによって関係性が表現され、下記の統合パターンによって連携手段が表現される。
組織パターン
-
パートナーシップ
それぞれのコンテキストが成立しないとどちらも成り立たず、互いに協力が必要な関係性。この場合、計画や統合時の結合管理、リリースを共同で行う必要がある。両者のインターフェイスを明確化し、それぞれのコンテキストのニーズを達成できるようにする必要がある。 -
顧客/供給者
顧客(下流)に対して供給者(上流)から一方的なサポートが必要になる関係性。この場合、下流のニーズを上流が満たす必要があるため、下流の要件に必要な情報をもとに上流は計画を立てる必要がある。 -
順応者
実際には顧客/供給者の関係性にあるにもかかわらず、何らかの現実的な理由を起因(例えば既に構築済のレガシーなシステムと連動しなければならない、サードパーティのサービスを利用する必要があるなど)とし、供給者の協力を得られない関係性を表す。この場合、顧客側が供給者の都合に順応しなければならない。その際、モデルやインターフェースは順応者の都合通りに提供されるとは限らないため、後述の腐敗防止層を設けることが推奨される。 -
別々の道
コンテキスト間で連携がなく、完全に独立している場合を示す。
-
巨大な泥団子
統合パターンに分類されることが多いが、実質的には組織パターンに該当するものと感じたため、こちらに記載。
既存システムが大規模かつ複雑でコンテキストを見出すことも難しい状態の場合、そのまま大きな固まりとして捉えるというもの。その場合、この巨大な泥団子をモデリングすることは諦め、自分たちのコンテキスト境界を侵食してこないように配慮する必要がある。
統合パターン
- 共有カーネル
複数のコンテキスト、複数のドメインにおいて共有が必要な部分が存在する場合、ドメインモデルならびにそれに基づくソースコードを共有する連携手法。共有カーネルに変更が発生する場合は必ず他チームの承認が必要となる。
影響範囲を変に広くすると、それぞれのコンテキスト・ドメインの都合に振り回されるようになるため、最小限に留めるのが大事。 - 公開されたホストサービス(OHS)
コンテキストが公開するサービスを利用する連携手法。昨今であればRESTAPIなどでサービス間の連動を図ることが多い。複数のクライアントで利用する場合、それぞれのクライアントの都合が出てくる場面が多いので、適宜サービスの個別化などの検討が必要。 - 公表された言語(PL)
コンテキスト間でモデルを共有する場合、個々のコンテキスト都合で定義されたモデルを変換するための変換方式を定義する必要がある。これを公表された言語という。昨今であればJSONやXMLなどで表され、OHSと併用するのが一般的(いわゆるRESTAPIなどの手法)。 - 腐敗防止層(ACL)
パートナーシップや顧客/供給者のように関係性が良好である場合、公開されたホストサービスや公表された言語によって提供されるモデルをそのまま利用することができたり、変更が発生する場合は影響が出ないように配慮してもらうことができる。
ただし順応者の場合、既に公開されているインターフェースから必要な情報のみを切り出して取得する必要があり、順応者の都合に関わらず発生する変更に対応しなければならない。
そこで、データを送受信する際に自コンテキスト向けのモデルに変換してからシステムで取り扱うようにしよう、というのが腐敗防止層の考え方。
これによりインターフェイスに何らかの変更が発生したり、別のサービスを利用しなければならなくなった場合でも、自コンテキスト内の実装を傷つけずに済むようになる。
自分がドメイン駆動開発を面白いと感じるのは、順応者と巨大な泥団子という概念があり、必ずしもクリーンな関係だけでは進まない開発やシステムをいかに守るかもきちんと考えられていること。
現実の開発はなかなか理想的には進まないのが常ではあるが、その中でもなるべくクリーンなシステムを構築したいのが開発者の願いというものだろう。
ドメイン駆動開発はそこも含めて設計戦術としてまとめられているのがとても興味深い。
まとめ
ドメイン駆動開発では、下記の設計戦略により、適切なモデルや処理構造を明らかにし、ビジネスの変化に耐えうるシステム構築を目的としている。
- ユビキタス言語により、ドメインエキスパート(業務の専門家=顧客)と同じ表現を用いてシステムをモデル化していく
- イベントストーミングにより、ドメインで発生する状態変化に着目し、システムが扱うべき集約・条件・状態の遷移を明らかにする
- 境界づけられたコンテキストをもとにコンテキストマップを作成し、開発体制を明確化する
イベントストーミングはシステムをモデル化するにあたってとても面白いアプローチだと思ったので、ぜひ実践の中でやってみたいですね。
また、これまでの経験の中でもコンテキストマップの整理を意識できていたら、もうちょっと開発体制をスムーズにできたかなと思う場面があったので、そこら辺も今後に活かしていければと感じました。
ユビキタス言語やイベントストーミングなどのアプローチがあってこそ実装戦略を有効に適用できるものと感じたため、軽量DDD(実装戦略のみを適用した開発手法のこと)にならないよう意識していきたいと思います。
また自分の中の情報をアップデートできたら更新します。