次の方法で共有


データ ポイント

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

Julie Lerman

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

Julie Lerman私はプログラミング人生の中で、コードとデータを再利用できるようにすることを自分を駆り立てる目標としてきました。そのため、ドメイン駆動設計 (DDD) の学習を始めた当初、コンテキストを限定して強制的に分離し、コードはもちろんデータまでコピーすることがある DDD の手法は理解に苦しみました。DDD 業界の優秀な開発者数人の手を借りて、私の古いやり方が招く潜在的な問題が明らかになったときは、卒倒するような気分でした。最終的には、どこで複雑さという犠牲を払うかを選択することが必要だと Eric Evans が教えてくれました。DDD はソフトウェアの複雑さを軽減することを目的としているため、その結果として重複したモデルやデータを保持するという代償を払うことになります。

このコラムのシリーズでは、DDD の概念と、それらの概念がデータ駆動型のエクスペリエンスにどのように適合するかを説明してきました。初回のコラムは 2013 年 1 月に公開された「コンテキストが限定される DDD を使って EF モデルを縮小する」(bit.ly/1isIoGE、英語) で、その後 https://msdn.microsoft.com/ja-jp/magazine/dn342868.aspx を第 1 部として 3 部構成の「ドメイン駆動設計のコーディング: データを重視する開発者のためのヒント」シリーズを執筆しました。このシリーズの第 1 部で、「共有データは複雑なシステムで障害になることがある」と説明しました。ここでは、今回説明するアプローチが有益な理由についての細かい考え方を知るとができます。

コンテキストが限定されるそれぞれの状況に固有のデータベースを関連付けるような、極端な DDD パターンに従っている場合、このようにコンテキストが限定された状態でデータを正確に共有する方法について何度も質問を受けてきました。その方法については、Pluralsight.com (bitly.com/PS-DDD、英語) の「Domain-Driven Design Fundamentals」(ドメイン駆動型設計の基礎) コースで、Steve Smith と一緒に語っています。ただ、このコースが目指した難易度よりも少し高度になってしまうため、実装は行いませんでした。

限定されたコンテキスト間で共通するデータは、さまざまな方法で利用できます。今回は、2 つシステム間でデータをミラーリングするというシナリオにのみ注目します。1 つはデータの編集を目的として設計されたシステムで、もう 1 つは一部のデータに読み取り専用でアクセスするだけのシステムです。

最初に、基本パターンをレイアウトし、次に細部を付け加えます。実装には、制御の反転 (IoC) やメッセージ キューといった多くの要素が関係します。これらのツールを使い慣れていれば、実装は簡単に理解できます。ここでは IoC やキューの実装について詳しく説明しませんが、このコラムに付属するコード サンプルで確認し、デバッグできます。

サンプル シナリオ: 顧客一覧の共有

今回のパターンのデモには、極めてシンプルなシナリオを選びました。1 つのシステムは顧客サービス専用で、顧客データに加えて、多数のデータを保持します。このシステムはデータ ストレージ メカニズムを利用しますが、今回のサンプルには重要ではありません。もう 1 つは受注用に設計するシステムです。このシステムのユーザーは顧客にアクセスする必要がありますが、発注を行った顧客を特定することだけが目的です。したがって、このようにコンテキストが限定されたシステムに必要なのは、顧客名と ID の読み取り専用の一覧だけです。そのため、2 つ目のシステムに接続するデータベースには、1 つ目のシステムが保持している顧客に基づいて得られる、顧客の名前と ID の最新の一覧だけが必要です。この目的を達成するために選んだアプローチは、1 つ目のシステムに保持している顧客ごとに、この 2 種類のデータを 2 つ目のシステムにミラーリングする方法です。

データのミラーリング: 概要

非常に大まかに言えば、今回採用するソリューションでは、顧客がシステム A に挿入されるたびに、その顧客の ID と名前をシステム B のデータ ストアに追加します。システム A にある既存の顧客の名前が変更されたら、システム B でも正確な名前を反映する必要があります。そのため、名前が変更されるたびにとシステム B のデータ ストアが更新されるようにします。今回はデータを削除しませんが、アクティブではなくなった顧客をシステム B のデータ ストアから削除するように機能を拡張することができます。今回の実装ではどちらにも対応しません。

今回は応答する必要があるシステム A の次の 2 つのイベントのみを扱います。

  • 顧客が挿入された
  • 既存の顧客の名前が更新された

接続型のシステムでは、InsertCustomer や UpdateCustomerName など、システム A が呼び出すメソッドをシステム B が公開してもかまいません。または、CustomerCreated や CustomerNameUpdated といったイベントをシステム A で発生させて、システム B などの他のシステムでキャッチし、応答してもかまいません。

各イベントに応答する場合は、システム B のデータベースにその応答処理が必要になります。

今回の 2 つのシステムは非接続型とし、パブリッシュ/サブスクライブのパターンを使用します。システム A は、1 つ以上のイベントをなんらかの種類のオペレータに向けて発行します。その後、1 つ以上のシステムがそのオペレータでサブスクライブして、特定のイベントが発生するのを待ち、イベントに対応する固有の処理を実行します。

パブリッシュ/サブスクライブ パターンは、DDD の原則に従い、2 つのシステムが互いに認識せず、直接通信しないようにします。そこで、腐敗防止層という概念を使用します。各システムは、2 つのシステム間でメッセージをシャッフルするオペレータ経由で通信します。

このオペレータがメッセージ キューです。システム A はこのキューにメッセージを送信し、システム B はキューからメッセージを取得します。今回の例では、システム B という 1 つのサブスクライバーしか使用しませんが、多数のサブスクライバーを設定することもできます。

イベント メッセージの内容

発行するイベントが CustomerCreated イベントの場合、システム A は「顧客が挿入されました。こちらが挿入された顧客の ID と名前です」という内容のメッセージを送信します。これがメッセージの全容ですが、丁寧な英語の文章ではなく、データによってこの内容を伝えます。メッセージ キューへのイベントの発行で興味深いのは、メッセージを取得するシステムやメッセージへの応答として実行される処理について、パブリッシャー側では意識しない点です。

システム B は、データベースに顧客を挿入するか、データベース内の顧客を更新することで、このメッセージに応答します。現実には、システム B がこのタスクを実行するのではなく、サービスが処理するようにします。また、更新方法はデータベースが判断するようにします。今回の例では、システム B のデータベースは、元の顧客レコードを削除して新しい顧客レコードを挿入するロジックを実行するストアド プロシージャを使用して、顧客を "更新" します。ID キーには GUID を使用するため、顧客の ID は適切に保持されます。今回の DDD ソフトウェアではデータベースが生成するキーについては考慮しません。事前に作成済みの GUID とデータベースが増分方式で生成するキーのどちらを使用するかは難しい問題です。勤務先のデータベース業務に適したロジックを定義する必要があります。

最終的には、システム B (受注システム) に、システムで使用できる完全な顧客一覧が格納されるようにします。ワークフローをさらに進め、この受注システムで特定の顧客の詳細情報 (クレジット カード情報や現在の配送先住所など) が必要になった場合は、サービスの呼び出しなど他のメカニズムを利用して、必要に応じたデータを取得します。ただし、このワークフローについては今回は対処しません。

メッセージ キューとの通信

非同期にメッセージを通信できるシステムをイベント バスと呼びます。イベント バスには、メッセージを格納し、メッセージを取得する必要のあるシステムにメッセージを提供するインフラストラクチャが組み込まれています。また、このインフラストラクチャを操作する API も用意されます。ここでは、このような形式の通信に着手しやすくなる実装に重点を置いて説明します。それがメッセージ キューです。メッセージ キューには、多くの選択肢があります。Pluralsight.com (英語) の DDD の基礎に関するコースでは、Smith と私は SQL Server Service Broker をメッセージ キューとして選択し、使用しました。2 人とも SQL Server に携わっていたためセットアップも簡単で、必要な作業はメッセージをキューにプッシュして取得する SQL を記述することだけでした。

今回のコラム執筆は、SQL Server Service Broker よりも広く使用されているオープン ソース メッセージ キューの 1 つ、RabbitMQ の使用方法を習得するチャンスだと思いました。そこで、RabbitMQ サーバー (もちろん Erlang も) をコンピューターにインストールしたほか、RabbitMQ .NET クライアントもインストールして自分のアプリケーションで RabbitMQ .NET クライアントに対するコードを簡単に記述できるようにしました。RabbitMQ の詳細については、rabbitmq.com (英語) を参照してください。また、Pluralsight.com (英語) で提供されている「RabbitMQ for .NET Developers」(.NET 開発者向け RabbitMQ) コースの内容も非常に参考になります。

こうして、システム A には、RabbitMQ サーバーにメッセージを送信するメカニズムを組み込みました。しかし、システム B (受注システム) は、そのどの処理にも関与しません。システム B は、顧客一覧がデータベース内にあることを想定するだけで、その一覧がデータベースに格納された方法は意識しません。RabbitMQ メッセージ キューのメッセージを確認し、その結果に応じて受注システムのデータベースを更新する処理は、別の小さな Windows サービスで行います。図 1 に、プロセス全体のワークフロー図を示します。

メッセージ キューにより、互いに認識していないシステム同士でメッセージの共有 (この例ではシステム B データベースの更新) が可能
図 1 メッセージ キューにより、互いに認識していないシステム同士でメッセージの共有 (この例ではシステム B データベースの更新) が可能

キューへのメッセージの送信

まず、システム A の Customer クラスについて説明します (図 2 参照)。例を簡潔にするため、Customer クラスには、ID、名前、顧客のソース、記録日といった数個のプロパティしか含めていません。DDD パターンに従って、オブジェクトには無作為な編集を防ぐ制約を組み込んでいます。新しい顧客は Create ファクトリ メソッドを使用して作成し、名前を解決する必要がある場合は FixName メソッドを使用します。

図 2 顧客管理に限定されるコンテキストで使用する Customer クラス

public static Customer Create(string name, string source) {
   return new Customer(name, source);
  }
  private Customer(string name, string source){
    Id = Guid.NewGuid();
    Name = name;
    InitialDate = DateTime.UtcNow;
    ModifiedDate = DateTime.UtcNow;
    Source = source;
    PublishEvent (true);
  }
  public Guid Id { get; private set; }
  public string Name { get; private set; }
  public DateTime InitialDate { get; private set; }
  public DateTime ModifiedDate { get; private set; }
  public String Source { get; private set; }
  public void FixName(string newName){
    Name = newName;
    ModifiedDate = DateTime.UtcNow;
    PublishEvent (false);
  }
  private void PublishEvent(bool isNew){
    var dto = CustomerDto.Create(Id, Name);
    DomainEvents.Raise(new CustomerUpdatedEvent(dto, isNew));
 }}

コンストラクターと FixName メソッドの両方で PublishEvent メソッドを呼び出していることに注目してください。呼び出されたメソッドは、単純な CustomerDto を作成してから、DomainEvents クラスを使用して新しい CustomerUpdatedEvent を発生させます (図 3 参照)。DomainEvents クラスは、Udi Dahan が 2009 年に執筆した MSDN マガジンの記事「ドメイン モデル パターンを使用する」(msdn.microsoft.com/magazine/ee236415) で取り上げています。今回の例では、単純な操作の応答としてイベントを発行しています。実際の実装では、データが正常にシステム A データベースに格納されてからこれらのイベントを発行することをお勧めします。

図 3 顧客が更新されるときにイベントをカプセル化するクラス

public class CustomerUpdatedEvent : IApplicationEvent{
  public CustomerUpdatedEvent(CustomerDto customer, 
   bool isNew) : this(){
    Customer = customer;
    IsNew = isNew;
  }
  public CustomerUpdatedEvent()
  {
    DateTimeEventOccurred = DateTime.Now;
  }
  public CustomerDto Customer { get; private set; }
  public bool IsNew { get; private set; }
  public DateTime DateTimeEventOccurred { get; set; }
  public string EventType{
    get { return "CustomerUpdatedEvent"; }
 }}

このイベントについて把握する必要のある情報は、CustomerUpdatedEvent の最終結果、つまり、顧客が新しい顧客であることを示すフラグを付けた CustomerDto です。また、汎用のイベント ハンドラーが必要とするメタデータも含めます。

これで、アプリケーションで定義した 1 つ以上のハンドラーで CustomerUpdatedEvent を処理できるようになります。ハンドラーには、CustomerUpdatedService というサービスを 1 つだけ定義しています。

public class CustomerUpdatedService : IHandle<CustomerUpdatedEvent>
{
  private readonly IMessagePublisher _messagePublisher;
  public CustomerUpdatedService(IMessagePublisher messagePublisher){
    _messagePublisher = messagePublisher;
  }
  public void Handle(CustomerUpdatedEvent customerUpdatedEvent){
    _messagePublisher.Publish(customerUpdatedEvent);
}}

このサービスでは、コードで発生する CustomerUpdatedEvent のすべてのインスタンスを処理します。このインスタンスは、指定したメッセージ パブリッシャーを使用してイベントを発行することで発生します。ここではパブリッシャーは指定せず、抽象インターフェイスの IMessagePublisher を参照しているだけです。ロジックを疎結合できる IoC パターンを採用しています。私は気まぐれなので、今日はメッセージ パブリッシャーを使用しても、明日は別のものを使いたくなるかもしれません。バックグラウンドでは、数多くの .NET 開発者が .NET アプリケーションで IoC を管理するために使用しているツール、StructureMap (structuremap.net) を使用しました。StructureMap を使用すると、DomainEvents.Raise によって発生したイベントを処理するクラスの格納場所を示すことができます。StructureMap の作成者 Jeremy Miller は、MSDN マガジンで、このサンプルに適用しているパターンと関連のある「実践におけるパターン」というすばらしいシリーズを執筆しています (https://msdn.microsoft.com/ja-jp/magazine/ee532098(en-us).aspx?sdmr=JeremyMiller&sdmi=authors、英語)。ここでは StructureMap を使用して、IMessagePublisher を認識したときに RabbitMQMessagePublisher 具象クラス (以下にロジックを示します) を使用することがわかるようにアプリケーションを構成しました。

public class RabbitMqMessagePublisher : IMessagePublisher{
  public void Publish(Shared.Interfaces.IApplicationEvent applicationEvent) {
    var factory = new ConnectionFactory();
    IConnection conn = factory.CreateConnection();
    using (IModel channel = conn.CreateModel()) {
      [code to define the RabbitMQ channel]
      string json = JsonConvert.SerializeObject(applicationEvent, Formatting.None);
      byte[] messageBodyBytes = System.Text.Encoding.UTF8.GetBytes(json);
      channel.BasicPublish("CustomerUpdate", "", props, messageBodyBytes);
 }}}

このコードでは、RabbitMQ の構成固有のコード行を多数削除しています。コード全体はダウンロードで確認できます (msdn.microsoft.com/magazine/msdnmag1014、英語)。

このメソッドの本質は、イベント オブジェクトの JSON 表現をキューに発行することです。Julie Lerman という名前の顧客を追加するときにこの文字列がどうなるかを見てみましょう。

{
"Customer":
  {"CustomerId":"a9c8b56f-6112-42da-9411-511b1a05d814",
    "ClientName":"Julie Lerman"},
"IsNew":true,
"DateTimeEventOccurred":"2014-07-22T13:46:09.6661355-04:00",
"EventType":"CustomerUpdatedEvent"
}

このメッセージを発行すると、顧客管理システムに関連する処理は完了です。

サンプル アプリケーションでは、一連のテストを使用してメッセージがキューに発行されるようにしています (図 4 参照)。完了時にキューを確認するようにテストを作成することはせず、自分のコンピューター上で RabbitMQ Manager を閲覧し、RabbitMQ Manager のツールを使用するようにしました。テスト コンストラクターでは、IoC というクラスを初期化しています。StructureMap の構成や、IMessagePublisher とイベント ハンドラーの関連付けを行うのはこのクラスです。

図 4 テストにおける RabbitMq への発行

[TestClass]
public class PublishToRabbitMqTests
{
  public PublishToRabbitMqTests()
  {IoC.Initialize();
  }
  [TestMethod]
  public void CanInsertNewCustomer()
  {
    var customer = Customer.Create("Julie Lerman", 
      "Friend Referral");
    Assert.Inconclusive("Check RabbitMQ Manager for a message re this event");
  }
  [TestMethod]
  public void CanUpdateCustomer() {
    var customer = Customer.Create("Julie Lerman", 
      "Friend Referral");
    customer.FixName("Sampson");
    Assert.Inconclusive("Check RabbitMQ Manager for 2 messages re these events");
}}

メッセージの取得と受注システムのデータベース更新

メッセージは、RabbitMQ サーバーに常駐し、取得を待ちます。この作業は、常時実行され、定期的にキューをポーリングして新しいメッセージの有無を確認する Windows サービスで処理します。メッセージがある場合は、このサービスがメッセージを取得して処理します。メッセージは、他のサブスクライバーが処理してもかまいません。今回のサンプル用には、サービスではなく 1 つのシンプルなコンソール アプリケーションを作成しました。これにより、Visual Studio で "サービス" を簡単に実行およびデバッグしながら学ぶことができます。次回同じ作業をするときは、1 つの Windows サービスを操作したり、簡易的に作成したコンソール アプリケーションを使用したりせず、Microsoft Azure WebJobs (https://azure.microsoft.com/ja-jp/documentation/articles/websites-dotnet-webjobs-sdk-get-started/、英語) の使用を検討するかもしれません。

サービスでは、Dahan の DomainEvents クラスを使用してイベントを発生させ、ハンドラー クラスでイベントに応答し、StructureMap を使用してイベント ハンドラーの場所を特定する IoC クラスを初期化する、という同じようなパターンを使用します。

またサービスは、RabbitMQ .NET クライアントの Subscription クラスを使用して、RabbitMQ でメッセージをリッスンします。この処理のロジックは下記の Poll メソッドにあります。このメソッドは、_subscription オブジェクトによって常にメッセージをリッスンし、メッセージを取得するたびにキューに格納した JSON のシリアル化を解除して CustomerUpdatedEvent に戻し、イベントを発生させます。

private void Poll() {
  while (Enabled) {
    var deliveryArgs = _subscription.Next();
    var message = Encoding.Default.GetString(deliveryArgs.Body);
    var customerUpdatedEvent =
      JsonConvert.DeserializeObject<CustomerUpdatedEvent>(message);
    DomainEvents.Raise(customerUpdatedEvent);
}}

サービスには、Customer という 1 つのクラスを含めます。

public class Customer{
  public Guid CustomerId { get; set; }
  public string ClientName { get; set; }
}

CustomerUpdatedEvent のシリアル化が解除されると、CustomerUpdatedEvent の Customer プロパティ (元は顧客管理システムの CustomerDto が設定されています) のシリアル化が解除され、このサービスの Customer オブジェクトになります。

発生するイベントの処理は、このサービスで最も興味深い部分です。イベントを処理する CustomerUpdatedHandler クラスを以下に示します。

public class CustomerUpdatedHandler : IHandle<CustomerUpdatedEvent>{
  public void Handle(CustomerUpdatedEvent customerUpdatedEvent){
    var customer = customerUpdatedEvent.Customer;
    using (var repo = new SimpleRepo()){
      if (customerUpdatedEvent.IsNew){
        repo.InsertCustomer(customer);
      }
      else{
        repo.UpdateCustomer(customer);
 }}}}

このサービスは、Entity Framework (EF) を使用してデータベースを操作します。図 5 では、単純なリポジトリで、関連操作が InsertCustomer と UpdateCustomer という 2 つのメソッドにカプセル化されていることがわかります。イベントの IsNew プロパティが true の場合、サービスはリポジトリの InsertCustomer メソッドを呼び出します。そうでない場合は UpdateCustomer メソッドを呼び出します。

図 5 InsertCustomer メソッドと UpdateCustomer メソッド

public void InsertCustomer(Customer customer){
  using (var context = new CustomersContext()){
    context.Customers.Add(customer);
    context.SaveChanges();
}}
public void UpdateCustomer(Customer customer){
  using (var context = new CustomersContext()){
    var pId = new SqlParameter("@Id", customer.CustomerId);
    var pName = new SqlParameter("@Name", customer.ClientName);
    context.Database.ExecuteSqlCommand      
      ("exec ReplaceCustomer {0}, {1}", 
        customer.CustomerId, customer.ClientName);
}}

これらのメソッドは、EF DbContext を使用して関連するロジックを実行します。挿入操作の場合、DbContext は顧客を追加して SaveChanges を呼び出します。データベースの挿入コマンドは EF によって実行されます。更新操作の場合、DbContext は CustomerID と CustomerName をストアド プロシージャに送信します。このストアド プロシージャは、私 (または信頼の置ける DBA) が更新を実行するように定義した任意のロジックを使用します。

したがってサービスでは、顧客管理システムに保持された状態と同じ最新の顧客名簿が常に受注システムの顧客一覧で使用されるように必要な処理をデータベースで実行します。

多くの階層とパズルのピースが必要

このワークフローのデモには単純なサンプルを使用したため、ソリューションの割には処理が極めて過剰なように思えるかもしれません。ですが、このコラムのポイントは、DDD プラクティスの使用時にソリューションを調整して複雑なソフトウェアの問題を解決する方法を示すことです。顧客管理の分野に的を絞るのであれば、他のシステムを意識する必要はありません。IoC、ハンドラー、およびメッセージ キューによる抽象化を使用することで、領域があいまいになることなく外部システムのニーズを満たすことができます。Customer クラスはイベントを発生させるだけです。このデモでは、最も簡単な方法でワークフローをわかりやすくするための場所として使用しましたが、使用する分野によってはあいまいすぎるかもしれません。開発するアプリケーションでは、変更を固有のデータ ストアにプッシュする際にリポジトリから発生させるなど、別の場所からイベントを発生させても問題はありません。

このコラムでダウンロードできるサンプル ソリューションでは RabbitMQ を使用しているため、RabbitMQ の簡易版オープン ソース サーバーをコンピューターにインストールする必要があります。サンプル ソリューションの ReadMe ファイルに、参照情報を含めてあります。また、私のブログ thedatafarm.com (英語) で、RabbitMQ マネージャーとデータベースを確認して結果を見ながら、順にコードを記述する過程を撮影した短いビデオを公開しました。


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 のコースをご覧ください。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Cesar de la Torre に心より感謝いたします。