CQRS – Query DTO

Wykorzystanie obiektów DTO w Read Model w architekturze CQRS.

Nawiązując do umieszczonego na łamach bloga artykułu „Auditor – CQRS – Query” chciałbym rozwinąć nieco bardziej tematykę zwracania danych przez Query. W opisywanej implementacji Query, wszystkie dane były reprezentowane za pomocą tablic asocjacyjnych – pojedynczy rekord jak i również kolekcja rekordów. W taki sposób były przekazywane do warstwy wyżej. Niestety nie daje to konkretnej informacji na temat faktycznej zwracanej struktury. Były to tablice asocjacyjne, zawierające jakieś dane w postaci jakiś klucz => wartość. Tyle.

Moim celem było zakomunikowanie w jasny sposób, jakie konkretnie informacje zwraca dane Query. Rozwiązanie zostało oparte o wzorzec DTO, o którym przeczytasz poniżej.

DTO

DTO czyli Data Transfer Object jest wzorcem dystrybucji. Jego zadaniem jest grupowanie danych do postaci obiektu, i przenoszenia ich pomiędzy:

  • metodami, klasami, modułami, warstwami aplikacji,
  • procesami, aplikacjami, systemami,
  • i w każdym innym przypadku gdy jest to niezbędne, czytaj – gdy zachodzi potrzeba przekazania danych.

Taki kontener na dane, który nie zawiera żadnej logiki. Łatwo go serializować do dowolnego formatu (np. JSON, XML), a następnie przywrócić do postaci obiektu. DTO to obiekty niezależne od domeny, ale także domena jest niezależna od nich – wykluczając obiekty które na podstawie DTO tworzą obiekty domenowe (i odwrotnie) – np. wykorzystując wzorce kreacyjne.

Query a DTO

W projekcie Auditor wyróżniam dwa typy obiektów DTO które zwracane są przez Query:

DTO reprezentujące pojedynczy byt

Czyli DTO reprezentujące jednostkę – w przypadku Auditor jest to np. ProjectDto, zawierający podstawowe informacje o identyfikatorze i nazwie projektu.

<?php

class ProjectDto implements \JsonSerializable
{
    public $id;
    public $name;

    public function __construct(array $data)
    {
        $this->id = isset($data['id']) ? (int)$data['id'] : null;
        $this->name = isset($data['name']) ? (string)$data['name'] : '';
    }

    public function jsonSerialize()
    {
        return $this;
    }
}

Obiekt uzupełniany jest w momencie tworzenia instancji, z danych dostarczonych w postaci tablicy asocjacyjnej. W konstruktorze następuje etap przypisania odpowiednich wartości do właściwości obiektu. Właściwości posiadają widoczność typu public, aby w łatwy sposób można było uzyskiwać informacje na temat transportowanych danych.

Każdy DTO w projekcie musi być łatwo serializowany do formatu JSON. Dlatego też, implementuję interfejs JsonSerializable. O samym interfejsie rozpisałem się w artykule: PHP – Serializacja obiektów za pomocą interfejsu JsonSerializable.

Jak się domyślasz właściwości o widoczności public można dowolnie zmieniać po utworzeniu instancji obiektu. Aby temu zapobiec musiałbym zmienić widoczność tych właściwości na private i stworzyć gettery dla każdego z nich. Najlepszym rozwiązaniem byłoby ustawienie ich jako readonly, taką możliwość daje np. język C#. Niestety w PHP takiej możliwości nie posiadamy, a gettery dla właściwości to przerost formy nad treścią (jak na razie). Dlatego zdecydowałem aby operować na właściwościach publicznych z założeniem, że na obiektach DTO nie można zmieniać wartości właściwości.

To jednak jedynie założenie, i w projekcie większym, tworzonym przez kilku programistów pewne założenia mogą się zatracać – jak nie od razu, to po jakimś czasie. To temat akurat na kolejny artykuł 🙂

Po stworzeniu DTO musiałem jeszcze nanieść poprawkę na Query.

<?php

public function execute(Dbal $dbal) : ProjectDto
{
    $sql = 'SELECT * FROM project WHERE id = :id LIMIT 1';
    $project = $dbal->fetchAssoc($sql, [':id' => $this->projectId]);

    return new ProjectDto($project);
}

Zwrócone dane z zapytania SQL wykorzystuję do stworzenia instancji obiektu DTO. Dodatkowo poprawiam return type dla metody. Teraz w jasny i jednoznaczny sposób określiłem, jaka struktura danych zwracana jest przez metodę, a zwracam instancję klasy ProjectDto. Nie muszę się już domyślać, ani też debugować kodu aby dowiedzieć się co zwraca metoda execute.

DTO będące kolekcją innych DTO

Tablica obiektów zawsze przyprawia mnie o dreszcze. Podobnie jak zwracanie tablicy asocjacyjnej w przypadku pojedynczych elementów, tak i tutaj nie wiadomo nic na temat struktury zwracanych danych. Moim celem było stworzenie obiektu który jasno wskazuje z jaką kolekcją elementów mamy doczynienia. ProjectCollectionDto jest kolekcją obiektów ProjectDto. Czyli „opakowaniem” na zbiór obiektów DTO reprezentujących „projekt”.

Do implementacji takiej kolekcji wykorzystałem klasę z biblioteki standardowej SPLArrayIterator. Wystarczy, że podczas tworzenia instancji obiektu przekażę tablicę z wynikami zapytania, a następnie zmodyfikuję zachowanie metody current. Tak aby zwracała obiekt DTO. Całość obrazuje poniższy przykład:

<?php

class ProjectCollectionDto extends \ArrayIterator implements \JsonSerializable
{
    public function current()
    {
        return new ProjectDto(parent::current());
    }

    public function jsonSerialize()
    {
        return iterator_to_array($this);
    }
}

Warto zwrócić uwagę na metodę jsonSerialize. Wykorzystałem funkcję iterator_to_array po to aby móc dalej korzystać z konwertowania DTO do typu JSON – z prawidłowym uwzględnieniem DTO dla pojedynczych elementów. Inaczej konwertowany jest element tablicy, a nie instancja DTO (funkcja iterator_to_array przetworzy iterator do tablicy, wywołując jednocześnie metodę current dla każdego elementu w celu jego pobrania).

Innym rozwiązaniem jest tworzenie wszystkich DTO w konstruktorze. Byłoby to o tyle lepsze rozwiązanie, że DTO tworzymy tylko raz. W aktualnej wersji tworzenie instancji obiektu DTO następuje za każdym razem gdy chcemy pobrać element kolekcji. Ulepszoną implementację, zamierzam wprowadzić w projekcie Auditor.

Została jeszcze do wprowadzenia mała poprawka dla Query:

<?php

public function execute(Dbal $dbal) : ProjectCollectionDto
{
    $sql = 'SELECT * FROM project';
    $results = $dbal->fetchAll($sql);

    return new ProjectCollectionDto($results);
}

Podobnie jak w przypadku zwracania DTO reprezentującego pojedynczy byt, zmieniony został return type.

Podsumowanie

W artykule zaprezentowałem przykład wykorzystania obiektów DTO. Zwracanie ich z Query zamiast tablic informuje programistę o strukturze zwracanych danych – jak pisałem wyżej:

„Nie muszę się już domyślać, ani też debugować kodu aby dowiedzieć się co zwraca metoda”

Dla mnie to największa wartość dodana. Raz zainwestowany czas w stworzenie jednej klasy (lub dwóch w przypadku kolekcji) procentuje podczas powrotu do kodu (po dniu, tygodniu czy miesiącu). Nie ma potrzeby zagłębiania się w szczegóły – jakie wykonywane jest zapytanie SQL, jakie pola zwraca (które od razu lądowały w response do użytkownika). Dostaję podpowiedzi w IDE (a nie magiczne klucze tablic) oraz możliwość nazwania zwracanych pól w dowolnie inny sposób – bez modyfikacji w zapytaniu SQL.

Podsumowując – programujmy obiektowo, wykorzystujmy obiekty – niech reprezentują byt. Uważajmy z tablicami asocjacyjnymi – one nie zapewniają struktury, a źle wykorzystane wprowadzają magię w kodzie którą ciężko zrozumieć, a tym bardziej ujarzmić.

PS. Mięsko związane z Query i DTO możesz na bieżąco śledzić w repozytorium projektu Auditor.

Na co dzień programujący CTO w Emphie Solutions. Projektuje, tworzy oraz wdraża rozwiązania oparte o ekosystem JavaScript. Rozwija swoje umiejętności z zakresu Cloud / DevOps / SRE. Fascynat programowania, architektury, chmury i dobrych praktyk w szerokim ujęciu. Na temat technologii publikuje materiały w ramach projektu DevEnv, którego jest założycielem.
PODZIEL SIĘ