Axon Step-By-Step : Command Callback

前回は、Axon Frameworkの根幹であるCommand Handlingを試した。 今回は、Command HandlerからのCallback(戻り値の取得)を試してみる。

前回のSampleCommandHandlerクラスを以下のように書き換える。

import org.axonframework.commandhandling.annotation.CommandHandler;

public class SampleCommandHandler {
    @CommandHandler
    public String handle(HelloCommand command) {
        return String.format("Hello, %s.", command.getName());
    }
}

標準出力していた処理を戻り値として返すようにしただけ。

次に、Command Handlerの戻り値を取得するCommandCallbackインターフェイスの実装クラスを定義する。

import org.axonframework.commandhandling.CommandCallback;

public class SampleCallback implements CommandCallback<String> {
    private String result;
    private Throwable cause;

    @Override
    public void onFailure(Throwable cause) {
        this.cause = cause;
    }

    @Override
    public void onSuccess(String result) {
        this.result = result;
    }

    public String getResult() {
        return result;
    }
    public Throwable getCause() {
        return cause;
    }
}

CommandCallback#onSuccessは、Command Handlerが正常終了した時に呼ばれ、

その戻り値が引数として渡される。

一方、CommandCallback#onFailureは、Command Handlerが例外をThrowした時に呼ばれ、

その例外オブジェクトが引数として渡される。

で、Testを以下のように書き換える。

import org.axonframework.commandhandling.CommandBus;
import org.axonframework.commandhandling.SimpleCommandBus;
import org.axonframework.commandhandling.annotation.AnnotationCommandHandlerAdapter;
import org.junit.Before;
import org.junit.Test;

public class CommandHandlingTest {
    CommandBus commandBus;

    @Before
    public void setup() {
        commandBus = new SimpleCommandBus();
        new AnnotationCommandHandlerAdapter(
        new SampleCommandHandler(), commandBus).subscribe();
    }

    @Test
    public void handleHelloCommand() {
        HelloCommand command = new HelloCommand();
        command.setName("zetta1985");
        SampleCallback callback = new SampleCallback();
        commandBus.dispatch(command, callback);
        assertEquals("Hello, zetta1985.", callback.getResult());
        assertNull(callback.getCause());
    }
}

CommandBus#dispatchの第二引数にCommandCallback実装クラスのインスタンスを渡す事で、 Command Handlingメソッドの戻り値を取得する事ができる。

ちなみに、Command Handlerが別スレッドで動く場合の戻り値取得用として java.util.concurrent.Futureインターフェイスを実装したFutureCallbackというクラスが提供されている。

CommandCallback実装クラスで指定した型パラメータと実際のCommand Handlerが返す値の型が違う場合、 当然のようにClassCastExceptionが発生するので注意。

また、CommandBus#dispatch(command, callback)で実行されたCommand Handlerでthrowされた例外は Callbackに渡されてしまうため、CommandBus#dispatchを呼び出した側は例外が発生したかどうかを確認するために 何らかの方法で例外オブジェクトを取得する必要がある。

この例だと、SampleCallback#getCauseの戻り値がnullである事を検証し Command Handlerで例外が発生していない事を検証しているが、実際に使ってみると非常にメンドクサイ。 戻り値毎にCallbackクラスを定義するのも冗長だし、 だからといって、CommandBus#dispatchのたびに匿名クラスを定義するのも冗長。

さらに、Reference Guideでは

CommandBus#dispatchの呼び出し側の後続処理がCommand Handlerの結果に依存する事は、 「a highly discouraged approach」とされている。

なので、基本的にはCallbackでCommand Handlerの戻り値を取得しない、というポリシーで。

CQRS的には、それが自然。 Commandは更新処理専用、何らかのデータを得る場合はQueryを使いましょう、という事ですな。

余談になるけれど、個人的にはCommandBus#dispatchを直接呼ぶのではなく、 CommandBus#dispatchに処理を委譲するFacadeを使う事を推奨したい。

Aside

まず、Facadeとなるインターフェイスを定義する。

public interface CommandDispatcher {
    void dispatch(Object command);
}

このインターフェイスは、CommandBus#dispatchの呼び出しに必要なCallbackの知識をカプセル化する。 また、このインターフェイスを介す事により、Command要求側がAxon Frameworkに依存しなくなる。

ここでは、戻り値を返さないdispatchメソッドのみを宣言しているが、 Commandの型によって戻り値が異なるdispatchメソッドを定義するなど、 Command要求側の要件にあったメソッドを用意してもいい。

そして、その実装クラスであるDefaultCommandDispatcherクラスを定義する。

import org.apache.commons.lang.Validate;
import org.axonframework.commandhandling.CommandBus;
import org.axonframework.commandhandling.callbacks.VoidCallback;
import org.zetta1985.framework.CommandDispatcher;

public class DefaultCommandDispatcher implements CommandDispatcher {
    private final CommandBus commandBus;

    public DefaultCommandDispatcher(CommandBus commandBus) {
        super();
        Validate.notNull(commandBus);
        this.commandBus = commandBus;
    }

    @Override
    public void dispatch(Object command) {
        Validate.notNull(command);
        commandBus.dispatch(command, new DefaultCallback());
    }

    static class DefaultCallback extends VoidCallback{
        @Override
        protected void onSuccess() {

        }

        @Override
        public void onFailure(Throwable cause) {
            throw (cause instanceof RuntimeException)
                ? (RuntimeException)cause : new CommandDispatchException(cause);
        }
    }
}

CommandDispatcher#dispacthの実装で、DefaultCallbackクラスをインスタンス化、CommandBus#dispacthの 第2引数に渡す。

DefaultCallbackにより、Command Handlerの結果による挙動は以下のようになる。

  • 正常終了時 : 何もしない。
  • 非検査例外発生時 : そのままthrow
  • 検査例外発生時 : RuntimeExceptionサブクラスのCommandDispatchException(別途定義)でラップしてthrow

これで、CommandBus#dispatchを呼び出すたびにわざわざCallbackをインスタンス化する必要が無くなった。

Summary

OpenSourceのFrameworkは、基本的にはどれも汎用的に作られているが、 実際にそのFrameworkを使ってアプリケーションを作る場合には、 その汎用性の高さが仇になる事もしばしば。

そう言った場合には、アプリケーション要件にあった独自実装を軽くラップする事で、 各アプリケーション固有の要件に上手く適合させる事が重要かと。

このエントリーをはてなブックマークに追加
comments powered by Disqus