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
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.