前回の記事に引き続きエリック・エヴァンスのドメイン駆動設計(日本語訳版)を理解するための用語解説をしていこうと思います。
この記事では本書で語られる多くの背景や詳細を省いているので、この記事を読んだだけでは理解できないと思います。本書を読む前に読んでおくと理解がしやすく、後から読んだ時にどんなものだったかを思い出せるような内容にすることを目指しています。
今回は第4部「モデルの整合性を維持する」から、各パターンについて解説します。
本記事で出てくる「ドメイン」「モデル」「値オブジェクト」などの用語の意味については他の記事を参照してください。
- その1: ドメインモデル
- その2: しなやかな設計
- その3: モデルの整合性を保つためのパターン(この記事)
モデルの整合性を維持する
巨大なシステム開発の現場では、開発チームも異なれば、担当する分野も異なります。その中で巨大なシステムを単一のモデルの下に構築し、すべての開発者がそれに従うことは不可能です。そのため、システムのさまざまな部分で、複数の開発チームがそれぞれにモデルを構築し、可能な限りチーム単独で開発できるようにする必要があります。
複数のモデルを開発するといっても、構築するシステムの目的自体は共通です。複数の開発チームはそれぞれの持つモデルが他チームのモデルと協業可能な状態を維持しなければなりません。時には2つのモデルを統合することを決断したり、あるいはレガシーシステムの存在によりモデル化に深入りしない領域を定めたりするかもしれません。本書では、複数のモデル間の境界や関係性を決定したり、伝達するためのパターンが記されています。
モデルの整合性を維持するためのパターン
境界づけられたコンテキスト(BOUNDED CONTEXT)
巨大なプロジェクトでは同じような概念に対して複数のモデルが作られてしまうことがあります。この別々のモデルに基づくコードが組み合わされると、それぞれのコードが持つ背景が異なるためにバグの温床となり、さらには開発者間のコミュニケーションの混乱の元にもなります。
そうならないようにするために、モデルを切り分けるための境界線を定義します。チーム編成、アプリケーションの機能、あるいはコードベースやデータベーススキーマなどの物理的な観点を単位にして境界を定義します。この境界内(コンテキスト)ではモデルを一貫性のあるものに保たなければなりません。また、境界の外の問題によりこのモデルが影響を受けることがないようにする必要があります。そうでないと、このモデルを担当するチームが単独で作業できなくなってしまいます。
継続的な統合(CONTINUOUS INTEGRATION)
複数の人が同一の境界づけられたコンテキストで作業していると、モデルの解釈が異なっていたりしてモデルが分裂する可能性が出てきます。
それを検出するために、境界づけられたコンテキスト内のコードと成果物を頻繁にマージさせる工程を開発プロセスに組み込みます。そのとき、自動化されたテストも用意します。コードを頻繁にマージ/ビルド/テストすることで問題が発生したとき、チーム間でコミュニケーションが行われ、人々のモデルの解釈の統合も行われます。
コンテキストマップ(CONTEXT MAP)
個々の境界づけられたコンテキスト内で作業をする人は、他の境界づけられたコンテキストとの関係を知らないために、他のコンテキストが担っている役割を作業中のコンテキスト内に実装したりしてコンテキスト間の境界を曖昧にしてしまうことがあります。また、コンテキスト同士を接続するためのコードに知らずに手を加えてしまって接続が複雑になってしまうこともあります。
これを防ぐために、プロジェクトに存在している境界づけられたコンテキストをすべて洗い出し、明確に定義します。これには、レガシーシステムに含まれている暗黙的なモデルも対象になります。境界づけられたコンテキストそれぞれに名前をつけ、その名前をユビキタス言語の一部にします。さらに、モデル同士の接続箇所や接続に伴う変換について概略を述べるとともに、モデル同士で共有しているものがあればそれを強調します。これにより作業者が自分の作業がコンテキストマップ上のどこに位置しているかがわかるようになります。逆に、コードの一部に手をつけた時にそれがどのコンテキストに属するかがわかる必要があります。
共有カーネル(SHARED KERNEL)
複数のチームが好き勝手に作ったアプリケーションを統合しようとしてもうまく適合しません。結果的に変換処理の修正作業が発生して最初から継続的な統合を行っていた場合よりもコストが高くなります。しかし、チーム間ですべてのコードを継続的に統合するのはそれはそれで統合作業に伴う作業やコミュニケーションのコストが増える可能性があります。
そこで、コードのすべてではなく、一部を統合することを考えます。チーム間でモデルの一部を共有することに対して合意を取り、そのモデルの一部にあたるコードやデータベース設計を共有します。これらのコードはもう一方のチームの合意なしには変更できません。そのため、共有するモデルの部分については慎重に選択する必要があります。
顧客/供給者の開発チーム(CUSTOMER/SUPPLIER DEVELOPMENT TEAMS)
様々な場所から呼び出されるソフトウェアを担当しているチーム(上流チーム)は、ソフトウェアを変更する場合、ソフトウェアを利用する他チーム(下流チーム)に対して、変更に追従することを要求する面倒な手続きをしたり、あるいは下流チームが追従に拒否を示した場合に交渉に追われることになります。この状況では上流チームが身動きが取れなくなります。
そうならないために、予め2つのチーム間で明確な顧客/供給者という関係をとることに合意を得ておきます。期待されるインタフェースを検証するための自動化された受け入れテストを共同で開発し、そのテストを上流チームの自動化テストに組み込むことで、下流への副作用を心配せずに自由にソフトウェアを変更できるようになります。
順応者(CONFORMIST)
上流/下流関係があるチームにおいて、上流に下流チームの要求に応える動機がなければ、下流チームにはどうすることもできません。
そんな状況では、上流チームのソフトウェアに完全に依存することが選択肢になります。上流チームとの境界づけられたコンテキスト間の変換層をあえて設けないことで、複雑な変換処理を実装せずにモデルの統合を進められます。また、上流チームが用いるユビキタス言語をそのまま下流チームでも使うことで上流チームとのコミュニケーションを取りやすくします。しかし、設計は上流チームに制限されるし、上流チームの動きに振り回されるかもしれないので選択肢としては魅力がないので最後の手段かもしれません。
腐敗防止層(ANTICORRUPTION LAYER)
新しいシステムを構築していて、それがレガシーシステムとやりとりしなければならない場合、古いモデルと連携しやすくしようとするあまり、新しいモデルをレガシーシステムのモデルに似せようとその場しのぎの修正をしてしまうかもしれません。しかも、やりとりするシステムが複数あるとなれば、問題はさらに深刻化します。
それらの問題を対処するために、隔離用のレイヤ(腐敗防止層)を作成します。腐敗防止層では、外部のモデルのデータを自分のモデルで解釈できる形に変換します。外部システムから得られるデータは、そのコンテキスト内での意味を表現しているはずです。このデータを自分のコンテキスト内で使われるユビキタス言語で表現されたドメインオブジェクトに変換するのです。これにより、自分のコンテキスト内で扱いやすくするとともに、外部モデルの影響を隔離することができます。
注意しなければならないのは、腐敗防止層は外部システムへの単一方向のメッセージ送信の仕組みではないということです。腐敗防止層は2つのモデル間で双方向の変換を行います。上記の説明は新しいシステムからの視点であるように感じますが、レガシーシステムから見た場合にも当てはまります。なんだか大変そうな気がしますが、その予想通り腐敗防止層はそれ自体で複雑なソフトウェアになり得ます。その実装について下記で少し掘り下げます。
腐敗防止層の実装
腐敗防止層は通常、サービスの集合として実装されます。しかし、外部システムをそのようにサービス群として表現することがこちらのモデルにとって嬉しいことがないのならばその限りではありません。その場合はいくつかのサービスあるいはエンティティになるかもしれません。こちらのモデル内のドメインオブジェクトとしてエンティティを使っていたら、実は裏側では外部システムとの変換を行っていた、という具合です。
通常の腐敗防止層の構成は下記の図のようになります。新システムから外部システムとやりとりする場合の実装です。外部システム側にはそのシステムを扱いやすくするためのファサード(FACADE)を用意します。新システムは自身のモデルのユビキタス言語で表現されるサービスを通して外部システムからデータを取り出します。そのサービスではアダプタ(ADAPTER)を使います。これは厳密にアダプターパターンに従ったものではないので注意が必要です。ここでのアダプタは、サービスで定義したメソッドの役割と同等の処理を、外部システムを使って実現する場合どのようにファサードへリクエストすればよいか、というリクエスト作成方法を知っていることが求められます。アダプタが知っているのはあくまでリクエストの作成方法です。サービスから渡されてきた新システムのドメインオブジェクトを、リクエスト作成に必要なデータに変換するのはまた別の複雑な仕事なので、これには変換サービスを用います。
腐敗防止層は新システムと外部システムの双方向のやりとりができます。その際には、双方のシステムに独自のサービス、ファサード、アダプタを用意します。変換サービスは双方向の変換を行う同一のものを用意して共有することも考えられます。
通信リンクの位置
腐敗防止層は2つのシステム(別々のサーバを想定)を接続するため、上記の図のどこかに通信の仕組みを持たなければなりません。外部システムとファサードを直接統合できるなら、アダプタとファサードの間に通信リンクを置きます。統合できないなら、ファサードと外部システムの間にリンクを置きます。腐敗防止層全体を外部システムに統合する場合は、こちらのシステムと腐敗防止層のサービスの間にリンクを置きます。こうしたことは実装やデプロイに関する意思決定なので、自分たちの状況に応じて判断する必要があります。それがどうあっても腐敗防止層が果たす役割に変わりはありません。
別々の道(SEPARATE WAYS)
モデルの統合に伴うコストは高くつきます。多くの場合、統合することで特別な利益は得られません。1つの機能を構成する2つのシステムが互いに機能を呼び出したり、オブジェクトの相互連携を必要としたりしないのであれば、統合は必要ないかもしれません。
その場合には、境界づけられたコンテキストを、他とは一切つながりがないということを宣言し(コンテキストマップで宣言するのがいいかもしれません)、その小さいスコープ内に特化したロジックを構築することも選択肢になります。別々の道を選択したとしても、それぞれの機能を呼び出す外部向けインタフェースをまとめておくこともできます。しかしデータ転送は可能な限り減らすことが望ましいです。
公開ホストサービス(OPEN HOST SERVICE)
あるシステムをその他多くの外部システムと統合しなければならない場合、それぞれに対して専用の変換サービスを作成していると、保守によりチームが停滞してしまうかもしれません。もしも外部システムからの要求に一貫性があるなら、その部分を多くの外部システムからの要求を網羅するサービス群として共通化できるかもしれません。
その場合には、こちらのシステムを操作できるようにするプロトコルを、サービスの集合として定義し、そのプロトコルを公開してこちらのシステムを統合したい人が全員使用できるようにします。共有プロトコルは新しい統合の要件に対応するために機能追加されていきますが、特定のシステム専用の要求には専用の変換サービスを用意してプロトコルを拡張し、共有プロトコルは単純で一貫性のある状態に保ちます。
複数のチームが理解して使用できるくらいに整理されたプロトコルを設計するのはかなり困難です。そのコストに見合う効果が得られるのは、こちらのシステムのリソースが外部に公開して操作できるほどに整理されている場合と、相当な数の統合がある場合だけです。
公表された言語(PUBLISHED LANGUAGE)
一方のドメインモデルを他のモデルとのやりとりするときのデータ形式として採用すると、その複雑さから開発のスピードが落ちたり、そのモデルに変更を加えたくても他モデルから使われているため自由に変更できなかったりと身動きが取れなくなってきます。
そうならないために、他モデルとのデータ交換に利用できる共有言語を使用する必要があります。共有言語はドメインの情報を表現できるものであり、明確にドキュメント化している必要があります。この共有言語からモデルで扱える形式に変換したり、逆にあるモデル表現を共有言語の形式に変換して他モデルとやりとりをします。共有言語は自分で一から作るか、すでに世の中にある形式が利用できます。例えば、主要なDBが持つインタフェースや、その業界で定められた形式(XMLを利用した化学用マークアップ言語など)があります。これらをすべて採用しなくてもその一部を流用すれば十分かもしれません。
おわりに
どうすればモデルの整合性を保って開発できるか、というパターンについて紹介しました。せっかく構築したモデルが他システムとの連携に引きずられて歪んでいくことのないよう、これらのパターンが手段としてあるということを覚えておくと役立つ時が来るかもしれません。
本書では、ここで省かれた背景や実例が多く記載されているので読んでしっかり理解しましょう。