O CQRS (Command Query Responsibility Segregation) jest w ostatnich latach bardzo głośno. Sam wielokrotnie mówiłem na ten temat prezentację i napisałem artykuł do ProgramistaMag. W tym tekście pominę wstęp teoretyczny i wskoczę prosto w kodzik.
[blogvember2016 no=”8″]
Commands
Podstawowym elementem składowym CQRS są komendy. Zresztą: sama nazwa o tym mówi, prawda? Command pattern, czyli zamknięcie wszystkich informacji potrzebnych do wykonania dowolnej akcji w osobnej, dedykowanej klasie. To tak, jakby… wziąć wszystkie parametry z jakiejś metody i je opakować.
Potrzebujemy w systemie abstrakcji nad pojęciem “komendy”. W .NET doskonale nada się do tego interfejs. Jednak komendy nie potrzebują współdzielić żadnych informacji, nie mają niczego wspólnego. Więc będzie to interfejs pusty, tzw. “marker interface“:
public interface ICommand { }
Tak, wiem, że można do tego zastosować również atrybuty/adnotacje. Owszem, można. Ale atrybuty to po prostu… więcej zachodu. I nie dają możliwości wykorzystania generics, co bardzo nam się przyda.
Każda komenda zaimplementuje taki interfejs. Ale co on daje, skoro jest pusty? Wbrew pozorom, całkiem sporo: daje nam możliwość łatwego odnalezienia wszystkich komend w systemie. Czy to ręcznie, przez programistę (“go to implementation” w R#), czy też przez runtime, za pomocą refleksji. To drugie zastosowanie wyciśniemy już za chwilę.
Skoro mamy komendę, to potrzebujemy “coś, co ją wykona”. Czyli: command handler. No to siup!
public interface IHandleCommand { }
W CQRS każda komenda ma jeden handler. Nie zero, nie wiele. JEDEN. Można o tym myśleć jak o rozbiciu metody na dwie osobne części. Argumenty i ciało metody w dedykowanych klasach.
Ale to jest znowu marker interface: nic nie robi, niczego nie definiuje! To prawda, dlatego potrzebujemy:
public interface IHandleCommand<TCommand> : IHandleCommand where TCommand : ICommand { void Handle(TCommand command); }
Wersja generyczna już przy deklaracji swojego typu mówi, jaką komendę obsługuje. I wreszcie mamy jakąś metodę: Handle().
Brakuje jeszcze jednego elementu spinającego całość: mechanizmu dystrybucji komend w systemie. “Szyny komend”:
public interface ICommandsBus { void Send<TCommand>(TCommand command) where TCommand : ICommand; }
Implementacja tego interfejsu może być różna w zależności od specyfiki systemu. Jedno z rozwiązań to:
public class CommandsBus : ICommandsBus { private readonly Func<Type, IHandleCommand> _handlersFactory; public CommandsBus(Func<Type, IHandleCommand> handlersFactory) { _handlersFactory = handlersFactory; } public void Send<TCommand>(TCommand command) where TCommand : ICommand { var handler = (IHandleCommand<TCommand>)_handlersFactory(typeof(TCommand)); handler.Handle(command); } }
Co my tu mamy? Dependency Injection! Nasza szyna jest zależna od “fabryki handlerów”: funkcji, która na podstawie typu wiadomości potrafi stworzyć instancję jej handlera. To jest abstrakcja doskonała: zostajemy przy czystym .NET (bo Func<>), nie uzależniamy się na tym etapie od żadnego kontenera DI, a jednocześnie dokładnie modelujemy swoje potrzeby. “Dam temu czemuś komendę, a to coś da mi handler”. Wsio.
Events
Koncept CQRS jest rozszerzalny o zdarzenia, czyli Eventy. Zdarzenia mogą posłużyć wielu celom (event sourcing, any1?), ale ich najbardziej podstawowe zastosowanie to: poinformować resztę systemu, że COŚ się wydarzyŁO.
Struktury potrzebne do ich implementacji są lustrzanym odbiciem tego, co znamy już z komend. Czyli:
public interface IEvent { }
public interface IHandleEvent { }
public interface IHandleEvent<TEvent> : IHandleEvent where TEvent : IEvent { void Handle(TEvent @event); }
Różnice zaczynamy zauważać przy “szynie zdarzeń”:
public interface IEventsBus { void Publish<TEvent>(TEvent @event) where TEvent : IEvent; }
Metoda “Send()” z CommandBus została zastąpiona metodą “Publish()”. Koncepcyjnie bowiem zdarzenia mogą mieć dowolną liczbę handlerów. Dowolną, czyli: 0, 1 lub wiele.
Łatwo domyślić się zatem, jak może wyglądać przykładowa implementacja:
public class EventsBus : IEventsBus { private readonly Func<Type, IEnumerable<IHandleEvent>> _handlersFactory; public EventsBus(Func<Type, IEnumerable<IHandleEvent>> handlersFactory) { _handlersFactory = handlersFactory; } public void Publish<TEvent>(TEvent @event) where TEvent : IEvent { var handlers = _handlersFactory(typeof(TEvent)) .Cast<IHandleEvent<TEvent>>(); foreach (var handler in handlers) { handler.Handle(@event); } } }
Jest to PRAWIE to samo co CommandBus, tylko obsługujemy scenariusz “wielu handlerów”. Uzależniamy się od funkcji zwracającej IEnumerable. Pięknie.
No i dobra, ale jak to wszystko… spiąć?
Wire it up!
O Dependency Injection pisałem już wielokrotnie, i pewnie jeszcze nieraz napiszę. I właśnie DI z sensownym kontenerem pomoże nam całość posklejać. Poukładać, dopasować, jak puzzle. I wreszcie: uruchomić.
Poniższy kod przedstawia konfigurację Autofac, mojego ulubionego kontenera DI w .NET. Autofac poważam, ponieważ bardzo mocno separuje on proces konfiguracji i tworzenia kontenera od procesu jego używania. Służą do tego nawet osobne klasy: ContainerBuilder i Container.
Konfiguracja zachowania Autofaca odbywa się w Modułach. Czyli: nie wrzucamy wszystkich porejestrowanych zależności na hurra w jedno miejsce, a sensownie dzielimy pomiędzy dedykowane klasy. Podobny mechanizm znajdziemy zresztą chociażby w StructureMap (Rejestry) czy Castle Windsor (Instalatory).
Zarejestrowanie WSZYSTKIEGO związanego z komendami można umieścić w CommandsModule:
public class CommandsModule : Module { protected override void Load(ContainerBuilder builder) { base.Load(builder); builder.RegisterAssemblyTypes(ThisAssembly) .Where(x => x.IsAssignableTo<IHandleCommand>()) .AsImplementedInterfaces(); builder.Register<Func<Type, IHandleCommand>>(c => { var ctx = c.Resolve<IComponentContext>(); return t => { var handlerType = typeof (IHandleCommand<>).MakeGenericType(t); return (IHandleCommand) ctx.Resolve(handlerType); }; }); builder.RegisterType<CommandsBus>() .AsImplementedInterfaces(); } }
I pozamiatane. Rejestrujemy wszystkie handlery, ich fabrykę, a na koniec samą szynę. Na uwagę zasługuje głównie proces rejestracji fabryki handlerów. Pamiętajcie, że rejestrując własną fabrykę w Autofac nie można używać bezpośrednio przekazanego parametru “c”: trzeba z niego pobrać IComponentContext i dopiero ten kontekst wykorzystać w zwracanej lambdzie. Inaczej: w końcu coś się wywali.
Dzięki tym rejestracjom jesteśmy w stanie dorzucić do kontrolera (czy gdziekolwiek indziej) zależność na ICommandBus i cieszyć się dystrybucją komend w systemie.
Podobnie wygląda sprawa ze zdarzeniami, tylko ze względu na konieczność użycia IEnumerable, rejestracja fabryki handlerów jest nieco bardziej skomplikowana:
public class EventsModule : Module { protected override void Load(ContainerBuilder builder) { base.Load(builder); builder.RegisterAssemblyTypes(ThisAssembly) .Where(x => x.IsAssignableTo<IHandleEvent>()) .AsImplementedInterfaces(); builder.RegisterGeneric(typeof (AllEventsHandler<>)) .As(typeof (IHandleEvent<>)); builder.Register<Func<Type, IEnumerable<IHandleEvent>>>(c => { var ctx = c.Resolve<IComponentContext>(); return t => { var handlerType = typeof(IHandleEvent<>).MakeGenericType(t); var handlersCollectionType = typeof(IEnumerable<>).MakeGenericType(handlerType); return (IEnumerable<IHandleEvent>)ctx.Resolve(handlersCollectionType); }; }); builder.RegisterType<EventsBus>() .AsImplementedInterfaces(); } }
Dwa razy “MakeGenericType()”, jedno po drugim, GENERICS AAALLLEEEEERT!!!!
To tak naprawdę nic szczególnie skomplikowanego. Po prostu potrzebujemy zarejestrować w kontenerze fabryki generyczne bez wykorzystania notacji generycznej.
RUN!
Hyc, koniec.
Zostało dorzucić do swojego systemu, zarejestrować oba moduły w kontenerze i używać. Poimplementować komendy i handlery i cieszyć się, jak się fajnie wywołują. Dorzucić zdarzenia i koncepcyjnie oddzielać AKCJE od REAKCJI.
Have fun.