2011-03-02
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; /** * @author t_hara */ 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というクラスが提供されている。*1
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」*2とされている。
なので、基本的には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に依存しなくなる。*3
ここでは、戻り値を返さない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; /** * @param 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() { // do nothing. } @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を使ってアプリケーションを作る場合には、
その汎用性の高さが仇になる事もしばしば。
そう言った場合には、アプリケーション要件にあった独自実装を軽くラップする事で、
各アプリケーション固有の要件に上手く適合させる事が重要かと。
Axon Frameworkは、実はその点においても優れているのだけれど、それは追々書くとして
次回は CQRS & EventSourcing の真骨頂、Aggregateについて。
