CQRS w praktyce, wprowadzenie - PHP post
2017-01-30
Od ponad roku komercyjnie rozwijam system, w którym model domeny projektowany jest jedynie z uwzględnieniem zasad takich
jak SOLID
, CQS
, Tell Don't Ask
. Ani raz od samego początku nie musieliśmy iść na kompromisy z powodu wybranej technologii,
wydajności czy ograniczeń wynikających z zewnętrznych narzędzi. Nie byłoby to możliwe gdyby nie CQRS,
podział odpowiedzialności i bezwzględne przestrzeganie kilku prostych zasad. Mam nadzieję, że ten tekst będzie pierwszym
z cyklu opisującego wykorzystanie CQRS w praktyce. Postaram się przedstawić kilka podstawowych pojęć tak aby kolejne,
uzupełniające wpisy mogły posłużyć jako ich rozszerzenia. Mam nadzieję, że w ten sposób przedstawię temat lepiej niż
w czasie 45 minutowego wystąpienia na żywo.
Wstęp
Zanim przejdziemy do kodu warto rozwinąć skrót CQRS, Command Query Responsibility Segregation.
Wzorzec opisał, przedstawił i prawdopodobnie wymyślił Greg Young.
Krótko mówiąc, celem tego wzorca jest rozdzielenie modelu służącego do zmiany stanu systemu od modelu służącego
do odczytywania stanu systemu.
Wzorzec ten powstał prawdopodobnie na bazie pojęcia Command Query Separation.
Pojęcie to z kolei wywodzi się z podstaw programowania obiektowego, mówi ono że metody obiektu można podzielić na dwa typy.
- Query - pozwalające uzyskać informację o obiekcie, odczytać go
- Commands - pozwalające wykonać operacje na obiekcie, zmodyfikować go
Trzymając się tej reguły nigdy nie powinniśmy tworzyć czegoś na wzór:
<?php
final class System
{
private $status = 0;
public function commandQuery() : int
{
$this->status = rand(0, 100);
return $this->status;
}
}
Jak widać, metoda commandQuery
służy zarówno do zmiany stanu obiektu System
jak i do jego pobrania.
Odrobinę bardziej przewidywalna w skutkach implementacja tej samej klasy wyglądałaby tak:
<?php
final class System
{
private $status = 0;
public function command() : void
{
$this->status = rand(0, 100);
}
public function query() : int
{
return $this->status;
}
}
To przydługie wprowadzenie było niezbędne przed przejściem do tematu właściwego, CQRS. Myśląc o CQRS warto pamiętać o CQS tylko w skali bardziej globalnej, CQS odnosi się do obiektów, natomiast CQRS do systemu jako całości. CQS mówi, że metody obiektu dzielimy na te które odczytują i te które modyfikują. CQRS natomiast wyznacza sposób w jaki należy komunikować się z systemem, jak zmieniać jego stan oraz jak ten stan odczytywać.
Czym jednak jest wspominany co kilka zdań Stan Systemu?
Załóżmy, że pracujemy z systemem służącym do zarządzania użytkownikami. Stanem systemu jest więc ilość użytkowników czy szczegółowe dane konkretnego użytkownika.
W jaki sposób można zmienić Stan Systemu?
Zmiana stanu to na przykład utworzenie nowego użytkownika czy modyfikacja danych któregokolwiek z użytkowników.
W tym momencie powinniście się już domyślać, że CQRS to nic innego jak uporządkowany i jednolity sposób pozwalający zmieniać i odczytywać stan systemu. Zacznijmy więc od zmian.
Write Model
Zmiana stanu systemu zgodnie z wzorcem CQRS powinna być powodowana jedynie po zmianach w Write Modelu
.
Postaram się wyjaśnić jak zbudować prosty system zbudowany z 3 prostych warstw wspartych przez
implementację konkretnych narzędzi.
- Domain
- Application
- UserInterface
W tej części skupie się jedynie na zmianie stanu systemu, odczyt zostawię na koniec. Trzeba w końcu mieć z czego czytać.
User Interface
Każdy system posiada jakiś interfejs. Czasami jak w przypadku aplikacji desktopowych są to okienka, czasami może to być terminal a czasami protokół HTTP. Komendy mają swój początek właśnie w interfejsie użytkownika i reprezentują jego intencje względem systemu. System może posiadać wiele interfejsów użytkownika. Przykładowo do obsługi systemu zarządzania użytkownikami można stworzyć API RESTowe lub umożliwić dostęp bezpośrednio poprzez terminal. Obydwa interfejsy mogą pozwalać zrobić dokładnie to samo tylko przy użyciu innych narzędzi. W jednym wypadku konieczne będzie wysłanie Requestu HTTP zawierającego email oraz nazwę użytkownika:
POST https://system.com/user
{
"email":"[email protected]",
"username":"norzechowicz"
}
w drugim natomiast trzeba będzie napisać odpowiednią komendą w terminalu. Być może będzie to coś w stylu:
bin/system user:create --email="[email protected]" --username="norzechowicz"
.
Niezależenie od interfejsu użytkownika intencja pozostaje bez zmian. Tak więc śmiało można powiedzieć, że komenda, czyli intencja zmiany stanu systemu powstaje w warstwie interfejsu użytkownika i jest transportowana do warstwy aplikacji, która jest niczym innym niż ciągle wspominanym Systemem.
Droga komendy od interfejsu użytkownika do systemu może być prosta:
<?php
namespace System\Userinterface\Web\Controller;
use System\Application\CommandBus;
class UserController
{
private $commandBus;
public function __construct(CommandBus $commandBus)
{
$this->commandBus = $commandBus;
}
public function createAction(Request $request) : Response
{
$command = new CreateNewUser(
(string) $request->post->get("email"),
(string) $request->post->get("username"),
);
$this->commandBus->handle($command);
return new Response(201);
}
}
Powyższy przykład to chyba najprostsza możliwa droga jaką przebywa komenda. Jest to przykład banalny, można powiedzieć
naiwny ale w większości przypadków wystarczający aby szybko i sprawnie przygotować pierwszą wersję systemu.
Po czasie może okazać się, że żądań utworzenia nowych użytkowników jest tak dużo, że zwrócenie odpowiedzi w sensownym czasie
jest niemożliwe, wtedy zamiast do CommandBus
komendę można wrzucić na kolejkę, jako response zwrócić numer jaki komenda
dostała w danej kolejce i obsłużyć ją w zupełnie innym miejscu.
Command
Komendy to krótko mówiąc żądania. Żądania przychodzące do systemu z zewnątrz. Gdyby system przedstawić jako obiekt, żądania byłyby metodami modyfikującymi ten obiekt. Przykładowa komenda w systemie zarządzania użytkownikami mogłaby wyglądać tak:
<?php
namespace System\Application\Command;
class CreateNewUser
{
private $email;
private $username;
public function __construct(string $email, string $username)
{
$this->email = $email;
$this->username = $username;
}
public function email() : string
{
return $this->email;
}
public function username() : string
{
return $this->username;
}
}
Komendy charakteryzuje fakt, że w zasadzie nie są one obiektami w rozumieniu OOP
, komenda jest bardziej strukturą
danych, która po utworzeniu nie może ulec zmianie. Komenda, będąca żądaniem powinna od momentu powstania do momentu obsłużenia
postać nienaruszona.
Command Bus
Szczerze mówiąc nie do końca wiem gdzie i kiedy powstał ten wzorzec.
Command Bus charakteryzuje się umiejętnością dopasowania dokładnie jednej intencji,
Command
do dokładnie jednegoCommandHandler
.
Istnieją języki programowania, w których coś takiego jak CommandBus
w ogóle nie istnieje. Jeżeli ktoś wie coś więcej na temat genezy powstania CommandBus
to
zapraszam do komentowania.
Najprostsza implementacja CommandBus
mogłaby wyglądać tak:
<?php
namespace System\Application;
use System\Application\Command\CreateNewUserHandler;
final class SimpleCommandBus implements CommandBus
{
private $handlers = [];
public function registerHandler(string $commandClass, $handler) : void
{
Assert::isObject($handler);
Assert::methodExists("handle", $handler);
$this->handlers[$commandClass] = $handler;
}
public function handle($command) : void
{
Assert::isObject($command);
Assert::arrayKeyExists(get_class($command), $this->handlers);
$this->handlers[get_class($command)]->handle($command);
}
}
Oczywiście istnieje wiele gotowych implementacji, wystarczy wybrać, tą która nam najbardziej pasuje.
Command Handler
Jest to w zasadzie ostatni element układanki. CommandHandler
to krótko mówiąc mechanizm interpretujący intencje użytkownika.
Najlepiej będzie przedstawić to na przykładzie.
<?php
namespace System\Application\Command;
use System\Application\Mailer;
use System\Domain\User\Email;
use System\Domain\User\Username;
use System\Domain\Users;
final class CreateNewUserHandler
{
private $users;
public function __construct(Users $users)
{
$this->users = $users;
}
public function handle(CreateNewUser $command) : void
{
$user = new User(
new Email($command->email()),
new Username($command->username())
);
$this->users->add($user);
$mailer->sendWelcomeMessage($command->email());
}
}
Powyższy kod wystarczająco dobrze sam siebie opisuje, nie będę więc tłumaczył do czego służy. Warto jednak
zwrócić uwagę na Users
. Jest to interfejs Repository Pattern, którego implementacja
może być bardzo dowolna. Ja osobiście przygotowałbym implementację System\Infrastructure\Doctrine\ORM\DoctrineUsers
.
Jednak wybór narzędzi jest tutaj zupełnie dowolny.
Domain
Nie będę się tutaj zbytnio rozwodził, model domeny to inaczej obiekty reprezentujące to co najistotniejsze w systemie z punktu widzenia jego właściciela.
<?php
namespace System\Domain;
use System\Domain\User\Email;
use System\Domain\User\Username;
final class User
{
private $id;
private $email;
private $username;
public function __construct(Email $email, Username $username)
{
$this->id = UUID::generate();
$this->email = $email;
$this->username = $username;
}
}
Warto zwrócić uwagę, że ten model nie posiada żadnych metod dostępu, ani do
username
, ani do
Model domeny to krótko mówiąc WriteModel
. Za jego pomocą zmieniamy stan systemu, oraz to właśnie on gwarantuje poprawność
stanu systemu. Model domeny powinien być tak skonstruowany aby pod żadnym pozorem nie dopuścił to utworzenia czegoś co
z biznesowego punktu widzenia jest niepoprawne. W systemie zarządzania użytkownikami można przyjąć, że każdy użytkownik musi
posiadać email oraz username. Dlatego też obiekt User
powinien zadbać o to aby nie dało się go utworzyć bez podania
którejkolwiek z wymaganych wartości.
Write model jak sama nazwa wskazuje służy do zapisu, warto utrzymać go w dobrej formie poprzez nie dodawanie do niego
zbędnych metod. W tej sytuacji mogłoby się wydawać, że istnienie metod User::email() : string
czy User::username() : string
jest jak najbardziej uzasadnione, jednak do momentu kiedy jakakolwiek inna część modelu domeny nie będzie potrzebowała
dostępu do tych danych, nie ma najmniejszego powodu aby je tworzyć. Ponieważ zgodnie z tym co wcześniej napisałem,
Command
służy do zmiany stanu systemu, a CommandHandler
nigdy nie zwraca wartości jaki sens miałoby utworzenie User::email() : string
?
I tak nie dałoby się tego wykorzystać nigdzie poza CommandHandlerem
a skoro nie ma jeszcze CommandHandlera
, który by
tej metody potrzebował to sama metoda też nie musi istnieć.
Osobiście uważam, że tworzenie metod na modelu domeny tylko na potrzeby testów również jest niepoprawnym podejściem. Uzależnia ono model domeny od testów, przez co faktyczne reguły biznesowe łatwo można zgubić pomiędzy szumem informacyjnym.
Infrastructure
Ponieważ uważam Doctrine
za najbardziej stabilny, niezawodny i rozwijany przez bardzo odpowiedzialny zespół mój wybór
nie powinien nikogo zaskoczyć. Oczywiście to nie znaczy, że inne narzędzia są złe, czy niedojrzałe. W moim wypadku Doctrine
wielokrotnie się sprawdził więc nie widzę póki co powodu do zmiany
<?php
namespace System\Infrastructure\Doctrine\ORM;
use system\Domain\Users;
use System\Domain\Exception\UserNotFoundException;
use Doctrine\ORM\EntityManager;
final class DoctrineUsers implements Users
{
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function add(User $user) : void
{
$this->entityManager->persist($user);
$this->entityManager->flush();
}
public function getByEmail(Email $email) : User
{
$user = $this->entityManager->getRepository(User::class)->findOneBy([
'email' => $email
]);
if ($user === null) {
throw new UserNotFoundException();
}
return $user;
}
}
Znowu, jest to najprostsza z możliwych implementacji zarówno repozytorium jak i handlera. W bardziej rozbudowanych przypadkach kiedy jednak handler musi wykonać więcej operacji, które z kolei przekładają się na więcej zapytań do bazy dobrym pomysłem będzie wprowadzenie abstrakcji reprezentującej transakcję jednak to już jest temat na inny wpis.
Wybór Doctrine
w moim wypadku jest nieprzypadkowy, bardzo dobrze sprawdza się on w mapowaniu najdziwniejszych obiektów.
Pozwala to robić przy użyciu bardzo wygodnego mechanizmu Embeddable
lub za pomocą Custom Mapping Type.
Obydwa mają swoje zalety, Embeddable sprawdza się w przypadku mapowania Value Objects
, jak np Email
czy Username
,
Custom Mapping Type świetnie sprawdza się w przypadku wykorzystania pola typu json
do przechowania kolekcji.
Oto jak mogłoby wyglądać mapowanie encji User
:
# User.orm.yml
System\Domain\User:
type: entity
table: sys_user
id:
id:
type: user_id # custom type UserIdType
generator: { strategy: NONE }
fields: ~
# no fields
embedded:
email:
class: System\Domain\User\Email
username:
class: System\Domain\User\Username
# User.Email.orm.yml
System\Domain\User\Email:
type: embeddable
fields:
email:
type: string
length: 255
unique: true
nullable: false
# User.Username.orm.yml
System\Domain\User\Username:
type: embeddable
fields:
email:
type: string
length: 255
unique: true
nullable: false
Jak widać, encja User
wraz z jej trzema składowymi UUID
, Email
, Username
została zmapowana przy użyciu Embeddable
oraz jednego Custom Mapping Type. Dzięki temu model domeny jest praktycznie nietknięty, jest on kompletnie nieświadomy tego
że istnieje coś co przerabia go na wpis w bazie danych.
Write Model - FAQ
Czym jest komenda?
Komenda to prosta struktura danych, nie pozwalająca się modyfikować, reprezentująca intencję użytkownika na zmianę stanu systemu.
Czym jest Write Model?
Write Model to model domeny biznesu, obiekty oraz ValueObjecty
które odzwierciedlają elementy i pojęcia biznesowe.
Write model powinien być w 100% odporny na złe wykorzystanie. Jego konstrukcja powinna uniemożliwiać komuś
utworzenie obiektów nie mających sensu z punktu widzenia biznesowego.
Write Model z powodzeniem może być obserwowany i mapowany przez dowolny ORM
o ile ten ORM
nie narzuca jego architektury.
Dlaczego komenda powinna być niemodyfikowalna?
Wyobraźcie sobie debugowanie problemu, który wnikłby w scenariuszu w którym komenda trafia na kolejkę, następnie jakiś
worker przetwarza komendę, zmienia jej zawartość (czyli zmienia intencję użytkownika), wrzuca ją na inną kolejkę i finalnie
trafia ona do workera, który przepuszcza ją przez CommandBus
. Po co sobie tak życie komplikować? Zamiast zmieniać intencję,
lepiej stworzyć nową komendę i pozwolić użytkownikowi wyrazić inną intencję. Użytkownik wprawdzie wyraża intencje,
ale tylko takie, na które systemu mu pozwala.
Czym jest CommandHandler
?
CommandHandler
to część systemu istniejąca w warstwie aplikacji, której zadaniem jest interpretacja intencji zmiany
stanu systemu. Jeden CommandHandler
obsługuje dokładnie jeden typ Command
, ani więcej ani mniej.
CommandHandler
nigdy nie zwraca wartości, co zresztą jest zgodne z CQS.
Czym jest CommandBus
?
Mechanizm, którego zadaniem jest dopasowanie CommandHandlera
do konkretnego typu Command
. Przykładową i godną polecenia
implementacją CommandBusa
jest Tactician.
Read Model
Odczytanie stanu systemu nie wymaga znajomości aż tak wielu pojęć jak w przypadku jego zmiany. Tutaj sprawa jest o wiele prostsza i skupia się w zasadzie na pobraniu informacji bezpośrednio z źródła danych bez zwracania uwagi na ich poprawność.
Query
Query
w przeciwieństwie do Command
nie jest jakimś konkretnym bytem, który da się opisać. Query
to bardziej
coś na wzór interfejsu, który umożliwia dostęp do danych i pobranie ich w prostej, ujednoliconej postaci. Ta postać to
coś co dla ułatwienia możemy nazwać ViewObject
.
W systemie zarządzania użytkownikami prawdopodobnie najbardziej istotne z punktu widzenia interfejsu użytkownika będzie umożliwienie pobrania ilości wszystkich użytkowników, konkretnego użytkownika lub wybranych użytkowników. Możemy to przedstawić za pomocą prostego interfejsu:
<?php
namespace System\Application\Query\User;
use System\Application\Query\User\UserView;
interface UserQuery
{
public function count() : int;
public function getById(string $userId) : UserView;
public function getAll() : array // w tym momencie moja dusza płacze, że PHP nie ma generyków.
}
Na pewno zwróciliście już uwagę na System\Application\Query\User\UserView
. Tak, to jest właśnie Read Model
, całkowicie
niezależny od Write Modelu
(modelu domeny biznesu). Warto też zauważyć, że Query
istnieje w warstwie Application
.
Prawdopodobnie można by utworzyć kolejną warstwę, będącą na równi z warstwą domeny, pod warstwą Application
, która zawierałaby
jedynie Query
i ViewObjects
jednak nie miałoby to prawdopodobnie żadnej wymiernej wartości.
- Write Model -
System\Domain\User
- Read Model -
System\Application\Query\User\UserView
Te dwa modele reprezentują to samo pojęcie biznesowe, użytkownika.
View Object
View Object
to byt bardzo podobny do Command
, można powiedzieć że powstał w tym samym celu - transportuje dane i zapewnia
ich niemodyfikowalność. Jedyna różnica polega na tym, że transport odbywa się w odwrotną stronę niż w przypadku Command.
Command
transportuje dane z User Interface
do Application
natomiast View Object
transportuje dane z Application
do User Interface
. Tak samo jak Command
, ViewObject
nie jest obiektem a raczej strukturą danych.
<?php
namespace System\Application\Query\User;
final class UserView
{
private $email;
private $username;
public function __construct(string $email, string $username)
{
$this->email = $email;
$this->username = $username;
}
public function email() : string
{
return $this->email;
}
public function username() : string
{
return $this->username;
}
}
W tym konkretnym przypadku ktoś mógłby zapytać po co w ogóle tworzyć 2 modele? Przecież równie dobrze można by
do System\Domain\User
dodać dwa gettery i mielibyśmy dokładnie to samo! I tak i nie. View Object
może być budowany
bezpośrednio po wykonaniu czystego zapytania SQL SELECT u.email, u.username FROM sys_user WHERE u.email = :userEmail
.
Dlaczego ma to znaczenie? Głównie dlatego, że korzystanie z ORM
ma swój koszt. ORM
jest bardzo wygodny, śledzi
obiekt od momentu jego utworzenia (a raczej odtworzenia) i podczas najmniejszej zmiany jest w stanie przygotować odpowiednie
zapytanie SQL. Wszystko to jednak zużywa zasoby, nietrudno wyobrazić sobie sytuację w której musimy na jednej stronie wyświetlić
sto, dwieście, pięćset albo kilka tysięcy obiektów. Teraz wyobraźmy sobie, że ORM
musi sklonować w pamięci stan każdego z
tych obiektów tak aby na końcu móc porównać obiekt wraz z jego stanem w pamięci ORMa
. Sytuacja staje się jeszcze gorsza
kiedy obiekt istnieje w asocjacji z kolekcją innych obiektów.
Często okazuje się, że chcąc wyświetlić potencjalnie proste dane potrzebujemy do tego wykorzystać kilkadziesiąt MB pamięci.
Skutkuje to finalnie zrzuceniem winy na ORM
i traktowanie go jako źródło wszelkiego zła, podczas gdy winę ponosi jego
złe wykorzystanie.
Kolejnym argumentem przemawiającym za oddzielaniem modelu Write
od Read
jest fakt, że dzięki nieśledzeniu ViewObjects
przez ORM
nie ma ryzyka, że ktoś przypadkowo zmieni stan systemu w miejscu, w którym nigdy byśmy się tego nie spodziewali.
Jest to szczególnie istotne w projektach, przy których pracują zespoły złożone z ludzi o różnym poziomie umiejętności.
Nikt chyba nie chciałby się obudzić z $entitiyManager->flush()
wykonanym gdzieś w Twigu
za pomocą Twig_Extension
?
Pamiętajmy też, że przykład prezentowany w tym tekście jest bardzo, bardzo uproszczony. W rzeczywistości tak proste aplikacje
nie istnieją. ViewObject
mogą agregować dane z wielu Write
modeli, mogą te dane normalizować i ułatwiać ich prezentowanie.
Nie trudno wyobrazić sobie system złożony z dwóch kompletnie różnych modeli. Powiedzmy Foo
i Bar
. Ich sposób działania
może być zupełnie różny przez co nie mogą istnieć jako jeden write model jednak sposób w jaki są prezentowany może być identyczny.
Warto wtedy rozważyć utworzenie FooBarView
.
Infrastructure
Jak już wspominałem, View Object
nie potrzebuje ORMa
, co więcej ORM
może okazać się wręcz szkodliwy. View Object
lepiej jest budować samemu z surowych danych. Oto jak może wyglądać prosta implementacja UserQuery
w oparciu o DBAL
.
<?php
namespace System\Infrastructure\Doctrine\Dbal;
use Doctrine\DBAL\Connection;
use System\Application\Query\User\UserView;
use System\Application\Query\User\UserQuery;
final class DbalUserQuery implements UserQuery
{
private $connection;
public function __construct(Connection $connection)
{
$this->connection = $connection;
}
public function count() : int
{
$queryBuilder = $this->connection->createQueryBuilder();
$queryBuilder
->select('count(u.id)')
->from('sys_user', 'u');
return $this->connection->fetchColumn($queryBuilder->getSQL(), $queryBuilder->getParameters());
}
public function getById(string $userId) : UserView
{
$queryBuilder = $this->connection->createQueryBuilder();
$queryBuilder
->select('u.email', 'u.username')
->from('sys_user', 'u')
->where('u.id = :userId')
->setParameter('userId', $userId);
$userData = $this->connection->fetchAssoc($queryBuilder->getSQL(), $queryBuilder->getParameters());
return new UserView($userData['email'], $userData['username']);
}
public function getAll() : array
{
$queryBuilder = $this->connection->createQueryBuilder();
$queryBuilder
->select('u.email', 'u.username')
->from('sys_user', 'u');
$usersData = $this->connection->fetchAll($queryBuilder->getSQL(), $queryBuilder->getParameters());
return array_map(function(array $userData) {
return new UserView($userData['email'], $userData['username']);
}, $usersData);
}
}
Oczywiście wykorzystanie QueryBuildera
jest tutaj kwestią gustu. Ja osobiście nie jestem fanem tego rozwiązania,
kto raz budował bardziej skomplikowane query
przy użyciu buildera
ten wie o czym myślę.
Read Model - FAQ
Czy jeden model domeny odpowiada jednemu View Object
?
Nie, View Object
nie jest związany bezpośrednio z konkretnym modelem. View Object
to jedynie wizualizacja danych.
Może być poskładany z wielu modeli. Przykładowo, w systemie do zarządzania książkami i autorami, View Object
autora,
może agregować książki oraz cytaty, mimo że z punktu widzenia domeny są to osobne modele, jednak w jakiś sposób z sobą
powiązane.
Czy View Object
powinien sprawdzać poprawność danych?
Nie, jeżeli jedyną możliwością na zmianę stanu systemu jest obsłużenie komendy nie ma ryzyka, że
dane użyte do utworzenia View Object
będą nieprawidłowe. Aby dane znalazły się w systemie muszą najpierw przejść
przez Write Model
, który z założenia dba o swoją poprawność, tak więc nie powinno się zdarzyć, że Read Model
powstanie
przykładowo w oparciu o nieprawidłowy adres email czy pusty username.
Czy View Object
podlega regułom OOP?
Nie do końca, View Object
to bardziej struktura danych niż obiekt.
Czy lepiej budować jeden wielki View Object
czy kilka mniejszych wariacji?
Na to pytanie nie ma chyba jednej dobrej odpowiedzi, czasami łatwiej przygotować kilka wariantów View Objectu
,
kiedy indziej nie warto już dopychać niczego więcej do View Objectu
.
Podsumowanie
Jednym z najczęściej powtarzających się pytań w kontekście potencjalnych problemów z CQRS'em jest chyba:
No dobra, ale mój kontroler musi zwrócić zmiany, jak mam to zrobić skoro handler nie zwraca wartości?
Tutaj technik jest kilka, opiszę je napewno w rozszerzeniach do tego tekstu jednak najprościej odpowiedzieć w ten sposób:
<?php
namespace System\Userinterface\Web\Controller;
use System\Application\CommandBus;
class UserController
{
private $commandBus;
private $userQuery;
public function __construct(CommandBus $commandBus, UserQuery $userQuery)
{
$this->commandBus = $commandBus;
$this->userQuery = $userQuery;
}
public function createAction(Request $request) : Response
{
$command = new CreateNewUser(
(string) $request->post->get("email"),
(string) $request->post->get("username"),
);
$this->commandBus->handle($command);
$userView = $this->userQuery->getByEmail($request->post->get("email"));
return new NewUserResponse($userView);
}
}
Wystarczy w kontrolerze najpierw obsłużyć Command
a potem odpytać Query
i przygotować response.
Intencja zawsze zawiera coś co pozwala pobrać zmodyfikowane dane.
Nie jest to wprawdzie zgodne z CQS
jednak często User Interface
rządzi się swoimi prawami.
Gdzie stosować?
Jak zwykle w IT, to zależy. Należy pamiętać, że większość stron www to proste CRUDy
, stąd ogromna popularność gotowców.
Jeżeli jednak pracujesz dla biznesu, w którym istotne są procesy, regresje są niedopuszczalne z nastawieniem na wieloletni
rozwój warto rozważyć CQRS
. Trzeba też pamiętać, że nie cała aplikacja powinna być zbudowana w ten sposób. Nawet
najbardziej skomplikowane systemy składają się w dużej mierze z elementów biznesowo nieistotnych.
Nie ma też co rzucać się na CQRS
kiedy tworzymy stronę wizytówkę zakładu fryzjerskiego kuzynki, wszystko z umiarem.