CQRSのコマンドはやっぱりコマンドバスを介したほうが良いと思った話


はじめに

CQRSの実現方法について記載されている.NETのエンタープライズアプリケーションアーキテクチャ: .NETを例にしたアプリケーション設計原則にてコマンドバスを用いてコマンドを管理したほうが良い、という旨が記載されているのですがあんまり必要性を感じられず以下のような実装を行っていました。
(EventStoreを用いたCQRS+イベントソーシングの実践と考察で考えた業務知識を例にして)

必要ないと感じる理由としては書き込み先(EventStoreとかRDBMSとか)に対して送信する際、イベントベースではなく状態ベースで送信しているからかなと。

  • 状態ベース
    集約に対して複数の変更があってもひとつのコマンド内で全部処理し、最後に書き込み先に一回だけ送信する。(永続指向のリポジトリ)
  • イベントベース
    集約に対して複数の変更があった場合、複数のコマンドを発行し、逐次書き込み先に送信する。(コレクション指向のリポジトリ)

けれど最近ネットの情報を漁っていたところ、以下の文言に出会い、考えを改めました。
引用先:What am I missing with this whole command bus craze?

Command Buses have other advantages. One of my favorites is that they give you a central interface for wrapping all the write operations to your Service Layer. Want to log every operation? Write a decorator for your Command Bus. Want to run a framework validator on every command that gets dropped in? Write a decorator for your Command Bus. It ain't AOP but it gives you that wrapping power where it counts.

google翻訳↓

コマンドバスには他にも利点があります。私のお気に入りの1つは、それらがあなたのサービス層へのすべての書き込み操作をラップするための中心的なインターフェースをあなたに与えるということです。すべての操作を記録したいですか?あなたのコマンドバスのためのデコレータを書きなさい。ドロップインされたすべてのコマンドに対してフレームワークバリデーターを実行したいですか?あなたのコマンドバスのためのデコレータを書きなさい。それはAOPではありませんが、それはあなたがそれが重要なところにその包み込む力を与えます。

なるほどDecoratorパターン。たしかにコマンドに共通の処理を入れたいと考えるとそれぞれに実装しなくてはいけないので大変ですよね。
そこでEventStoreを用いたCQRS+イベントソーシングの実践と考察で使用したソースにコマンドバスを導入したたため、何が嬉しくなったか投稿します。

どういう実装にしたか

必要な個所だけ抜粋します。(元ソース)

    public interface ICommand {}
    public interface ICommandHandler { Task HandleAsync(ICommand _command); }
    public interface ICommandBus { Task ExecuteAsync(ICommand _command); }

    // ↓コマンドの一例.
    public interface I利用者を登録するCommand : ICommand
    {
        string 苗字 { get; }
        string 名前 { get; }

        I利用者を登録するCommand Create(string _苗字, string _名前);
    }
    public interface I利用者を登録するCommandHandler : ICommandHandler { }

こんな感じでコマンドハンドラとコマンド、コマンドバスのインターフェースを定義しました。
コマンドはそのコマンドを実行するために必要なものを格納するクラス。
コマンドハンドラはコマンドの情報を基に処理するクラス。
コマンドバスはコマンドを引数にコマンドハンドラを特定し処理を実行します。

使用するときは、コマンドバスのインスタンスをどこかから持ってきて(Containerとか)コマンドを実行するための情報をかき集めてコマンドを生成してExecuteAsyncをコールする感じです。(ソース)

            await CommandBus.ExecuteAsync(利用者を登録するCommand.Create("田中", "太郎"));

コマンドバスの実装は以下な感じ。コマンドを引数にコマンドハンドラを特定したら実行します。(ソース)

    public class CommandBus: ICommandBus
        public async Task ExecuteAsync(ICommand _command)
        {
            var handlerType = GetHandler(_command);

            var handler = Container.Resolve(handlerType) as ICommandHandler;

             if (handler == null)
                throw new ArgumentException(nameof(_command), "ICommandに対応するICommandHandlerが登録されていません。");

            await handler.HandleAsync(_command);
        }
    }

コンテナへの登録は以下な感じ。(ソース)

                        container.RegisterType<ICommandBus, CommandBus>();

これで後は

public class CommandBusDecorator: ICommandBus
    private ICommandBus CommandBus { get; }
    public CommandBusDecorator(ICommandBus _commandBus) { CommandBus = _commandBus; }
    public async Task ExecuteAsync(ICommand _command)
    {
        // コマンド実行前に好きなことする.
        await CommandBus.ExecuteAsync(_command);
        // コマンド終了後に好きなことする.
    }

みたいなのを作ってコンテナへの登録を

                        container.RegisterType<CommandBus, CommandBus>();
                        container.RegisterFactory<ICommandBus>(c => new CommandBusDecorator(c.Resolve<CommandBus>()));

とすればいくらでもデコレートできるようになります。

何が嬉しいか

コマンドの履歴を楽に取れるようになった

コマンドの終了後にEventStoreに対してCommandHandlerとCommandをWriteしておけば全てのコマンドの履歴がEventStoreで確認できるようになりました。(ソース)

実行時間がわかる

大した話ではないんですけど、コマンド実行前後にストップウォッチで計ってEventDataのMetadataに格納すればひとつのコマンドの実行時間がわかります。(↑の画像参照)
これに限らずMetadaに見たい情報を付与して送信すればシステムのボトルネックの調査とかに利用できそうです。

まとめ

CQRSのコマンド部におけるコマンドバスの有用性についてでした。
今回はコマンドハンドラをすぐに実行していますが、Queueを導入すれば直列処理にもできそうです。

あとEventStoreはRDBMSよりも履歴を取るのが楽だなと思いました。
それからデザインパターンは勉強した気になってもなかなか適用箇所に気付けませんね。

参考