次の方法で共有


ベスト プラクティス

ドメイン駆動設計の概要

David Laribee

この記事では、次の内容について説明します。

  • ユビキタス言語からモデル化する
  • コンテキスト境界と集計ルート
  • 単一責任の原則を使用する
  • リポジトリとデータベース
この記事では、次のテクノロジを使用しています。
Visual Studio

目次

プラトン的モデル
言うべきことを言う
コンテキスト
価値提案を把握する
単一責任システム
エンティティは ID とライフサイクルを持つ
値オブジェクトは記述する
集計ルートによりエンティティを結合する
ドメイン サービス モデルの主要な操作
リポジトリにより集計ルートを省略する
データベースの関連事項
DDD の使用を開始する

ドメイン駆動設計 (DDD) とは、洗練されたオブジェクト システムの設計に役立つ原則とパターンをまとめたものです。設計に DDD を適切に適用することで、ドメイン モデルと呼ばれるソフトウェア抽象化を実現できます。このモデルにより複雑なビジネス ロジックをカプセル化できるため、実際の業務とコードとの間に存在するギャップを小さくすることができます。

この記事では、DDD に関連する基本的な概念と設計パターンについて解説します。機能豊富なドメイン モデルを設計し、増強していくための、簡単な紹介記事と考えてください。説明上の背景についてですが、ここでは複雑なビジネス ドメインを使用し、主に保険ポリシーを扱っているとします。

ここで紹介されているアイディアに興味を抱いた場合は、ぜひ『Domain-Driven Design: Tackling Complexity in the Heart of Software』(Eric Evans 著) を読み、知識を深めることをお勧めします。この書籍は DDD に関する解説本の草分けですが、それ以上に、業界で最も経験豊かなソフトウェア開発者の 1 人が披露する情報の宝庫でもあります。この記事で紹介する DDD のパターンや原則は、この書籍で解説されている概念を参考としたものです。

アーキテクチャ上の必要からコンテキストを切り分ける

コンテキスト境界は、必ずしもアプリケーションの機能領域だけで整理されるわけではありません。コンテキスト境界は、システムを分割することで目的のアーキテクチャを実現する場合に、非常に有用です。この手法の古典的な例としては、堅牢なトランザクション フットプリントとレポート ポートフォリオの両方を備えたアプリケーションが考えられます。

そのような環境 (非常によく見られます) の場合、レポート データベースをトランザクション データベースから分離することが望ましい場合がよくあります。信頼性の高いレポートを作成するには適切な正規化を進める必要があり、さらにトランザクション ビジネス ロジックのコードをオブジェクト指向パラダイムで作成し続けられるようにオブジェクト リレーショナル マッパーを使用する必要があります。Microsoft メッセージ キュー (MSMQ) などのテクノロジを利用して、モデルからのデータ更新を発行し、それをレポート用と分析用に最適化されたデータ ウェアハウスに組み込むことができます。

これにはショックを感じる人もいるでしょう。しかし、データベースの管理者や開発者であれば、なんとか対応できるはずです。コンテキスト境界を利用して、この理想の環境に一歩近づくことができます。アーキテクチャ上のコンテキスト境界に興味がある場合は、Greg Young のブログをチェックすることをお勧めします。Greg Young はこのような手法に精通しており、このテーマに関して多くの記事を執筆しています。

プラトン的モデル

まだ解説を始めたばかりですから、私が "モデル" という言葉をどのような意味で使用するのか説明しても、無駄ではないでしょう。この疑問に答えるためには、短い哲学の旅に出る必要があります。案内役としてふさわしい人は、プラトンしか考えられません。

ソクラテスの弟子の中で最も有名なプラトンは、概念、人間、場所、直感や感覚で理解する事物は、真実の影に過ぎないという説を唱えました。プラトンは、この本当の事物という概念を "フォーム (形式)" と名付けました。

フォームを説明するために、プラトンは洞窟の寓話として知られるたとえ話を使いました。この寓話では、暗闇の洞窟の奥深くに数人の人間が閉じこめられています。洞窟の中の人たちに見えるのは、洞窟の出口から届く光が当たる 1 つの壁面のみです。出口の前を動物が横切ると、その壁に影が映ります。洞窟の中の人にとっては、その影こそが実体となります。出口の前をライオンが横切ると、彼らはライオンの影を指さして "逃げろ!" と叫びます。しかし、それは現実には実際のフォーム (ライオンそのもの) ではなく、その影に過ぎないのです。

プラトンによるフォームの理論は、DDD にも適用できます。この理論の教えは、理想のモデルに少しずつ近づくために役立ちます。あなたがコードを使用して表現しようとしているフォームに至る道は、ドメイン専門家の頭の中や、関係者の要望の中、これまで取り組んできた業界の要件の中に散在しています。これこそが、本当の意味で、プラトンの想像上の洞窟に映し出される影なのです。

さらに、そのフォームに到達しようとしても、プログラム言語や所要期間、予算などの制限を受けることがよくあります。このような制限は、影の映る壁しか見えない洞窟の住人が受ける制限と同じであると言っても過言ではないでしょう。

優れたモデルは、その実装に依存しない属性を数多く備えています。つまり、ドメイン モデルを作成しようとする人がまず理解する必要があるのは、頭の中にあるモデルと、コードとして作成するモデルの間にある不整合なのです。

あなたが作成するソフトウェアは、本当のモデルではありません。それは、実現しようとしたアプリケーションの "フォーム" が顕現したものに過ぎず、影と呼んでもかまいません。ただし、それが完璧なソリューションのまがい物だとしても、時間をかけてそのコードを本当の "フォーム" に近付けていくことができます。

DDD では、このような概念をモデル駆動設計と呼びます。モデルの理解は、コードを作成する中で深められます。ドメイン駆動設計では、設計者は大量のドキュメントや複雑なダイアグラム作成ツールに悩まされることはありません。その代わりに、ドメインの理解をコードに直接反映させることに集中できます。

モデルを取り込んだコードという概念は、DDD の基幹をなします。目前の問題に的を絞り、その問題の解決に限定するようにソフトウェアを維持することで、新しい洞察や発想をソフトウェアに盛り込みやすくなります。このことを表した言葉で、私が気に入っているのが、"知識をかみ砕いてモデルにする" という Eric Evans の言葉です。ドメインに関する重要事項を学習すれば、進むべき方向がわかるでしょう。

言うべきことを言う

DDD によって得られる、この目標を達成するための手法を見てみましょう。開発者としての仕事で大きな位置を占めるのは、コードを作成しない人もこちらの意図を理解できるようにしながら作業することです。何らかのプロセスを使用して組織内で作業する場合、ユーザー ストーリー、タスク、またはユーザー ケースと呼ばれる要件が与えられることがあります。与えられた要件または仕様が何であれ、それが完璧であることはあまりありません。

一般的に、要件はあいまいな内容であったり、概要的な内容で表されたりします。これが有効なのは、ソリューションの設計と実装を行う過程で、対象のドメインに専門的技術をもたらす人物に開発者が接触できる場合です。それこそが、ユーザー ストーリーの要点です。通常、ユーザー ストーリーは、たとえば "[役割] として、[利点] が実現できるように [機能] が必要です" などというテンプレートに従って表現されます。

保険ポリシー管理のドメインから、例を挙げてみましょう。たとえば、"保険業者として、安全なエクスポージャを作成し、危険なエクスポージャを排除できるように、ポリシーに対する承認をコントロールする必要がある" というテンプレートが考えられます。

この文章の意味を理解できる人はいるでしょうか。私自身も、重要と位置付けられたこの文章を見たとき、理解できませんでした。この抽象的な説明から、サポート対象ソフトウェアの設計を開始するために必要となるすべてを理解できるでしょうか。

適切に作成されたユーザー ストーリーは、その作成者、つまりユーザーと話し合うきっかけとなります。つまり、承認または拒否を行うポリシーの機能に関する作業を開始するときに、理想としては保険業者に接触する必要があるということです。知識のない開発者にとって、保険業者は、特定の分野のエクスポージャが保険会社にとって安全であるかどうかを判断できるドメイン専門家なのです (専門家とまで行かなくても、少なくとも知識は持っています)。

機能について保険業者 (または任意のプロジェクト ドメイン専門家) と話し合いを始めるときには、保険業者の使用する用語に特に注意してください。このようなドメイン専門家は、社内や業界内で一般的となっている用語を使います。DDD では、この語彙をユビキタス言語と呼びます。開発者はこの語彙を理解し、ドメイン専門家との会話に使うだけでなく、コードにも同じ用語が反映されるようにする必要があります。"階級コード"、"保険料セット"、"エクスポージャ" などの用語が会話で頻繁に使われた場合、コードでも対応するクラス名を使用することになるでしょう。

これは、DDD における非常に基本的なパターンです。一見、ユビキタス言語はわかりやすいものに感じられます。多くの人は、これを既に直感的に実践しているでしょう。しかし、このようなビジネス言語をコード内で意識的に、厳格な規則として使用することが極めて重要なのです。そうすることで、ビジネス用語と技術用語のかい離を減らすことができます。手段を目的に従属させ、"ビジネス価値の提供" という本来の職務から離れずに済みます。

コンテキスト

ある意味で、開発者は組織化の責任者です。問題を解決するために、コードを (できれば意図的に) 抽象的概念としてまとめ上げる必要があります。デザイン パターン、階層型アーキテクチャ、オブジェクト指向原則などのツールは、果てしなく複雑化したシステムに秩序を与えるフレームワークとなります。

DDD によって、組織化の手段が充実し、業界で広く知られるパターンを借用できるようになります。DDD によって適用される組織化パターンの最も大きな利点は、システム内のすべての詳細レベルに対応するソリューションが存在することです。コンテキスト境界によって、ソフトウェアをモデルのポートフォリオとして考えられるようになります。モジュールを利用することで、大きな 1 つのモデルを小さな単位に組織化することができます。この記事の後半では、集計ルートを使って、関連性の高い少数のクラスの間で小規模のコラボレーションを組織化する方法について説明します。

ほとんどのエンタープライズ システムには、責任の割り当て粒度の粗い領域が存在します。DDD では、この最上位の組織化を "境界コンテキスト" と呼びます。

労働者補償保険のポリシーは、次のような要素と関連付ける必要があります。

  • 見積り販売
  • 全体的なポリシーのワークフロー (更新、解約)
  • 給与見積の監査
  • 4 半期ごとの自己見積
  • 料金の設定と管理
  • 代理店および仲介業者への手数料の支払い
  • 顧客への請求
  • 一般会計
  • 許容されるエクスポージャの決定 (保険責務の引き受け)

すばらしい。こんなにたくさんあるなんて。このすべてを一体型のシステムに盛り込むことは可能です。しかし、そうした場合、霧に包まれて先の見えない道に迷い込むことになります。全体的なワークフローのコンテキストでポリシーについて話し合う場合と、給与監査のコンテキストでポリシーについて話し合う場合では、その内容はまったく異なっています。同じ 1 つのポリシー クラスを使用した場合、そのクラスのプロファイルは肥大化し、単一責任の原則 (SRP) などの実績ある手法から遠く離れていくことになります。

コンテキスト境界の分離と隔離に失敗したシステムは、"大きな泥だんご" と呼ばれる (面白い名前の) アーキテクチャ スタイルに陥ることがよくあります。1999 年に、Brian Foot と Joseph Yoder が同名の論文 (『Big Ball of Mud (大きな泥だんご)』) の中で、このアーキテクチャ スタイル (反アーキテクチャ スタイルと見なされることもあります) を定義しました。

DDD を使用すると、コンテキストを識別し、モデル化作業を特定のコンテキストに限定するようになります。コンテキスト マップと呼ばれる単純なダイアグラムを使用して、このシステムの境界を調べることができます。すべての機能を備えた保険ポリシー管理システムに含まれるコンテキストは、前に列挙したとおりです。図 1 は、文章による説明の一部をグラフィカルなコンテキスト マップにしたものです。

fig01.gif

図 1 コンテキスト境界からコンテキスト マップへ

さまざまな境界コンテキストの間に、重要な関係がいくつかあることに気が付きましたか。これは価値の高い情報です。なぜなら、状況をしっかりと把握してビジネス上の決定とアーキテクチャ上の決定を下せるからです。このような決定事項としては、パッケージと展開の設計や、モデル間のメッセージのマーシャリングに使用するテクノロジの選択などがありますが、最も重要であるのは、マイルストーン設定や作業、時間、および人材の配置を選択する決定です。

最後に、境界コンテキストに関する非常に重要な考察を紹介します。それは、それぞれのコンテキストが、それぞれ独自のユビキタス言語を持つということです。監査サブシステムのポリシーの概念と、コア ワークフローのポリシーの概念を区別することは非常に重要です。なぜなら、この 2 つは異なるものであるからです。ID が同じでも、多くの場合は値オブジェクトや子エンティティ (後で説明します) がまったく異なっています。コンテキスト内でモデル化を行うため、ドメイン専門家との間やチーム内で生産的なコミュニケーションが実現するように、そのコンテキスト内での正確性を得るためのユビキタス言語も必要となります。

モデル内の一部の領域は、相互に密接にグループ化されます。モジュールは特定のコンテキスト内でこれらのグループを組織化する手段であり、小さな境界として機能します。そこでは、他のモジュールへの関連付けを検討する必要があります。モジュールは、"小さな泥だんご" に陥ることを防ぐもう 1 つの組織化手法でもあります。技術的には、モジュールは簡単に作成できます。Microsoft .NET Framework 内では、単なる名前空間です。しかし、モジュールを識別するコードの作成には、少し時間がかかります。最終的に、モデル内にいくつかの小型のモデルが現れてくることがあります。このような場合、それを複数の名前空間に分割することを検討しても良いでしょう。

モデルを分割してモジュールのまとまりを作成することは、IDE において有効です。つまり、複数の using ステートメントを使用してモジュールを明示的にインクルードする必要があるため、IntelliSense での作業がはるかに明確化されます。さらに、NDepend などの静的分析ツールを使用して、システムのより大きな概念的まとまりの間の関連性を調べることもできます。

モデルの組織に変更を加えることによって、実践的で費用対効果に優れた計画になっていくはずです。モジュール (または名前空間) を使用してモデルを分割する場合、独立したコンテキストを扱っているかどうか検討する必要があります。通常、別のコンテキストを切り離すと、コストはさらに高くなります。モデルが 2 つになるだけでなく、それが 2 つのアセンブリ内に存在することも多く、アプリケーション サービスやコントローラなどを使用してそれを接続する必要があるためです。

腐敗防止層

腐敗防止層 (ACL) は、ドメイン以外の概念がモデルに紛れ込むことを防止するゲートキーパーの作成に役立つ DDD パターンの 1 つです。ACL により、モデルが清浄に保たれます。

基本的には、リポジトリも一種の ACL です。リポジトリによって、SQL や オブジェクト リレーショナル マッピング (ORM) の構成要素をモデルの外部にとどめられます。

ACL は、Michael Feathers が自著の『Working Effectively With Legacy Code』で説明している "シーム (seam)" を導入するために、非常に有効です。シームとは、レガシー コードを切り離し、変化を導入する境界となる領域です。DDD の手法を使用し、コードの最も価値のある部分をリファクタリングして強化する上で、シームを見つけてコア ドメインを隔離することは、非常に効果的である可能性があります。

価値提案を把握する

ほとんどの開発組織では、経験豊富なビジネス担当者や能力の高い開発者は数人しかいません。問題を切り分けて解説し、保守しやすい洗練されたオブジェクト指向ソリューションを構築する能力を持つ開発者は限られています。顧客から最大限の報酬を得るには、アプリケーションのコア ドメインを確実に理解することが必要です。コア ドメインは、DDD の適用に最大の価値をもたらす境界コンテキストです。

どのようなエンタープライズ システムにも、他と比較して重要性の高い領域が存在します。重要性の高い領域は、顧客の中心的な能力と関連がある傾向があります。企業で専用設計の総勘定元帳ソフトウェアを運用することは、ほとんどないでしょう。しかし、その企業が保険業界に属していて (前に示した例と一致します)、責任を会員全員で分散するというリスク プールの管理によって利益を上げている場合には、危険なリスクを排除することと、傾向を識別することに長けていることが必要になります。また、たとえば顧客が医療保険金請求会社である場合、その戦略は、支払いの自動化により請求金額支払い従業員を支援することで、競合企業と同じ価格を実現するというものになります。

どのような業界であれ、自社または顧客が市場である程度の強みを持っているのであれば、通常はその強みのある領域にカスタム ソフトウェアが存在します。そのカスタム ソフトウェアに、モデル化するコア ドメインが含まれている可能性があります。

投資の価値は別の次元、つまり優秀な技術を実現するために知的資本を投資するという次元から評価できます。上級開発者の多くは、新しいテクノロジに夢中になる人種です。そのため、革新的な技術が絶え間なく開発され、ベンダは顧客の要望に応えて競争力を維持するために新しいテクノロジを使った製品を頻繁にリリースせざるを得ません。上級開発者が取り組まなくてはならない課題は、私が理解する限り、システムの基本部分の価値を高める根本的な原則とパターンを習得することです。新しいフレームワークやプラットフォームを導入することは魅力的ですが、ベンダがそのような技術を生み出す理由は、それが機能することを単純に信頼させるためであることを忘れないようにする必要があります。

単一責任システム

前に説明したように、DDD では機能豊富なドメイン モデルを構築するためのパターン言語を使用できます。パターンを実装することで、ある程度までの SRP の遵守を無償で実現できます。これは間違いなく価値があります。

SRP は、インターフェイスまたはクラスの根本的な目的を達成するために役立ちます。SRP を利用して、高い凝集度を実現できます。凝集度を高めることは、コードの発見、再利用、および保守が容易になるという大きな利点があります。

DDD により、中心的なパターンの集合の中で、特定の種類のクラス責任が認識されます。この記事では最も重要なパターンのみをいくつか取り上げますが、Eric Evans の著書の中では、クラス レベルからアーキテクチャ レベルまで、多数のパターンが紹介されています。ここでは入門的な内容にするため、エンティティ、値オブジェクト、集計ルート、ドメイン サービス、リポジトリに対応するクラス レベルのみを説明します。これは紹介記事ですから、各パターンの責任について、それぞれ 1 ~ 2 個のコード例とヒントのみを使って説明します。

エンティティは ID とライフサイクルを持つ

エンティティは、システム内の "もの" です。エンティティは多くの場合、人、場所、そしてつまり、"もの" を指す名詞として考えると便利です。

エンティティは ID とライフサイクルの両方を持っています。たとえば、システム内で特定の顧客にアクセスする場合、その顧客を番号で指定することができます。取引注文が完了すると、システムは停止し、長期ストレージにデータを保存します (履歴レポート システム)。

エンティティは、データの単位ではなく動作の単位として考えてください。動作を所有するエンティティにロジックを追加します。ほとんどの場合、モデルに追加する予定の操作を受け取るエンティティが存在するか、新しいエンティティの作成または抽出が求められます。機能の少ないコードであれば、外部からのエンティティを検証するサービス クラスやマネージャ クラスが多数存在します。一般的に、これはエンティティの内部から実行する方がずっと好ましいと思われます。カプセル化の基本的原理と関連する利点をすべて活用し、エンティティを動作にすることになるからです。

エンティティ間に依存関係を作ることで、問題が生じる場合があります。システム内のさまざまなエンティティの間に関連付けを作成する必要があるのは明らかです。たとえば、ポリシーにおける実用的な既定値を判断するために、"ポリシー" エンティティから "製品" エンティティを取得する必要がある場合が考えられます。エンティティの本質的な動作を実行するために外部のサービスが必要な場合、失敗することが多いようです。

個人的には、エンティティでない他のクラスを含める必要があっても困ることはありません。自分のエンティティの外部にある中心的な動作を撤廃することは避けようとします。エンティティは、本質的に動作の単位であることを常に意識してください。多くの場合、動作は一種のステート マシンとして実装されます。エンティティに対してコマンドを呼び出すときには、これによって内部状態が変更されます。ただし、追加データを取得する必要があることや、外部に副作用が発生することもあります。これを実現する方法としては、コマンド メソッドに対する依存関係を作成することをお勧めします。

public class Policy
{
      public void Renew(IAuditNotifier notifier)
      { 
            // do a bunch of internal state-related things,
            // some validation, etc. ...
            // now notify the audit system that there's
            // a new policy period that needs auditing notifier.ScheduleAuditFor(this);
      }
}

 

 

 

この方法の利点は、制御の反転 (IOC) コンテナを使用してエンティティを作成する必要がないことです。もう 1 つ、個人的に文句なく許容できる方法は、サービス ロケータを使用して、メソッド内の IAuditNotifier を解決することです。この方法にはインターフェイスをきれいな状態に維持できるという利点がありますが、最初の方法の方が、高いレベルの依存関係をより詳細に把握できます。

値オブジェクトは記述する

値オブジェクトは、モデル化するドメインで重要である記述子またはプロパティです。エンティティとは異なり、ID を持ちません。ID を持つ要素をただ説明するのみです。たとえば、"35 ドル" という名前のエンティティを変更することや、銀行口座の残高をインクリメントすることは、普通考えられません。

値オブジェクトの利点の 1 つは、非常に洗練された、意図が明確な方法でエンティティのプロパティを記述できることです。一般的な値オブジェクトである金額を資金送金 API で扱う場合、数値よりも関数パラメータの方がはるかにわかりやすくなります。インターフェイスやエンティティ メソッドで値オブジェクトを見つければ、すぐに自分が何を扱っているのか判別することができます。

値オブジェクトは不変です。作成後に変更を加えることはできません。なぜ、不変であることが重要なのでしょうか。値オブジェクトを使用すれば、副作用のない機能 (これも、DDD から借用された概念です) が実現できます。20 ドルに 20 ドルを加えることによって、元の "20 ドル" は変化するでしょうか。いや、変化しません。"40 ドル" という新しい金額記述子が作成されます。C# では、パブリック フィールドに読み取り専用キーワードを使用することで、副作用のない不変の関数を作成できます。図 2 を参照してください。

public class Money 
{ 
      public readonly Currency Currency; 
      public readonly decimal Amount; 
      public Money(decimal amount, Currency currency) 
      {       
            Amount = amount; Currency = currency; 
      } 
      public Money AddFunds(Money fundsToAdd) 
      {      
             // because the money we're adding might 
            // be in a different currency, we'll service 
            // locate a money exchange Domain Service. 
            var exchange = ServiceLocator.Find<IMoneyExchange>(); 
            var normalizedMoney = exchange.CurrentValueFor(fundsToAdd, this.Currency); 
            var newAmount = this.Amount + normalizedMoney.Amount; return new Money(newAmount, this.Currency); 
      }
}
public enum Currency { USD, GBP, EUR, JPY }

集計ルートによりエンティティを結合する

集計ルートとは、コンシューマから直接参照される特殊なエンティティです。集計ルートを指定すると、単純な規則がいくつか適用され、モデルを構成するオブジェクトの過剰な結合を避けることができます。集計ルートでは、それ自体のサブエンティティが強力に保護されることに注意してください。

重要な規則として注意が必要なことは、集計ルートはソフトウェアが参照を維持できる唯一のエンティティの種類であることです。これにより、すべての要素が他のすべての要素に関連付けられるという、強固に結合されたシステムを作成できなくする制限が加えられるため、"大きな泥だんご" の防止に役立ちます。

たとえば、Policy (ポリシー) という名前のエンティティがあるとします。ポリシーは毎年更新されるため、Period (期間) という名前のエンティティも存在します。Period は Policy がなければ存在できず、Period の操作は Policy を通じて行われるため、Policy は集計ルートであり、Period はその子供であると言えます。

集計ルートは、その使いやすさからお勧めできます。次に示すコンシューマ コードを例に考えてみましょう。このコードでは、Policy 集計ルートにアクセスします。

Policy.CurrentPeriod().Renew()

ここでは、保険ポリシーの更新を行います。保険ポリシー管理のコア ドメインのクラス ダイアグラムを思い出してください。動作を呼び出すまでに、どれほど多くの参照を使用しているかに注目してください。

この方法にはいくつかの問題があります。まず、明らかにデメテルの法則に違反しています。オブジェクト O のメソッド M は、特定の種類のオブジェクト (それ自体、そのパラメータ、それによって作成またはインスタンス化されるオブジェクト、直接のコンポーネント オブジェクト) のみを呼び出す必要があります。

この方法は、利便性を求めた過剰な参照ではないでしょうか。IntelliSense は Visual Studio などの最新の IDE で最も便利な機能の 1 つですが、実際に呼び出す機能を実現するために参照を作りすぎると、システムに不必要な結合がもたらされます。前の例の場合、Policy クラスと Period クラスに依存することになります。

詳細については、Brad Appleton が自分のサイトで優れた記事を公開していますので参照してください。デメテルの法則の意味、理論、使用法、注意事項などが詳細に解説されています。

"千の傷による死 (death by a thousand cuts)" という言い回しは、結合が過剰なシステムにおける悪夢のようなメンテナンス作業をうまく表現しています。不必要な参照をシステム全体に作成した場合、硬直的なモデルが作成されることになり、1 か所に変更を加えるとその影響が連鎖的に広がって、コンシューマ コード全体に変更が発生します。同じ目的は、私の知る限り、もっと表現力の高いコードを使うことで達成できます。

Policy.Renew()

集計ルートの実行する処理がわかるでしょうか。集計ルートは、現在の期間を探すだけでなく、既に更新期間が存在しているかどうかなど、必要な処理に必要な情報をすべて探します。

振舞駆動開発 (BDD) などの手法を使用して集計ルートの単体テストを実行すると、そのテストはブラックボックス化された状態テストのパラダイムに近づく傾向があることがわかります。集計ルートとエンティティは、多くの場合、最終的にはステート マシンとなり、それに応じた動作が適用されます。ここでは、最終的にステートの検証、加算、および減算となります。図 3 の更新の例では、かなり多くの動作が存在しています。これを BDD 形式のテストとして表す方法は、非常に明白です。

public class When_renewing_an_active_policy_that_needs_renewal 
{ 
      Policy ThePolicy; 
      DateTime OriginalEndingOn; 
      [SetUp] public void Context() 
      { 
            ThePolicy = new Policy(new DateTime(1/1/2009)); 
            var somePayroll = new CompanyPayroll(); 
            ThePolicy.Covers(somePayroll); 
            ThePolicy.Write(); 
            OriginalEndingOn = ThePolicy.EndingOn; 
      } 
      [Test] public void Should_create_a_new_period() 
      { 
            ThePolicy.EndingOn.ShouldEqual(OriginalEndingOn.AddYears(1)); 
      } 
}

ドメイン サービス モデルの主要な操作

ドメインには、ID やライフサイクルを持たない操作や処理が含まれる場合があります。ドメイン サービスには、このような概念をモデル化するためのツールが用意されています。一般的に、ドメイン サービスはステートレスで凝集度が高く、単独のパブリック メソッドを備えていることが多く、セットで処理する場合は過負荷になることもあります。

サービスの使用をお勧めする理由はいくつかあります。動作に複数の依存関係が関わっており、その動作を配置する適切な場所がエンティティ上に見つからない場合に、私はサービスを使用します。ユビキタス言語で処理または操作が一次的概念として扱われる場合、私はサービスがメソッドのオーケストレーションの中心点として機能するかどうかを質問します。

ドメイン サービスは、更新の際に使用できます。これは代替的な形式です。IAuditNotifier を Policy エンティティの Renew メソッドに直接埋め込む代わりに、依存関係の解決を処理するドメイン サービスを抽出することができます。エンティティを使用するよりも、IOC コンテナからドメイン サービスを解決する方が無理がありません。この手法は、複数の依存関係が存在するときにこそ有効ですが、いずれにせよ、ここではこの代替的な手法を紹介します。

次に、ドメイン サービスの簡単な例を示します。

public class PolicyRenewalProcesor 
{ 
      private readonly IAuditNotifier _notifier; public PolicyRenewalProcessor(IAuditNotifier notifier) 
      { 
      _notifier = notifier; 
      }
      public void Renew(Policy policy) 
      { 
            policy.Renew(); _notifier.ScheduleAuditFor(policy); 
      } 
}

.

 

"サービス" という言葉は、開発業界では非常によく使われる用語です。サービスは、サービス指向アーキテクチャ (SOA) と見なされることもあります。また、サービスは、特定の人物、場所、物を表さずに、プロセスを実現することが多い、アプリケーション内の小さなクラスとも考えられます。通常、ドメイン サービスは後者に分類されます。多くの場合、ドメイン サービスには、ドメイン専門家がユビキタス言語として導入する動詞やビジネス アクティビティの名前が付けられます。

一方、アプリケーション サービスは、階層型アーキテクチャを実現するために非常に適しています。ドメイン モデル内のデータをクライアント アプリケーションで求められる形状にマッピングするために使用できます。たとえば、DataGrid に表形式データを表示する必要があるが、モデル内の粒度が粗くギザギザのオブジェクト グラフはそのまま維持する必要があるときなどに有効です。

アプリケーション サービスは、複数のメソッドを統合する際にも非常に役立ちます。たとえば、ポリシー監査とコア ポリシー ワークフローの間の変換などです。同様に、インフラストラクチャの依存関係の統合に使用できます。Windows Communication Foundation (WCF) を使用してドメイン モデルを公開するという、よくあるシナリオを考えてみましょう。WCF 属性で修飾されたアプリケーション サービスを使用して、WCF が純粋なドメイン モデルにリークしないようにしながら、このシナリオを実現します。

アプリケーション サービスは非常に広く浅くなる傾向があり、凝集力のある機能を実現します。図 4 に、アプリケーション サービスの好例を示します。このコードで、インターフェイスと部分実装を検討してみましょう。

public IPolicyService 
{ 
      void Renew(PolicyRenewalDTO renewal); 
      void Terminate(PolicyTerminationDTO termination); 
      void Write(QuoteDTO quote); } 
      public PolicyService : Service 
      { 
      private readonly ILogger _logger; public PolicyService(ILogger logger, IPolicyRepository policies) 
      {
             _logger = logger; _policies = policies; 
      } 
      public void Renew(PolicyRenewalDTO renewal) 
      { 
            var policy = _policies.Find(renewal.PolicyID); 
            policy.Renew(); 
            var logMessage = string.Format( "Policy {0} was successfully renewed by {1}.", Policy.Number, renewal.RequestedBy); _logger.Log(logMessage); 
      } 
}

リポジトリにより集計ルートを省略する

エンティティは、どこから取得するのでしょうか。どのように保存するのでしょうか。その疑問に答えるのが、Repository パターンです。リポジトリは、メモリ内のコレクションを表し、一般的には 1 つの集計ルートにつき 1 つのリポジトリを使用します。

リポジトリは、スーパー クラスの候補として適しています。これは、Martin Fowler が Layer Supertype パターンと呼んでいるものに相当します。ジェネリックを使用して、前の例から基本となるリポジトリ インターフェイスを派生させるのは簡単です。

public interface IRepository<T> where T : IEntity 
{ 
      Find<T>(int id); Find<T>(Query<T> query); 
      Save(T entity); Delete(T entity); 
}

 

リポジトリにより、SQL ステートメントやストアド プロシージャなどのデータベースの概念やデータ保持の概念がモデルに混入することを防ぎ、当面の目的である "ドメインのキャプチャ" から注意がそらされないようにします。このように、モデル コードとインフラストラクチャを分離するという優れた特性があります。詳細については、「腐敗防止層」を参照してください。

ここまでで、集計ルートとその下位のエンティティ、およびアタッチされた値オブジェクトが実際にどのようにディスクに保存されるのかについて、まだ説明していないことに気付いたでしょうか。敢えて説明しませんでした。モデル内の動作を実行するために必要なデータを保存することは、モデル自体とはあまり関係のない問題です。データ保持は、インフラストラクチャだからです。

このようなテクノロジについて概説することは、DDD の紹介というこの記事の目的を超えています。ここで説明できることは、オブジェクト リレーショナル マッピング (ORM) フレームワーク上のモデルのデータをドキュメント指向データベースに保存できるように、適切なオプションが多数用意されており、単純なシナリオに対応するデータ マッパーを "自作" する際に利用できる、ということです。

DDD の関連情報

DDD 公式サイト

ユーザー ストーリーの構築方法 (Dan North のブログ)

"Big Ball of Mud (大きな泥だんご)" アーキテクチャ スタイル

CodeBetter の Greg Young のブログ

単一責任の原則に関する Robert C. Martin の論文

Brad Appleton による "デメテルの法則" に関する記事

Martin Fowler による Layer Supertype パターンの解説

Robert C. Martin による S.O.L.I.D. 原則の解説

データベースの関連事項

ここまで読んできたあなたは、「よくわかった。でも、エンティティはどこに保存すればいい?」と考えたに違いありません。もちろん、この重要な詳細を扱う必要はあります。しかし、モデルのデータ保持の方法や保存先は、DDD の概要とはあまり関連しないのです。

多くの開発者やデータベース管理者は、データベースがモデルであると主張するでしょう。ほとんどの場合、それは部分的には正解です。高度に正規化され、ダイアグラム作成ツールを使用して視覚化されたデータベースには、ドメイン内の情報とリレーションシップが大量に含まれることがあります。

ただし、一次的な手法としてのデータ モデルは、重要なものが一部抜け落ちています。同じドメイン内で固有の動作を理解するには、エンティティ関係図 (ERD) やエンティティ関係モデル、クラス ダイアグラムなどのデータのみの手法では失敗します。アプリケーションの各部を動作中に調べ、各タイプが処理を実行するためにどのように協調動作するのかを調べる必要があります。

私がモデル化を行う際には、よくホワイトボードをコミュニケーション ツールとして使用して、シーケンス図を作成します。この方法には、動作の設計や問題に関する本質的なコミュニケーションのほとんどが含まれており、統一モデリング言語 (UML) やモデル駆動型アーキテクチャの儀式も不要になります。特に、ホワイトボードに描いた図を消すつもりであるときなどは、不必要な儀式は避けることにしています。ダイアグラムが UML に 100% 準拠しているかどうかは気にせず、すばやくスケッチできる単純な四角形、矢印、レーンを使うことを好みます。

チームでシーケンス図をまだ使用していない場合は、この手法を習得することを強くお勧めします。SRP、階層型アーキテクチャ、データ モデル化における動作設計、および全般的なアーキテクチャの考慮事項に関する問題にチーム メンバが取り組む際に、この手法が絶大な効果を発揮することをこの目で見てきました。

DDD の使用を開始する

オブジェクト指向プログラミングに熟練することは、簡単なことではありません。技能は、プロフェッショナルな開発者のほとんどが手に入れられるものだと思いますが、技能を得るにはそれに専念し、勉強し、そして繰り返し実践することが必要になります。職人気質や、常に学ぶ姿勢を受け入れることも役立ちます。

どうすれば始められるのでしょうか。簡潔に言えば、まず勉強することです。S.O.L.I.D. 原則などについて学習し、Eric Evans の著作を読みましょう。投資した時間は、将来それ以上の価値をもたらしてくれるはずです。InfoQ では、DDD の主要概念の一部を紹介した簡易版の DDD 解説書を発行しています。財務上または一時的な予算である場合、または単なる調査である場合は、ここから始めることをお勧めします。しっかりした基礎を築いたら、Yahoo! DDD グループにアクセスして、設計者たちがどのような問題に取り組んでいるかを読み、書き込みに参加してください。

DDD は新しい原則でも、新しい手法でもありません。実績ある手法をまとめ合わせたものです。実践する準備が整ったら、そのような原理、手法、パターンのうち、自分の状況に最も適したものを適用してみてください。DDD に含まれる要素には、他よりも広く適用できる要素があります。ユビキタス言語を発見して使用し、モデル化するコンテキストを識別することによって、コア ドメインの存在理由を理解することは、まったく不明瞭で "フリーサイズ" のリポジトリを使用し続けるよりも、はるかに重要です。

ソリューションを設計する際には、価値を第一に考えて設計してください。設計者が芸術を生み出し、開発者が設計者の一種であるなら、開発者が扱う媒体はビジネス価値であるべきです。価値を意識することは、原則への準拠や保存テクノロジの選択などの検討事項よりも優先されます。そのような選択が重要に感じられることもありますが、価値を意識することも同様に重要なのです。

Dave Laribee は、VersionOne の製品開発チームを率いています。全米各地で開発者イベントに講師として招かれており、2007 年と 2008 年に Microsoft Architecture MVP を受賞しました。CodeBetter ブログ ネットワークで、ブログ (thebeelog.com) を執筆しています。