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!

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.

Podziel się tym z innymi!

Może Ci się również spodoba