データ ポイント

コンテキストが限定されるドメイン駆動設計でのデータ共有パターン (第 2 部)

Julie Lerman

コード サンプルのダウンロード

Julie Lerman2014 年 10 月号のコラム (msdn.microsoft.com/magazine/dn802601) では、コンテキストが限定される (BC) ドメイン駆動型設計 (DDD) を使用して、複数の BC それぞれに固有のデータベースを分離した状態で、各データベース間でデータをミラーリングするパターンについて説明しました。このシナリオの顧客管理 BC 側では、ユーザーが顧客の詳細情報の挿入、更新、削除を行いながら顧客データを管理でき、もう 1 つの受注システム BC 側では、顧客の ID キーと名前という 2 つの重要な顧客情報にアクセスする必要がありました。これらのシステムは 2 つの独立した BC に属するため、2 つのシステム間でデータを共有してやり取りすることはできません。

DDD は、複雑な問題を解決することが目的です。ドメインでの問題を単純化すれば、多くの場合、ドメインから複雑さを取り除くことができます。その点では、顧客管理 BC は管理するデータがその後共有されるかどうかを意識する必要はありません。前回のコラムでは、パブリッシュ/サブスクライブ パターンを採用し、ドメインのイベント、メッセージ キュー (RabbitMQ)、サービスを利用して問題を解決しました。これには多数の動的要素が関与しました。

多くの概念を盛り込むことにより説明が面倒にならないように、手っ取り早い方法としてイベントをメッセージ キューにパブリッシュし、顧客クラスから直接一連のイベントをトリガーすることにしました。

今月は、このソリューションを 2 つの方法で拡張します。まず、ワークフローの中のもっと妥当な場所でメッセージをパブリッシュします。つまり、(新しいまたは変更した) 顧客が、顧客システムのデータベースに正常に保存されたことを最初に確認した後パブリッシュします。また、関連するイベントへの応答としてのみイベントがパブリッシュされるようにします。新しい顧客の作成後にイベントをパブリッシュするのも妥当です。しかし、今回はメッセージ キューに更新をパブリッシュするタイミングをより厳しく見極める必要のある仮想的状況にも対処します。今回の場合で言えば、顧客の名前が変更されたときにのみ、関連メッセージが確実にパブリッシュされるようにします。顧客名に影響しないデータが変更された場合は、メッセージをパブリッシュしません。

この 2 点を変更することで、現実のシナリオにより即したソリューションになります。

作成または更新後の顧客の保存

現在のソリューションでは、顧客を作成した場合、または顧客の名前を変更した場合に通知を行います。コンストラクターと次の FixName メソッドは、どちらも PublishEvent を呼び出します。

public void FixName(string newName){
    Name = newName;
    ModifiedDate = DateTime.UtcNow;
    PublishEvent(false);
  }

PublishEvent がトリガーするワークフローでは、メッセージをキューにパブリッシュします。

private void PublishEvent(bool isNew){
    var dto = CustomerDto.Create(Id, Name);
    DomainEvents.Raise(new CustomerUpdatedEvent(dto, isNew));
  }

このソリューションの詳細については、10 月号のコラムを参照してください。イベントは、クラスから発生させるのではなく、新しい顧客インスタンス、または顧客の名前への修正が正常にデータベースに保存されたことを認識した後に発生させます。

そこで、PublishEvent メソッドと、このメソッドの呼び出しを Customer クラスから削除します。

データ層には、顧客集計用のデータ アクセス ロジックを含むクラスがあります。このクラスに PublishEvent メソッドを移動し、名前を PublishCustomerPersistedEvent に変更します。顧客をデータベースに保存するのに使用しているメソッドでは、SaveChanges の完了後に新しいイベントを呼び出します (図 1 参照)。

図 1 データを保存した後に保存クラスでイベントを発生

public class CustomerAggregateRepository {
public bool PersistNewCustomer(Customer customer) {
  using (var context = new CustomerAggregateContext()) {
    context.Customers.Add(customer);
    int response = context.SaveChanges();
    if (response > 0) {
      PublishCustomerPersistedEvent(customer, true);
      return true;
    }
    return false;
  }
}
public bool PersistChangeToCustomer(Customer customer) {
  using (var context = new CustomerAggregateContext()) {
    context.Customers.Attach(customer);
    context.Entry(customer).State = EntityState.Modified;
    int response = context.SaveChanges();
    if (response > 0) {
      PublishCustomerPersistedEvent(customer, false);
      return true;
    }
    return false;
  }
}
   private void PublishCustomerPersistedEvent(Customer customer, 
     bool isNew) {
     CustomerDto dto = CustomerDto.Create(customer.Id, customer.Name);
     DomainEvents.Raise(new CustomerUpdatedEvent(dto, isNew));
   }
 }

このように移動する場合、メッセージをデータ層プロジェクトにパブリッシュするインフラストラクチャも移動する必要があります。図 2 は、このイベントをデータ層に移動した後のプロジェクトと、前回のコラムで作成した関連プロジェクト (CustomerManagement.Core と CustomerManagement.Infastructure) を並べています。これで、CustomerUpdatedEvent、DTO、およびサービスがインフラストラクチャ プロジェクトに含まれるようになります。インフラストラクチャ ロジックをドメインの外に移動したので満足しました。中核となるドメインにインフラストラクチャ ロジックを必要とするコードの感触に違和感を感じていました。

イベントのパブリッシュをデータ層に移動する前後のプロジェクト構造
図 2 イベントのパブリッシュをデータ層に移動する前後のプロジェクト構造

2 つのテストを使用して、挿入と更新が正常に完了したときに、実際適切なメッセージがキューにパブリッシュされることを検証します。3 つ目のテストとして、更新が失敗した場合はメッセージがキューにパブリッシュされないことを検証します。これらのテストは、コラム付属のダウンロード ソリューションで確認できます。

ドメイン モデルではなく、データ層で

非常に単純な変更ですが、この移動によって、(技術的な理由ではなく) イベントを BC の外に出してデータ層に含めるのが適切だということを納得するのに苦労しました。犬を散歩させながら自問自答しました (独り言を言いながら森の中や通りを歩くのは、私にとっては異常なことではありません。さいわい閑静な場所に住んでいるため、他人から正気を疑われることもありません)。このときは、最終的に「イベントのパブリッシュは BC に属さない」ということで納得しました。つまり、BC はその BC 自体のみしか面倒を見ないためです。BC は、他の BC やサービスあるいはアプリケーションが行うことには関知しません。そのため、「保存したデータを受注システムと共有する必要がある」ことは、BC にとっては意味がありません。これは、ドメインのイベントではなく保存に関連するイベントなので、アプリケーション イベントの 1 つと見なします。

移動によって新たに生じる問題の解決

パブリッシュ イベントを保存層に移動すると、ある問題が生じます。PersistChangeToCustomer メソッドは、他の編集内容も Customer エンティティに保存します。たとえば、Customer エンティティには、顧客の配送先住所や請求先住所を追加または更新する機能もあります。住所は値オブジェクトなので、作成したり新しい値のセットに置き換えたりすると、その変更が顧客に反映されます。

いずれかの住所を変更したら、PersistChangeToCustomer が呼び出されます。しかし、この場合は顧客の名前が変更されたことを通知するメッセージをキューに送信しても意味がありません。

では、この保存層に顧客の名前が変更されていないことを知らせるには、どうすればよいでしょう。直感的な解決方法は、NameChanged などのフラグ プロパティを追加することです。しかし、必要になるたびにブール値を追加して、詳細状態を追跡しなければならないのは好ましくありません。そこで顧客クラスからイベントを発生させることを考えました。ただし、新たなメッセージをキューに送信することのないイベントです。「メッセージを送信しないでください」とだけ伝えるようなメッセージを送りたくはありません。ですが、このイベントをキャプチャするにはどうすればよいでしょう。

ここでも、Jimmy Bogard がすばらしいソリューションによって救いの手を差し伸べてくれています。2014 年 5 月のブログ記事「A Better Domain Events Pattern」(優れたドメイン イベントのパターン、bit.ly/1vUG3sV、英語) で、彼はすぐにイベントを発生させるのではなく、イベントを集めておき、必要に応じて集めたイベントを保存層で取り出して処理することを提案しています。このパターンのポイントは、DomainEvents 静的クラスを削除することです。このクラスは、イベントが発生するタイミングを制御できないため、副作用を引き起こす可能性があります。これは、ドメインのイベントに関する新しい考え方です。今回のリファクタリングでは偶然にもこの問題は起きていませんが、それでも DomainEvents 静的クラスを使用していることは確かです。いつもどおり、学習を続けて自分のやり方を進化させていこうと思います。

Bogard のアプローチは非常にすばらしいものですが、今回はアイデアを借りて、彼の実装とは若干異なる方法を使用します。メッセージをキューに送信する必要はありません。必要なのは、イベントを読み取ることだけです。さまざまな状態フラグを無分別に作成することなく、イベントを顧客オブジェクトでキャプチャするのは優れた方法です。たとえば、必要になるたびに「名前が変更された」というブール値を含めて、true か false に設定しなければならないという不自然さを回避できます。

Bogard は、IEntity インターフェイスで、ICollection<IDomainEvent> 型の Events プロパティを使用しています。ドメインに複数のエンティティがある場合は、これと同じようにするか、Events プロパティを Entity 基本クラスに追加していたでしょう。しかし今回のデモでは、新しいプロパティを直接 Customer オブジェクトに置きます。プライベート フィールドを作成して Events を読み取り専用として公開しているので、集めたイベントは Customer でのみ変更できます。

private readonly ICollection<IDomainEvent> _events;
public ICollection<IDomainEvent> Events {
  get { return _events.ToList().AsReadOnly(); }
}

次に、これに関連するイベントで、第 1 部で使用した IDomainEvent インターフェイスを実装する CustomerNameFixedEvent を定義します。CustomerNameFixedEvent に必要なことはあまり多くありません。このイベントでは、インターフェイスの一部である DateTimeEventOccurred プロパティを設定します。

public class CustomerNameFixedEvent : IDomainEvent{
  public CustomerNameFixedEvent(){
    DateTimeEventOccurred = DateTime.Now;
  }
  public DateTime DateTimeEventOccurred { get; private set; }   }
}

これで、Customer.FixName メソッドを呼び出せば、このイベントのインスタンスを Events コレクションに追加できます。

public void FixName(string newName){
  Name = newName;
  ModifiedDate = DateTime.UtcNow;
  _events.Add(new CustomerNameFixedEvent());
}

このコードを使用すると、状態プロパティよりもはるかにコードの独立性は強くなります。さらに、今後ドメインを改良するときに、Customer クラスのスキーマを変更せずにロジックを追加できます。保存メソッドではこの点を利用できます。

PersistChangeToCustomer メソッドには、新しいロジックを追加しています。このロジックは、受け取った customer でこのイベントの有無を確認し、イベントが存在する場合はメッセージをキューに送信します。図 3 に、パブリッシュする前にイベントの種類を確認する新しいロジックを含めたメソッドの全体像を再度示します。

図 3 イベントの種類を確認する PersistChangeToCustomer メソッド

public bool PersistChangeToCustomer(Customer customer) {
    using (var context = new CustomerAggregateContext()) {
      context.Customers.Attach(customer);
      context.Entry(customer).State = EntityState.Modified;
      int response = context.SaveChanges();
      if (response > 0) {
        if (customer.Events.OfType<CustomerNameFixedEvent>().Any()) {
          PublishCustomerPersistedEvent(customer, false);
        }
        return true;
      }
      return false;
    }
  }

値が 0 より大きい SaveChanges への応答として、customer が正常に保存されたかどうかを示すブール値をメソッドから返す点は変わりません。その場合は、Customer.Events で CustomerNameFixedEvents の有無を確認し、保存されている customer について前と同じようにメッセージをパブリッシュします。なんらかの理由で SaveChanges が失敗した場合、おそらく例外によって失敗したことを示しますが、応答の値を利用してメソッドから False を返すことも考えています。呼び出し側のロジックが、失敗に対して行う処理を決定します (おそらくは保存を再実行するか、エンド ユーザーまたは他のシステムに通知を送信します)。

Entity Framework (EF) を使用しているので、一時的な接続エラーが発生したら SaveChanges を再実行するように EF6 を構成することもできます。ただし、SaveChanges の再実行は SaveChanges からの応答を受け取るまでに完了します。DbExecutionStrategy の詳細については、2013 年 12 月号の記事「Entity Framework 6: 上級者向けエディション」(msdn.microsoft.com/ja-jp/magazine/dn532202.aspx) を参照してください。

新しいロジックを検証するために新しいテストを追加しました。このテストでは、新しい顧客を作成して保存した後、顧客の BillingAddress プロパティを編集し、その変更を保存します。新しい顧客が作成されたことを示すメッセージはキューに送信されますが、住所変更の更新に応答するメッセージは送信されません。

[TestMethod]
public void WillNotSendMessageToQueueOnSuccessfulCustomerAddressUpdate() {
  Customer customer = Customer.Create("George Jetson", "Friend Referral");
  var repo = new CustomerAggregateRepository();
  repo.PersistNewCustomer(customer);
  customer.CreateNewBillingAddress
    ("123 SkyPad Apartments", "", "Orbit City", "Orbit", "n/a", "");
  repo.PersistChangeToCustomer(customer);
  Assert.Inconclusive(@"Check status of RabbitMQ Manager for a create message,
    but no update message");
}

今回のコラムのレビューをしてくれた Stephen Bohlen は、メッセージがキューに到達したことを確認するもう 1 つの手段として、"テスト スパイ パターン" xunitpatterns.com/TestSpy.html (英語) を提案しています。

2 つのパーツに 1 つのソリューション

限定されたコンテキストでデータを共有する方法は、DDD を学習する開発者の多くが疑問に思うことです。Steve Smith と私が開いた Pluralsight の「Domain-Driven Design Fundamentals」(ドメイン駆動型設計の基礎) コース (bit.ly/PS-DDD、英語) では、この方法について軽く触れましたが、実装は行いませんでした。このようなデータ共有を成功させる方法については、何度も詳細な説明を求められてきました。今回の短期シリーズの第 1 部では、多くのツールを活用して、ある BC のデータベースに格納されているデータを、別の BC で使用しているデータベースと共有するワークフローを構築しました。メッセージ キュー、イベント、および制御の反転コンテナーを使用してパブリッシュ/サブスクライブ パターンを調整すると、非常に独立性の高い方法でこのパターンを実現できます。

今月は、「DDD に注目したとき、メッセージのパブリッシュが有効なタイミングはいつか」という質問への回答としてサンプルを拡張しました。もともとは、新しい顧客が作成されるたびに、または顧客の名前が更新されるたびに Customer クラスでイベントをパブリッシュしてデータ共有ワークフローをトリガーしていました。メカニズムはすべて揃っていたので、今回は簡単にロジックをリポジトリに移動でき、それによって顧客データが正常にデータベースに保存されたことを確認するまでメッセージのパブリッシュを遅らせることもできました。

さらに微調整を加える余地は各所にありますし、ツールを使い慣れた他のツールに変えることもできます。ですが、読者の皆さんには、限定されたコンテキスト固有の独立したデータベースを使用するためのデータ駆動設計パターンをスムーズに進める方法をご理解いただけたと思います。


Julie Lerman は、バーモント ヒルズ在住の Microsoft MVP、.NET の指導者、およびコンサルタントです。世界中のユーザー グループやカンファレンスで、データ アクセスなどの .NET トピックについてプレゼンテーションを行っています。彼女のブログは thedatafarm.com/blog (英語) で、彼女は O'Reilly Media から出版されている『Programming Entity Framework』(2010 年) および『Code First』版 (2011 年)、『DbContext』版 (2012 年) を執筆しています。彼女の Twitter (twitter.com/julielerman、英語) をフォローして、juliel.me/PS-Videos (英語) で彼女の Pluralsight コースをご覧ください。

この記事のレビューに協力してくれた技術スタッフの Stephen Bohlen に心より感謝いたします。
Stephen は、現在マイクロソフトの Technical Evangelism and Development (TED) チームでプリンシパル ソフトウェア エンジニアを務めています。彼は、ソフトウェアとテクノロジに関する 20 年以上にわたる多彩な経験を生かし、最先端やプレリリース版の開発者向けマイクロソフト製品およびマイクロソフト テクノロジの導入において、マイクロソフト パートナー組織の選択をサポートしています。