Základní koncepty DDD

Entity (Entity)

Entity je objekt, který je definován svou identitou, nikoli svými atributy. Entity mají životní cyklus a mohou se v průběhu času měnit, ale jejich identita zůstává stejná.

Příklad: Entity v PHP

<?php

namespace App\UserManagement\Domain\Model;

class User
{
    private UserId $id;
    private string $name;
    private Email $email;
    private \DateTimeImmutable $createdAt;

    public function __construct(UserId $id, string $name, Email $email)
    {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
        $this->createdAt = new \DateTimeImmutable();
    }

    public function id(): UserId
    {
        return $this->id;
    }

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

    public function email(): Email
    {
        return $this->email;
    }

    public function changeName(string $name): void
    {
        $this->name = $name;
    }

    public function changeEmail(Email $email): void
    {
        $this->email = $email;
    }

    public function createdAt(): \DateTimeImmutable
    {
        return $this->createdAt;
    }
}

V tomto příkladu je User entita, která je definována svou identitou (UserId). Uživatel může změnit své jméno nebo e-mail, ale jeho identita zůstává stejná.

Hodnotové objekty (Value Objects)

Hodnotové objekty jsou objekty, které jsou definovány svými atributy, nikoli svou identitou. Hodnotové objekty jsou neměnné (immutable) a nemají žádnou identitu.

Příklad: Hodnotový objekt v PHP

<?php

namespace App\UserManagement\Domain\ValueObject;

class Email
{
    private string $value;

    public function __construct(string $value)
    {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException('Invalid email address');
        }

        $this->value = $value;
    }

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

    public function equals(Email $other): bool
    {
        return $this->value === $other->value;
    }

    public function __toString(): string
    {
        return $this->value;
    }
}

V tomto příkladu je Email hodnotový objekt, který je definován svou hodnotou. E-mailová adresa je neměnná a nemá žádnou identitu. Dva e-maily jsou považovány za stejné, pokud mají stejnou hodnotu.

Agregáty (Aggregates)

Agregát je skupina souvisejících objektů, které jsou považovány za jednu jednotku z hlediska změn dat. Každý agregát má kořenovou entitu (aggregate root), která je jediným vstupním bodem pro manipulaci s agregátem.

Příklad: Agregát v PHP

<?php

namespace App\OrderManagement\Domain\Model;

class Order
{
    private OrderId $id;
    private UserId $userId;
    private array $items = [];
    private OrderStatus $status;
    private \DateTimeImmutable $createdAt;

    public function __construct(OrderId $id, UserId $userId)
    {
        $this->id = $id;
        $this->userId = $userId;
        $this->status = OrderStatus::CREATED;
        $this->createdAt = new \DateTimeImmutable();
    }

    public function id(): OrderId
    {
        return $this->id;
    }

    public function userId(): UserId
    {
        return $this->userId;
    }

    public function addItem(ProductId $productId, int $quantity, Money $price): void
    {
        if ($this->status !== OrderStatus::CREATED) {
            throw new \DomainException('Cannot add items to a non-created order');
        }

        $this->items[] = new OrderItem($this->id, $productId, $quantity, $price);
    }

    public function removeItem(ProductId $productId): void
    {
        if ($this->status !== OrderStatus::CREATED) {
            throw new \DomainException('Cannot remove items from a non-created order');
        }

        $this->items = array_filter($this->items, function (OrderItem $item) use ($productId) {
            return !$item->productId()->equals($productId);
        });
    }

    public function confirm(): void
    {
        if ($this->status !== OrderStatus::CREATED) {
            throw new \DomainException('Cannot confirm a non-created order');
        }

        if (empty($this->items)) {
            throw new \DomainException('Cannot confirm an empty order');
        }

        $this->status = OrderStatus::CONFIRMED;
    }

    public function cancel(): void
    {
        if ($this->status !== OrderStatus::CREATED && $this->status !== OrderStatus::CONFIRMED) {
            throw new \DomainException('Cannot cancel a non-created or non-confirmed order');
        }

        $this->status = OrderStatus::CANCELLED;
    }

    public function items(): array
    {
        return $this->items;
    }

    public function status(): OrderStatus
    {
        return $this->status;
    }

    public function createdAt(): \DateTimeImmutable
    {
        return $this->createdAt;
    }
}

V tomto příkladu je Order agregát, který obsahuje kolekci OrderItem objektů. Order je kořenovou entitou (aggregate root) a poskytuje metody pro manipulaci s položkami objednávky.

Repozitáře (Repositories)

Repozitář je objekt, který poskytuje rozhraní pro přístup k agregátům. Repozitáře skrývají detaily persistence a poskytují doménově orientované rozhraní pro přístup k datům.

Příklad: Repozitář v PHP

<?php

namespace App\Domain\Repository;

use App\Domain\Model\Order;
use App\Domain\Model\OrderId;
use App\Domain\Model\UserId;

interface OrderRepository
{
    public function save(Order $order): void;

    public function findById(OrderId $id): ?Order;

    public function findByUserId(UserId $userId): array;
}

V tomto příkladu je OrderRepository rozhraní, které definuje metody pro ukládání a načítání objednávek. Konkrétní implementace tohoto rozhraní by mohla používat Doctrine ORM nebo jiný mechanismus persistence.

Doménové služby (Domain Services)

Doménová služba je objekt, který poskytuje doménovou logiku, která nepatří přirozeně do žádné entity nebo hodnotového objektu. Doménové služby jsou bezstavové a pracují s entitami a hodnotovými objekty.

Příklad: Doménová služba v PHP

<?php

namespace App\Domain\Service;

use App\Domain\Model\Order;
use App\Domain\Model\Payment;
use App\Domain\Model\PaymentId;
use App\Domain\Repository\PaymentRepository;

class PaymentService
{
    private PaymentRepository $paymentRepository;

    public function __construct(PaymentRepository $paymentRepository)
    {
        $this->paymentRepository = $paymentRepository;
    }

    public function processPayment(Order $order, PaymentMethod $paymentMethod): Payment
    {
        if ($order->status() !== OrderStatus::CONFIRMED) {
            throw new \DomainException('Cannot process payment for a non-confirmed order');
        }

        $payment = new Payment(
            new PaymentId(),
            $order->id(),
            $this->calculateTotalAmount($order),
            $paymentMethod
        );

        $this->paymentRepository->save($payment);

        return $payment;
    }

    private function calculateTotalAmount(Order $order): Money
    {
        $total = new Money(0);

        foreach ($order->items() as $item) {
            $total = $total->add($item->price()->multiply($item->quantity()));
        }

        return $total;
    }
}

V tomto příkladu je PaymentService doménová služba, která poskytuje logiku pro zpracování plateb. Tato logika nepatří přirozeně do žádné entity nebo hodnotového objektu.

Doménové události (Domain Events)

Doménová událost je objekt, který reprezentuje něco, co se stalo v doméně a co by mohlo být zajímavé pro jiné části systému. Doménové události jsou neměnné a obsahují informace o tom, co se stalo.

Příklad: Doménová událost v PHP

<?php

namespace App\Domain\Event;

use App\Domain\Model\OrderId;
use App\Domain\Model\UserId;

class OrderCreatedEvent
{
    private OrderId $orderId;
    private UserId $userId;
    private \DateTimeImmutable $occurredAt;

    public function __construct(OrderId $orderId, UserId $userId)
    {
        $this->orderId = $orderId;
        $this->userId = $userId;
        $this->occurredAt = new \DateTimeImmutable();
    }

    public function orderId(): OrderId
    {
        return $this->orderId;
    }

    public function userId(): UserId
    {
        return $this->userId;
    }

    public function occurredAt(): \DateTimeImmutable
    {
        return $this->occurredAt;
    }
}

V tomto příkladu je OrderCreatedEvent doménová událost, která reprezentuje vytvoření nové objednávky. Tato událost obsahuje informace o tom, která objednávka byla vytvořena, pro kterého uživatele a kdy se to stalo.

Ohraničené kontexty (Bounded Contexts)

Ohraničený kontext je explicitní hranice, ve které je model platný. V rámci ohraničeného kontextu existuje konzistentní model a všudypřítomný jazyk. Různé ohraničené kontexty mohou mít různé modely a jazyky.

E-shop Katalog zboží Objednávky Zákazníci Fakturace Katalog Produkt Kategorie Vyhledávání Objednávka Položka objednávky Doprava Způsob platby Zákazník Adresa Košík Faktura Fakturační adresa DPH Upstream / Downstream (OHS) Partnership Customer / Supplier

Příklad: Ohraničené kontexty v PHP

src/
├── OrderProcessing/           # Ohraničený kontext: Zpracování objednávek
│   ├── Domain/
│   │   ├── Model/
│   │   │   ├── Order.php
│   │   │   ├── OrderItem.php
│   │   │   └── OrderStatus.php
│   │   └── Repository/
│   │       └── OrderRepository.php
│   └── Application/
│       ├── Command/
│       │   ├── CreateOrder.php
│       │   └── CreateOrderHandler.php
│       └── Query/
│           ├── GetOrder.php
│           └── GetOrderHandler.php
└── UserManagement/            # Ohraničený kontext: Správa uživatelů
    ├── Domain/
    │   ├── Model/
    │   │   ├── User.php
    │   │   └── UserStatus.php
    │   └── Repository/
    │       └── UserRepository.php
    └── Application/
        ├── Command/
        │   ├── RegisterUser.php
        │   └── RegisterUserHandler.php
        └── Query/
            ├── GetUser.php
            └── GetUserHandler.php

V tomto příkladu jsou OrderProcessing a UserManagement dva ohraničené kontexty. Každý kontext má svůj vlastní model a jazyk. V kontextu OrderProcessing může být uživatel reprezentován pouze jako UserId, zatímco v kontextu UserManagement je uživatel reprezentován jako plnohodnotná entita User.

Všudypřítomný jazyk (Ubiquitous Language)

Všudypřítomný jazyk je společný jazyk používaný vývojáři a doménovými experty. Tento jazyk je používán v kódu, dokumentaci a komunikaci. Všudypřítomný jazyk pomáhá překonat komunikační bariéry mezi vývojáři a doménovými experty.

Doménový expert Vývojový tým Ubiquitous Language Byznys vize a požadavky Doménová terminologie Implementace kódu Technická terminologie Sdílený jazyk Slovník pojmů Context Map

Příklad: Všudypřítomný jazyk v e-commerce doméně

V e-commerce doméně by všudypřítomný jazyk mohl zahrnovat pojmy jako:

  • Košík (Cart) - Dočasná kolekce produktů, které si zákazník vybral k nákupu.
  • Objednávka (Order) - Potvrzený nákup zákazníka, který obsahuje produkty, dodací adresu a platební informace.
  • Katalog (Catalog) - Kolekce všech produktů dostupných k prodeji.
  • Zákazník (Customer) - Osoba, která nakupuje produkty.
  • Produkt (Product) - Položka, která je dostupná k prodeji.
  • Kategorie (Category) - Skupina souvisejících produktů.
  • Platba (Payment) - Transakce, kterou zákazník platí za objednávku.
  • Dodání (Shipping) - Proces doručení objednávky zákazníkovi.

Tyto pojmy by byly používány konzistentně v kódu, dokumentaci a komunikaci mezi vývojáři a doménovými experty. Například, místo použití termínu "uživatel" by se používal termín "zákazník", pokud se jedná o osobu, která nakupuje produkty.

Důležité poznámky

Při implementaci DDD je důležité:

  • Používat všudypřítomný jazyk konzistentně v celém projektu.
  • Definovat jasné hranice mezi ohraničenými kontexty.
  • Používat agregáty pro zajištění konzistence dat.
  • Používat repozitáře pro přístup k agregátům.
  • Používat doménové události pro komunikaci mezi ohraničenými kontexty.

V další kapitole se podíváme na implementaci DDD v Symfony 7.