次の方法で共有



2015 年 7 月

Volume 30 Number 7

Cutting Edge - CQRS とメッセージベースのアプリケーション

Dino Esposito | 2015 年 7 月

Dino Esposito結局のところ、コマンド クエリ責務分離 (CQRS: Command and Query Responsibility Segregation) とは、状態を変更するコードと、状態を読み取るだけのコードを分離するソフトウェア設計です。複数の異なる層を基盤として論理的に分離しても、それぞれ個別の層が関与するように物理的に分離してもかまいません。CQRS の背後にはマニフェストも最先端の理念もありません。唯一の魅力は、設計の簡潔さです。ビジネスの複雑さが増している昨今、効率を高め、最適化し、成功をもたらす最も無難な方法は、設計を簡潔にすることです。

前回 (msdn.microsoft.com/magazine/mt147237) は、あらゆる種類のアプリケーションに当てはまるように、CQRS アプローチの全体像を示しました。コマンドとクエリにそれぞれ個別のスタックを備えた CQRS アーキテクチャを使用する場合は、それぞれのスタックを個別に最適化することになります。

モデルの制約はありません。そのため、特定の操作を危険にすることも、非実用的になることも、負荷が極めて高くなることもありません。システムの考え方はタスク指向が強くなります。もっと重要なことは、CQRS が自然なプロセスとして行われることです。集約など、ドメイン駆動型の設計概念さえ厄介なものではなくなり、設計の中で落ち着く場所が自然に見つかります。これこそが、設計を簡略化する威力です。

CQRS に関して、自身のビジネスに関係が深い導入事例やアプリケーションを探してみてください。おそらく、ほとんどの参考資料が、イベントとメッセージを使用してビジネス ロジックをモデル化および実装するアプリケーション シナリオを取り上げているのがわかります。CQRS は、非常に単純なアプリケーション (プレーンな CRUD アプリケーションなど) でも問題なく機能しますが、ビジネス ルールが極めて複雑な状況や、変化の度合いが非常に高い状況の方が真価を発揮します。

メッセージベースのアーキテクチャ

現実のシステムを見ていると、処理中に行われる操作と、その操作の結果として生じるイベントがあるのがわかります。操作とイベントにはデータが伴い、新しいデータが生成されることもあります。重要なのは、それが単なるデータにすぎないことです。このような操作の実行をサポートする場合、完璧な機能を備えたオブジェクト モデルは必ずしも求められません。オブジェクト モデルが役に立つのは間違いありませんが、ビジネス ロジックを編成する際の選択肢の 1 つでしかありません。これについては後ほど説明します。

メッセージベースのアーキテクチャは、複雑に入り組み、頻繁に変化するビジネス ワークフローの管理を極めて簡略化できるところにメリットがあります。この種のワークフローには、レガシ コード、外部サービス、動的に変化する規則などが関係してきます。ただし、メッセージベースのアーキテクチャの構築は、コマンドとクエリのスタックを明確に分離する CQRS のコンテキスト以外ではほぼ不可能です。したがって、分離したコマンド スタックには、以下のアーキテクチャを使用します。

メッセージは、コマンドにもイベントにもなり得ます。コードでは通常、基本メッセージ クラスを定義して、そこからコマンドとイベント用の基本クラスを追加で定義します (図 1 参照)。

図 1 基本メッセージ クラスの定義

public class Message
{
  public DateTime TimeStamp { get; proteted set; }
  public string SagaId { get; protected set; }
}
public class Command : Message
{
  public string Name { get; protected set; }
}
public class Event : Message
{
  // Any properties that may help retrieving
  // and persisting events.
}

コマンドとイベントは意味合いがやや異なりますが、果たす目的は関連しています。イベントの意味合いは Microsoft .NET Framework とほぼ同じです。つまり、何かの事象が発生したときに、データを伴ってそれを通知するクラスです。コマンドとは、バック エンドに対して実行される操作を表します。その操作はユーザーや他のなんらかのシステム コンポーネントが要求したものです。イベントとコマンドは、標準の命名規則に従います。コマンドは動詞を命令形 (SubmitOrderCommand など) にして表し、イベントは動詞を過去形 (OrderCreated など) にして表します。

通常、インターフェイス要素をクリックすることでコマンドが生成されます。システムがそのコマンドを受け取ると、タスクが生成されます。実行時間の長いステートフルなプロセス、単一の操作、ステートレスなワークフローなど、あらゆるものをタスクにすることができます。このようなタスクを一般的に "Saga (サガ)" と呼びます。

タスクは、プレゼンテーションからミドルウェアに向かって一方向に進み、最終的にはシステムやストレージ状態を変更します。コマンドは通常プレゼンテーションにデータを返すことはありません。ただし、操作の正常完了や失敗の理由などのフィードバックを迅速に示す場合など一部の例外はあります。

明示的なユーザー操作だけが、コマンドを呼び出す方法ではありません。システムと非同期にやり取りする自律型のサービスからコマンドを呼び出すこともできます。ここでは製品の出荷など、B2B のシナリオについて考えてみます。このようなシナリオでは、パートナーとのコミュニケーションが HTTP サービス経由で行われます。

メッセージベースのアーキテクチャにおけるイベント

コマンドがタスクを生成します。多くのタスクは、複数のステップで構成され、そのステップを組み合わせてワークフローを形成します。特定のステップを実行すると、さらに作業を進めるために、結果の通知が別のコンポーネントに渡されることがよくあります。このようにコマンドによってトリガーされるサブタスクのチェーンは、長く複雑になる可能性があります。メッセージベースのアーキテクチャは、(コマンドが呼び出した) 個別の操作とイベントに関連してワークフローをモデル化できるようになる点が優れています。コマンドとそれによって生じるイベント用にハンドラー コンポーネントを定義することで、どのような複雑なビジネス プロセスでもモデル化することが可能です。

さらに重要なのが、従来のフローチャートのように、実際の作業の流れに従うことができる点です。その結果、規則を実に簡単に理解できるようになり、各分野の専門家とのコミュニケーションも円滑になります。さらに、完成したワークフローは、それぞれが少量のステップを実行する無数の小さなハンドラーに分かれます。また、各ステップが非同期にコマンドを実行して、イベントの他のリスナーに通知します。

このアプローチの大きなメリットの 1 つは、アプリケーションのロジックを簡単に変更および拡張できる点です。新しいパーツを作成してシステムに追加するだけです。しかも、既存のコードと既存のワークフローに影響がないことを 100% 確信できます。この理由としくみについて理解するために、ここからはメッセージベースのアーキテクチャの実装の詳細を一部確認します。まずは、新しいインフラストラクチャ要素のバスについて紹介します。

バスの概要

最初に見るのは、手作りのバス コンポーネントです。バスの中核をなすインターフェイスは次のとおりです。

public interface IBus
{
  void Send<T>(T command) where T : Command;
  void RaiseEvent<T>(T theEvent) where T : Event;
  void RegisterSaga<T>() where T : Saga;
  void RegisterHandler<T>();
}

一般的に、バスはシングルトンです。バスは要求を受け取り、コマンドを実行しイベントを通知します。実際には、バスは具体的な作業をまったく行いません。そのコマンドまたはイベントを処理するために登録されているコンポーネントを選択するだけです。バスは、コマンドやイベントによって呼び出されたり、別のコマンドによって進められる、既知のビジネス プロセスのリストを保持しています。

コマンドやその関連イベントを処理するプロセスを、一般的に "Saga (サガ)" と呼びます。最初にバスを構成するときに、ハンドラーとサガの各コンポーネントを登録します。ハンドラーも簡単な種類のサガにすぎません。ハンドラーは、1 回限りの操作を表します。この操作が要求されると、操作が開始され、他のイベントにチェーンされることなく終了します。開始された操作が、別のコマンドをバスにプッシュすることで終了することもあります。メモリ内にサガとハンドラーの参照を保持するバス クラスの実装例を図 2 に示します。

図 2 バス クラスの実装例

public class InMemoryBus : IBus
{
  private static IDictionary<Type, Type> RegisteredSagas =
    new Dictionary<Type, Type>();
  private static IList<Type> RegisteredHandlers =
    new List<Type>();
  private static IDictionary<string, Saga> RunningSagas =
    new Dictionary<string, Saga>();
  void IBus.RegisterSaga<T>() 
  {
    var sagaType = typeof(T);
    var messageType = sagaType.GetInterfaces()
      .First(i => i.Name.StartsWith(typeof(IStartWith<>).Name))
      .GenericTypeArguments
      .First();
    RegisteredSagas.Add(messageType, sagaType);
  }
  void IBus.Send<T>(T message)
  {
    SendInternal(message);
  }
  void IBus.RegisterHandler<T>()
  {
    RegisteredHandlers.Add(typeof(T));
  }
  void IBus.RaiseEvent<T>(T theEvent) 
  {
    EventStore.Save(theEvent);
    SendInternal(theEvent);
  }
  void SendInternal<T>(T message) where T : Message
  {
    // Step 1: Launch sagas that start with given message
    // Step 2: Deliver message to all already running sagas that
    // match the ID (message contains a saga ID)
    // Step 3: Deliver message to registered handlers
  }
}

バスにコマンドを送信すると、3 手順のプロセスが実行されます。バスはまず、そのメッセージ受信時に開始するよう構成されているサガがあるかどうかを、登録済みサガのリストで確認します。そのようなサガが見つかったら、新しいサガ コンポーネントのインスタンスを作成し、メッセージを渡して、サガを実行中リストに追加します。最後に、同じメッセージに関係する登録済みハンドラーがあるかどうかを確認します。

バスに渡されるイベントはコマンドと同様に扱われ、登録済みリスナーにルーティングされます。ただし、ビジネス シナリオに関連する場合は、イベントがイベント ストアにログ記録されることがあります。イベント ストアとは、追加のみが可能なプレーンなデータ ストアで、システム内のすべてのイベントを追跡します。ログ記録されたイベントには、さまざまな用途があります。追跡目的にのみイベントをログに記録することも、単なるデータ ソース (イベント ソース) として使用することもできます。また、前回のエンティティの状態を保存するために従来のデータベースを使用しながら、データ エンティティの履歴の追跡にイベント ストアを使うこともできます。

サガ コンポーネントの作成

サガとは、「サガに関連付けられているビジネス プロセスを開始するコマンドまたはイベント」、「サガが処理できるコマンドのリスト」、「サガが対象とするイベントのリスト」を宣言するコンポーネントです。サガ クラスは、対象とするコマンドとイベントを宣言するために使用するインターフェイスを実装します。IStartWith や ICanHandle などのインターフェイスを、次のように定義します。

public interface IStartWith<T> where T : Message
{
  void Handle(T message);
}
public interface ICanHandle<T> where T : Message
{
  void Handle(T message);
}

以下は、サンプル サガ クラスのシグネチャの例を示しています。

public class CheckoutSaga : Saga<CheckoutSagaData>,
       IStartWith<StartCheckoutCommand>,
       ICanHandle<PaymentCompletedEvent>,
       ICanHandle<PaymentDeniedEvent>,
       ICanHandle<DeliveryRequestRefusedEvent>,
       ICanHandle<DeliveryRequestApprovedEvent>
{
  ...
}

上記のサガは、オンライン ストアの清算プロセスを表しています。ユーザーが清算ボタンをクリックするとこのサガが開始され、アプリケーション層がバスに Checkout コマンドをプッシュします。サガ コンストラクターは一意 ID を生成しますが、これは 1 つのビジネス プロセスの同時実行インスタンスを処理するために必要になります。同時実行される複数の清算サガを処理できるようにする必要があります。ID は、コマンド要求と共に送信される一意値で、GUID でもセッション ID でもかまいません。

サガでは、コマンドやイベントを処理するために、バス コンポーネントから呼び出される ICanHandle インターフェイスまたは IStartWith インターフェイスに Handle メソッドを用意します。Handle メソッドでは、サガは計算やデータ アクセスを実行します。その後、他のリスナー サガに別のコマンドをポストするか、単に通知としてイベントを発生させます。たとえば、図 3 のような清算ワークフローを想像してください。

清算ワークフロー
図 3 清算ワークフロー

このサガは、支払い受領までのすべてのステップを実行します。支払い受領時には、続けて PaymentSaga を実行するために AcceptPayment コマンドをバスにプッシュします。PaymentSaga を実行すると、PaymentCompleted イベントまたは PaymentDenied イベントが発生します。これらのイベントは再び CheckoutSaga によって処理されます。そのサガは、次に取引先の輸送会社の外部サブシステムと通信する別のサガに対して実行される別のコマンドを含む配送ステップに進みます。

コマンドとイベントが連結されるため、完了に達するまでサガは終了しません。その点ではサガを、開始点と終了点のある従来のワークフローと考えてもかまいません。また、サガは通常永続化もサポートします。永続化は、一般的にバスによって処理されます。ここで示したサンプル Bus クラスは永続化をサポートしません。NServiceBus などの商用バスも、Rebus などのオープン ソースのバスも SQL Server を使用します。永続化が行われるようにするには、各サガ インスタンスに一意 ID を提供する必要があります。

まとめ

最新のアプリケーションを本当に効果的にするためには、ビジネス要件に従って拡張可能にする必要があります。メッセージベースのアーキテクチャにより、ビジネス ワークフローを拡張および変更して、新しいシナリオをサポートするのが驚くほど簡単になります。拡張は、完全に分離して管理できます。新しいサガまたは新しいハンドラーを追加し、アプリケーションの起動時にそれをバスに登録して、対象のメッセージのみを処理する方法について通知するだけです。新しいコンポーネントは、必要なときにだけ自動で呼び出され、システムの残りの部分と並行して機能します。メッセージベースのアーキテクチャは、このように容易、シンプルかつ効果的です。


Dino Esposito は、『Microsoft .NET: Architecting Applications for the Enterprise』(Microsoft Press、2014 年)、および『Programming ASP.NET MVC 5』(Microsoft Press、2014 年) の共著者です。JetBrains の Microsoft .NET Framework および Android プラットフォームのテクニカル エバンジェリストでもあります。世界各国で開催される業界のイベントで頻繁に講演しており、software2cents.wordpress.com (英語) や Twitter (twitter.com/despos、英語) でソフトウェアに関するビジョンを紹介しています。

この記事のレビューに協力してくれた技術スタッフの Jon Arne Saeteras に心より感謝いたします。