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!

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.

1
public interface Command { }
1
2
3
public interface CommandHandler<T extends Command> {
  void handle(T command);
}
1
2
3
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ć.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.

1
2
3
4
public AutoCommandsBus(final Set<CommandHandler> handlers) {
  this.handlers = handlers.stream()
      .collect(Collectors.toMap(this::obtainHandledCommand, handler -> handler));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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.

1
2
3
4
5
6
@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

1
public interface Event { }
1
2
3
public interface EventHandler<T extends Event> {
  void handle(T event);
}
1
2
3
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}
1
2
3
4
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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.