Refleksja + CQRS = Niezła mieszanka!
W ostatnim wpisie poruszyłem temat CQRS z perspektywy laika. Napisałem, że chciałbym zaimplementować mechanizm znaleziony na stronie devstyle w swoim kodzie. Byłem przez to zmuszony do użycia po raz pierwszy refleksji i muszę Wam powiedzieć… ME LIKEY!

Utworzenie znacznikowych interfejsów
Maciej Aniserowicz swoje rozwiązanie przedstawił w C#, natomiast ja stanąłem przed wyzwaniem, aby je dostosować do realiów Javy. Rozpocząłem, więc od utworzenia kilku interfejsów. Zacznijmy od przyjrzenia się przypadkowi komend obsługujących zapis w CQRS.
public interface Command { }
public interface CommandHandler<T extends Command> {
void handle(T command);
}
public interface CommandsBus {
void send(Command command);
}
Interfejs Command nie jest niczym szczególnym, ponieważ nic w sobie nie ma. Jest to tylko znacznik dla refleksji. Klasy reprezentujące komendy muszą go po prostu zaimplementować. Natomiast CommandHandler posiada już w sobie metodę handle (wskazuję jeszcze raz, że zwraca void), która będzie obsługiwać TYLKO JEDNĄ wybraną komendę. Jest to bardzo ważne, ponieważ wynika z założeń architektury. Na koniec zostaje szyna CommandsBus obsługująca cały ten proces. Jej implementacja powinna zawierać sposób dopasowywania komend do ich handlerów. I w tym momencie przychodzi nam na pomoc właśnie… REFLEKSJA!
Kierownik, czyli szyna CommandsBus
Będziemy poszukiwać par CommandHandler - Command i rejestrować je w mapie Map<Type, CommandHalder> handlers, gdzie _Type{:target=“blank”} reprezentuje superinterfejs dla wszystkich typów w Javie. Poniższy kawałek kodu wyłuskuje dla wybranego handlera komendę jaką ten ma się opiekować.
private Type obtainHandledCommand(final CommandHandler handler) {
ParameterizedType commandHandlerType = Arrays.stream(handler.getClass().getGenericInterfaces())
.filter(type -> type instanceof ParameterizedType)
.map(type -> (ParameterizedType) type)
.filter(this::isCommandHandlerInterfaceImplemented)
.findFirst()
.orElseThrow(NotImplementedCommandHandlerInterfaceException::new);
return Arrays.stream(commandHandlerType.getActualTypeArguments())
.map(this::acquireCommandImplementationType)
.flatMap(Optional::stream)
.findFirst()
.orElseThrow(NotImplementedCommandInterfaceException::new);
}
Jeżeli dany handler nie będzie poprawnie zaimplementowany dostaniemy wyjątek przy starcie aplikacji. W konstruktorze szyny przekazujemy zbiór naszych handlerów, które można wstrzyknąć np. przy pomocy Springa.
public AutoCommandsBus(final Set<CommandHandler> handlers) {
this.handlers = handlers.stream()
.collect(Collectors.toMap(this::obtainHandledCommand, handler -> handler));
}
@Bean
public CommandHandler<AddCommand> addCommandHandler(EventsBus eventsBus) {
return new AddCommandHandler(eventsBus);
}
@Bean
public CommandHandler<RemoveCommand> removeCommandHandler(EventsBus eventsBus) {
return new RemoveCommandHandler(eventsBus);
}
@Bean
public CommandsBus commandsBus(Set<CommandHandler> handlers) {
return new AutoCommandsBus(handlers);
}
Niby niewiele kodu, ale kryje się za nim wielka potęga. Wystarczy tylko wstrzyknąć szynę do wybranej klasy i wywołać na niej send.
@Override
public void send(final Command command) {
Optional.ofNullable(handlers.get(command.getClass()))
.ifPresentOrElse(handler -> handler.handle(command),
() -> { throw new NoHandlerForCommandException(command); });
}
Voilà! Teraz bez problemu można korzystać z dobroci całej tej magii. Przyjrzyjmy się jeszcze obsłudze eventów.
Obsługa eventów
public interface Event { }
public interface EventHandler<T extends Event> {
void handle(T event);
}
public interface EventsBus {
void publish(Event event);
}
Wygląda to praktycznie identycznie jak w przypadku komend. Jedyną różnicą jest to, że w szynie mamy metodę publish a nie send. Implementacja poszukiwania czy handler poprawnie obsługuje event jest również bliźniaczo podobna do wcześniej przedstawionej.
private Type obtainHandledEvent(final EventHandler handler) {
ParameterizedType eventHandlerType = Arrays.stream(handler.getClass().getGenericInterfaces())
.filter(type -> type instanceof ParameterizedType)
.map(type -> (ParameterizedType) type)
.filter(this::isEventHandlerInterfaceImplemented)
.findFirst()
.orElseThrow(NotImplementedEventHandlerInterfaceException::new);
return Arrays.stream(eventHandlerType.getActualTypeArguments())
.map(this::acquireEventImplementationType)
.findFirst()
.orElseThrow(NotImplementedEventInterfaceException::new);
}
public AutoEventsBus(final Set<EventHandler> handlers) {
this.handlers = handlers.stream()
.collect(Collectors.groupingBy(this::obtainHandledEvent, Collectors.toSet()));
}
Należy zwrócić uwagę na fakt, że używamy tutaj Collectors.groupingBy, ponieważ jeden event może mieć WIELE handlerów. Wynik przypisujemy do mapy Map<Type, Set<EventHandler>> handlers. Całość znowu możemy wykorzystać w frameworku wykorzystującym wstrzykiwanie zależności.
@Bean
public EventHandler<MailEvent> firstMailEvent() {
return new FirstMailEventHandler();
}
@Bean
public EventHandler<MailEvent> secondMailEvent() {
return new SecondMailEventHandler();
}
@Bean
public EventHandler<ChatEvent> chatEvent() {
return new ChatEventHandler();
}
@Bean
public EventsBus eventsBus(Set<EventHandler> handlers) {
return new AutoEventBus(handlers);
}
Teraz tylko wstrzykujemy do wybranej klasy EventsBus i wywołujemy na nim eventsBus.publish(event).
Podsumowanie
Przygoda z implementacją CQRS rozpoczyna się naprawdę ciekawie. Nie wiem jak to będzie wyglądało w praktyce w mojej aplikacji AnimalShelter, ale jestem dobrej myśli po przeklinaniu tego w testowym projekcie. Kod można znaleźć w tym miejscu. W następnym artykule przedstawię testy jednostkowe jakie udało mi się napisać do tego rozwiązania.