Mamy to! 🙂 Pierwszy update projektu TheGame. I od razu lecimy z DDD. Na start mamy aż dwa komponenty! Zapraszam serdecznie na lekturę wpisu 🙂

Domena gry: Resources i dlaczego dwa komponenty?

W poprzednim wpisie mówiłem o jednym komponencie: Resources. Bo na początku miał być jeden komponent, który miał być przeznaczony do wszystkiego, co jest związane z surowcami. Miała być obsługa magazynowania, kopania nowych surowców i inne. No ale podczas badania domeny wyszło mi, że tak być nie może.

Zacznijmy od tego: surowce kopią się, czy są kopane? Mi wydaje się, że są kopane. Przez kopalnie. Jest to zupełnie inny kontekst, który może z czasem być bardziej zaawansowany, niż się wydaje. Możemy mieć różne sposoby wyliczania ilości wykopywanych zasobów. Możemy mieć zasoby wyczerpywalne oraz takie ze stałym wydobyciem. Możemy mieć surowce, które… nie będą kopane. Bo będą generowane poprzez wytwarzanie, np. elektryczność. Z czasem może okazać się, że powstaną nowe koncepcje, które będą powoli rozszerzały ten komponent.

Z drugiej strony mamy surowce, które są przechowywane. Póki co, robimy przechowywanie na planecie. Ale szybko może okazać się, że będziemy mieli wiele innych miejsc, które będą mogły gromadzić surowce: księżyce, statki transportowe czy pola zniszczeń, które będą generowane po odbytej walce. Szybko okaże się, że komponent będzie rozszerzał się poprzez np. mechanizmy ładowania i rozładunku. Wydaje mi się, że również jest to coś, co wpłynęło na moją decyzję o rozdzieleniu tych dwóch komponentów od siebie.

Technicznie: co już mamy?

Wiem, wiem – Was interesuje przede wszystkim mięcho. I mam tutaj dla Was dobrą nowinę: zmergowałem pierwszą PRkę ?. Link do niej znajdziecie tutaj: https://github.com/senghe/TheGame/pull/1, będę tutaj nieco posiłkował się jej kodem.

Komendy publiczne vs komendy z getterami

Nie wiem jak Wy, ale ja cenię sobie pracę z Symfony Messengerem. Koncept command busa i handlerów jest dla mnie zbawieniem. Każdą komendę możemy traktować jako input do naszej aplikacji. Jeżeli do jakiegoś procesu nie ma komendy, to z dużą dozą prawdopodobieństwa ten proces nie istnieje.

Co wprawniejsi na pewno zauważyli, że w trakcie kodowania komendy z takich jak ta poniżej:

<?php

declare(strict_types=1);

namespace TheGame\Application\Component\ResourceStorage\Command;

use TheGame\Application\Component\ResourceStorage\Exception\InvalidDispatchAmountException;
use TheGame\Application\SharedKernel\CommandInterface;

final class DispatchResourcesCommand implements CommandInterface
{
    public function __construct(
        public readonly string $planetId,
        public readonly string $resourceId,
        public readonly int $amount,
    ) {
        if ($this->amount <= 0) {
            throw new InvalidDispatchAmountException(
                $this->planetId,
                $this->resourceId,
                $this->amount
            );
        }
    }
}

na takie:

<?php

declare(strict_types=1);

namespace TheGame\Application\Component\ResourceStorage\Command;

use TheGame\Application\Component\ResourceStorage\Exception\InvalidDispatchAmountException;
use TheGame\Application\SharedKernel\CommandInterface;

final class DispatchResourcesCommand implements CommandInterface
{
    public function __construct(
        private readonly string $planetId,
        private readonly string $resourceId,
        private readonly int $amount,
    ) {
        if ($this->amount <= 0) {
            throw new InvalidDispatchAmountException(
                $this->planetId,
                $this->resourceId,
                $this->amount
            );
        }
    }

    public function getPlanetId(): string
    {
        return $this->planetId;
    }

    public function getResourceId(): string
    {
        return $this->resourceId;
    }

    public function getAmount(): int
    {
        return $this->amount;
    }
}

Argumentem za tymi pierwszymi jest to, że mamy przecież nowego PHPa i możemy korzystać z propertek publicznych. I racja, możemy. Tym bardziej, że w projekcie mam PHP w wersji >=8.1. No ale ja zmieniłem na gettery. Te znienawidzone przez wszystkich gettery. Ale ja ich potrzebowałem.

Okazuje się, że podczas początkowego developmentu zapomniałem, że testy jednostkowe piszę w PHPSpecu. W tym PHPSpecu, który robi BDD, czyli Behavior-driven development. Nie będę tutaj bawił się w tłumaczenie, czym to jest – poszukacie sobie sami. Ważne jest to, że przez BDD w speckach nie możemy pisać testów do kodu opartego o właściwości klas. Dlatego właśnie taki prosty immutable z nowego PHPa (słowa kluczowe public i readonly) odpada. No ale z mojej perspektywy – to, czy skorzystam z niemutowalnych publicznych propertiesów czy z getterów, nie jest istotne. I tu mamy immutable i tu mamy immutable. Najważniejsze jest, aby wszędzie było spójnie, i aby komendy i zdarzenia były niezmienne.

Komunikacja komponentów przez obsługę zdarzeń

Mamy dwa komponenty: jeden wpływa na drugi. Jeżeli wykopane zostają nowe surowce (komponent ResourceMines), to ten drugi komponent (ResourceStorage) musi to obsłużyć, czyli załadować te surowce. Jednym ze standardowych rozwiązań jest komunikacja przez obsługę zdarzeń. Każdy z komponentów może produkować zdarzenia (wysyłać je na szynę), a pozostałe mogą na nie nasłuchiwać. Poniżej mamy command handler, który wysyła zdarzenie:

<?php

declare(strict_types=1);

namespace TheGame\Application\Component\ResourceMines\CommandHandler;

use TheGame\Application\Component\ResourceMines\Command\ExtractResourcesCommand;
use TheGame\Application\Component\ResourceMines\Domain\Event\ResourceHasBeenExtractedEvent;
use TheGame\Application\Component\ResourceMines\ResourceMinesRepositoryInterface;
use TheGame\Application\SharedKernel\Domain\PlanetId;
use TheGame\Application\SharedKernel\EventBusInterface;
use TheGame\Application\SharedKernel\Exception\InconsistentModelException;

final class ExtractResourcesCommandHandler
{
    public function __construct(
        private readonly ResourceMinesRepositoryInterface $minesRepository,
        private readonly EventBusInterface $eventBus,
    ) {
    }

    public function __invoke(ExtractResourcesCommand $command): void
    {
        // ...

        $extractionResult = $minesCollection->extract();

        foreach ($extractionResult as $resourceAmount) {
            $event = new ResourceHasBeenExtractedEvent(
                $command->getPlanetId(),
                $resourceAmount->getResourceId()->getUuid(),
                $resourceAmount->getAmount(),
            );
            $this->eventBus->dispatch($event);
        }
    }
}

A tu mamy listener z drugiego komponentu, który nasłuchuje na to zdarzenie:

<?php

declare(strict_types=1);

namespace TheGame\Application\Component\ResourceStorage\EventListener;

use TheGame\Application\Component\ResourceMines\Domain\Event\ResourceHasBeenExtractedEvent;
use TheGame\Application\Component\ResourceStorage\Command\DispatchResourcesCommand;
use TheGame\Application\SharedKernel\CommandBusInterface;

final class DispatchResourcesExtractedByMinesEventListener
{
    public function __construct(
        private readonly CommandBusInterface $commandBus,
    ) {
    }

    public function __invoke(ResourceHasBeenExtractedEvent $event): void
    {
        $command = new DispatchResourcesCommand(
            $event->getPlanetId(),
            $event->getResourceId(),
            $event->getAmount(),
        );
        $this->commandBus->dispatch($command);
    }
}

Listener ma dosyć prostą logikę – wrzuca komendę na command busa. Niektórzy mogą stwierdzić, że „tak tak, inaczej się nie robi”. No ale – wydaje mi się, że robi się. Trzeba tylko znać konsekwencje i na nie się godzić.

Kiedy mamy listener, którego zadaniem jest utworzenie komendy, to znaczy, że tą operację możemy wykonać również z innych miejsc w systemie: z kontrolera, skryptu konsolowego… cokolwiek. Korzystamy w tym miejscu z komendy, czyli potencjalnego punktu wejście do komponentu. Z drugiej strony – czy musi tak być? Czy listener musi pracować na komendzie? Przecież możemy mieć logikę, która nie powinna być dostępna na zewnątrz. Taki przypadek rozwiązujemy przez utworzenie serwisu niepublicznego (nie należącego do tzw. bridge komponentu) i korzystamy z niego. Puryści są zadowoleni, bo listener nie ma logiki, a zwolennicy ukrywania logiki w międzyczasie przybijają sobie piątkę. Mamy win-win 🙂

Uwaga: strefa wolna od Doctrine (póki co)! ?

Tak tak, nie mamy skonfigurowanych encji Doctrinowych. Nie mamy też podłączonego Messengera. Nie mamy prawie nic. I tak w sumie powinno się tworzyć oprogramowanie w DDD. Tutaj domena jest na pierwszym miejscu. Reszta – konfiguracja i infrastruktura – jest na drugim miejscu. Co jednak nie oznacza, że mamy totalną samowolkę. Aby tworzyć oprogramowanie za pomocą tej techniki, musimy mieć pojęcie na temat tego, w jaki sposób będziemy to później podpinali. Musimy wiedzieć, na ile możemy sobie pozwolić, a na ile nie możemy. Dlatego na pierwszych projektach dobrze jest jednak robić domenę oraz konfigurację/infrastrukturę równocześnie. Ja na szczęście szlaki mam już przetarte, dlatego mogę sobie pozwolić na kąpiel w czystej domenie 🙂

Napisałem pierwsze specki

Chwilę to trwało, nie do końca mi się w sumie chciało, ale – mamy 75 specek. Możecie przejrzeć je sobie, odwiedzając katalog spec w głównym katalogu projektu. Mamy tam w dużej mierze lustrzane odbicie katalogu src. Bo specki piszemy jeden do jednego – jeden plik na jedną klasę. To ma swoje zalety i wady, może kiedyś o tym coś niecoś opowiem 🙂

Mamy pierwsze obrazki! 🙂

Na koniec chciałem podzielić się z Wami pierwszymi obrazkami, które prawdopodobnie wykorzystam w podstawowym suite fixturkowym. Kolejno mamy: kopalnię metalu, kopalnię kryształu, syntezator gazu oraz elektrownię słoneczną. Gra jest projektowana w ten sposób, aby ilość zasobów była konfigurowalna, ale w domyślnym zestawie fixturek chciałbym próbować odwzorować to, co miałem te kilkanaście lat temu.

PS. Ładne te obrazki, co nie? 🙂

Plany na następny raz

W zasadzie, to wydaje mi się, że mógłbym podpiąć tego Doctrine oraz Messengera i napisać kilka testów integracyjnych w PHPUnit. I wydaje mi się, że tego możecie oczekiwać na następny raz. A w międzyczasie będę badał kolejny fragment domeny.

Profesjonalista, zajmujący się na co dzień aplikacjami biznesowymi w ekosystemie PHP. Jego pasją jest odkrywanie nowych konceptów programistycznych oraz wzorce architektoniczne. Uwielbia również pisać testy, gdyż jak sam uważa, dobry kod to przetestowany kod.

Comments are closed.