CQRS w praktyce, wprowadzenie - PHP post

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":"norbert@orzechowicz.pl",
    "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="norbert@orzechowicz.pl" --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 jednego CommandHandler.

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 email, to nie jest błąd. Dojdę do tego w dalszej części tekstu.

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.

Czytaj również

Napisz do autora: norbert@orzechowicz.pl

Podobał Ci się ten wpis?

Akceptuję

Ten serwis używa plików cookies. Więcej o plikach cookies.