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.
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.
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.