Případová studie
Úvod
V této případové studii se podíváme na implementaci systému pro správu projektů pomocí Domain-Driven Design a CQRS v Symfony 7. Systém umožňuje uživatelům vytvářet projekty, přidávat úkoly, přiřazovat úkoly členům týmu a sledovat jejich stav. Tato případová studie demonstruje praktické použití DDD principů v reálném projektu, s důrazem na správnou implementaci strategických a taktických vzorů.
Požadavky
Systém pro správu projektů má následující požadavky:
- Uživatelé se mohou registrovat a přihlašovat.
- Uživatelé mohou vytvářet projekty.
- Uživatelé mohou přidávat úkoly do projektů.
- Uživatelé mohou přiřazovat úkoly členům týmu.
- Uživatelé mohou měnit stav úkolů (To Do, In Progress, Done).
- Uživatelé mohou přidávat komentáře k úkolům.
- Uživatelé mohou sledovat aktivitu na projektech a úkolech.
- Systém musí být škálovatelný a udržitelný.
Architektura
Pro implementaci systému pro správu projektů jsme zvolili kombinaci strategického a taktického DDD s CQRS architekturou v Symfony 7. Na strategické úrovni jsme identifikovali klíčové bounded contexts a jejich vzájemné vztahy. Na taktické úrovni jsme implementovali doménové modely, agregáty, hodnotové objekty a doménové služby. Pro organizaci kódu jsme zvolili vertikální slice architekturu, která nám umožňuje organizovat kód podle funkcí (features) místo technických vrstev, což vede k lepší modularitě a udržitelnosti.
Strategický design: Bounded Contexts a Context Map
Prvním krokem při implementaci DDD bylo identifikovat bounded contexts (ohraničené kontexty) a vytvořit context map (mapu kontextů), která definuje vztahy mezi nimi. Systém je rozdělen do následujících bounded contexts:
- UserManagement - Správa uživatelů, registrace, autentizace. Tento kontext obsahuje vše, co souvisí s identitou a přístupovými právy uživatelů.
- ProjectManagement - Správa projektů, vytváření, aktualizace. Tento kontext se zaměřuje na životní cyklus projektů a jejich vlastnosti.
- TaskManagement - Správa úkolů, vytváření, aktualizace, přiřazování. Tento kontext řeší vše, co souvisí s úkoly v rámci projektů.
- CommentManagement - Správa komentářů, přidávání, aktualizace. Tento kontext se zabývá komunikací a zpětnou vazbou k úkolům.
- ActivityTracking - Sledování aktivity, zaznamenávání událostí. Tento kontext poskytuje přehled o aktivitách v systému.
Mezi těmito kontexty jsme definovali následující vztahy:
- UserManagement ⟷ ProjectManagement: Vztah typu Partnership, kde oba kontexty spolupracují na správě členství uživatelů v projektech.
- ProjectManagement ⟷ TaskManagement: Vztah typu Customer-Supplier, kde ProjectManagement je zákazníkem a TaskManagement dodavatelem služeb pro správu úkolů v rámci projektů.
- TaskManagement ⟷ CommentManagement: Vztah typu Customer-Supplier, kde TaskManagement je zákazníkem a CommentManagement dodavatelem služeb pro komentáře k úkolům.
- ActivityTracking: Vztah typu Conformist ke všem ostatním kontextům, kde ActivityTracking přijímá události z ostatních kontextů a zaznamenává je.
Pro komunikaci mezi kontexty jsme implementovali Anti-Corruption Layer (ACL) tam, kde bylo potřeba překládat koncepty mezi různými kontexty, a použili jsme doménové události pro asynchronní komunikaci.
Taktický design a struktura projektu
Na taktické úrovni jsme implementovali následující DDD vzory:
- Entity - Objekty s identitou, které se v průběhu času mění (např. User, Project, Task).
- Value Objects - Neměnné objekty bez identity, které reprezentují koncepty v doméně (např. UserId, ProjectId, TaskStatus).
- Aggregates - Shluky objektů, které jsou považovány za jednu jednotku z hlediska změn dat (např. Project s TaskCollection).
- Domain Events - Události, které nastávají v doméně a mají význam pro doménové experty (např. ProjectCreated, TaskAssigned).
- Repositories - Objekty, které zapouzdřují přístup k persistenci agregátů (např. ProjectRepository, TaskRepository).
- Domain Services - Služby, které implementují doménovou logiku, která nepatří do žádné entity nebo hodnotového objektu (např. TaskAssignmentService).
Struktura projektu odráží jak strategický, tak taktický design DDD. Níže je ukázka správné struktury projektu, kde každý bounded context má svou vlastní doménovou vrstvu, infrastrukturu a aplikační služby:
src/
├── UserManagement/ # Bounded Context: Správa uživatelů
│ ├── Domain/ # Doménová vrstva
│ │ ├── Model/ # Doménové modely
│ │ │ └── User.php # Entita uživatele
│ │ ├── ValueObject/ # Hodnotové objekty
│ │ │ ├── UserId.php # Identifikátor uživatele
│ │ │ └── Email.php # Email uživatele
│ │ ├── Event/ # Doménové události
│ │ │ └── UserRegistered.php # Událost registrace uživatele
│ │ └── Repository/ # Repozitáře (rozhraní)
│ │ └── UserRepository.php # Rozhraní pro práci s uživateli
│ ├── Infrastructure/ # Infrastrukturní vrstva
│ │ └── Repository/ # Implementace repozitářů
│ │ └── DoctrineUserRepository.php # Doctrine implementace
│ ├── Application/ # Aplikační vrstva
│ │ ├── Command/ # Příkazy
│ │ │ ├── RegisterUser.php # Příkaz pro registraci uživatele
│ │ │ └── RegisterUserHandler.php # Handler příkazu
│ │ └── Query/ # Dotazy
│ │ ├── GetUser.php # Dotaz pro získání uživatele
│ │ └── GetUserHandler.php # Handler dotazu
│ └── Presentation/ # Prezentační vrstva
│ ├── Controller/ # Kontrolery
│ │ ├── RegistrationController.php # Kontroler pro registraci
│ │ └── SecurityController.php # Kontroler pro autentizaci
│ └── ViewModel/ # View modely
│ └── UserViewModel.php # View model uživatele
├── ProjectManagement/ # Bounded Context: Správa projektů
│ ├── Domain/ # Doménová vrstva
│ │ ├── Model/ # Doménové modely
│ │ │ ├── Project.php # Entita projektu (Aggregate Root)
│ │ │ └── ProjectMember.php # Entita člena projektu
│ │ ├── ValueObject/ # Hodnotové objekty
│ │ │ ├── ProjectId.php # Identifikátor projektu
│ │ │ └── ProjectStatus.php # Status projektu
│ │ ├── Event/ # Doménové události
│ │ │ ├── ProjectCreated.php # Událost vytvoření projektu
│ │ │ └── MemberAdded.php # Událost přidání člena
│ │ └── Repository/ # Repozitáře (rozhraní)
│ │ └── ProjectRepository.php # Rozhraní pro práci s projekty
│ ├── Infrastructure/ # Infrastrukturní vrstva
│ │ └── Repository/ # Implementace repozitářů
│ │ └── DoctrineProjectRepository.php # Doctrine implementace
│ ├── Application/ # Aplikační vrstva
│ │ ├── Command/ # Příkazy
│ │ │ ├── CreateProject.php # Příkaz pro vytvoření projektu
│ │ │ └── CreateProjectHandler.php # Handler příkazu
│ │ └── Query/ # Dotazy
│ │ ├── GetProjects.php # Dotaz pro získání projektů
│ │ └── GetProjectsHandler.php # Handler dotazu
│ └── Presentation/ # Prezentační vrstva
│ ├── Controller/ # Kontrolery
│ │ ├── ProjectController.php # Kontroler pro vytvoření projektu
│ │ └── ProjectsController.php # Kontroler pro seznam projektů
│ └── ViewModel/ # View modely
│ └── ProjectViewModel.php # View model projektu
├── TaskManagement/ # Bounded Context: Správa úkolů
│ ├── Domain/ # Doménová vrstva
│ │ ├── Model/ # Doménové modely
│ │ │ └── Task.php # Entita úkolu (Aggregate Root)
│ │ ├── ValueObject/ # Hodnotové objekty
│ │ │ ├── TaskId.php # Identifikátor úkolu
│ │ │ └── TaskStatus.php # Status úkolu
│ │ ├── Event/ # Doménové události
│ │ │ ├── TaskCreated.php # Událost vytvoření úkolu
│ │ │ ├── TaskAssigned.php # Událost přiřazení úkolu
│ │ │ └── TaskStatusChanged.php # Událost změny stavu
│ │ ├── Service/ # Doménové služby
│ │ │ └── TaskAssignmentService.php # Služba pro přiřazení úkolu
│ │ └── Repository/ # Repozitáře (rozhraní)
│ │ └── TaskRepository.php # Rozhraní pro práci s úkoly
│ ├── Infrastructure/ # Infrastrukturní vrstva
│ │ └── Repository/ # Implementace repozitářů
│ │ └── DoctrineTaskRepository.php # Doctrine implementace
│ ├── Application/ # Aplikační vrstva
│ │ ├── Command/ # Příkazy
│ │ │ ├── CreateTask.php # Příkaz pro vytvoření úkolu
│ │ │ ├── CreateTaskHandler.php # Handler příkazu
│ │ │ ├── AssignTask.php # Příkaz pro přiřazení úkolu
│ │ │ ├── AssignTaskHandler.php # Handler příkazu
│ │ │ ├── ChangeTaskStatus.php # Příkaz pro změnu stavu
│ │ │ └── ChangeTaskStatusHandler.php # Handler příkazu
│ │ └── Query/ # Dotazy
│ │ ├── GetTask.php # Dotaz pro získání úkolu
│ │ └── GetTaskHandler.php # Handler dotazu
│ └── Presentation/ # Prezentační vrstva
│ └── Controller/ # Kontrolery
│ ├── TaskController.php # Kontroler pro úkoly
│ ├── AssignController.php # Kontroler pro přiřazení
│ └── StatusController.php # Kontroler pro změnu stavu
├── CommentManagement/ # Bounded Context: Správa komentářů
│ ├── Domain/ # Doménová vrstva
│ │ ├── Model/ # Doménové modely
│ │ │ └── Comment.php # Entita komentáře
│ │ ├── ValueObject/ # Hodnotové objekty
│ │ │ └── CommentId.php # Identifikátor komentáře
│ │ ├── Event/ # Doménové události
│ │ │ └── CommentAdded.php # Událost přidání komentáře
│ │ └── Repository/ # Repozitáře (rozhraní)
│ │ └── CommentRepository.php # Rozhraní pro práci s komentáři
│ ├── Infrastructure/ # Infrastrukturní vrstva
│ │ └── Repository/ # Implementace repozitářů
│ │ └── DoctrineCommentRepository.php # Doctrine implementace
│ ├── Application/ # Aplikační vrstva
│ │ └── Command/ # Příkazy
│ │ ├── AddComment.php # Příkaz pro přidání komentáře
│ │ └── AddCommentHandler.php # Handler příkazu
│ └── Presentation/ # Prezentační vrstva
│ └── Controller/ # Kontrolery
│ └── CommentController.php # Kontroler pro komentáře
├── ActivityTracking/ # Bounded Context: Sledování aktivity
│ ├── Domain/ # Doménová vrstva
│ │ ├── Model/ # Doménové modely
│ │ │ └── Activity.php # Entita aktivity
│ │ ├── ValueObject/ # Hodnotové objekty
│ │ │ └── ActivityId.php # Identifikátor aktivity
│ │ └── Repository/ # Repozitáře (rozhraní)
│ │ └── ActivityRepository.php # Rozhraní pro práci s aktivitami
│ ├── Infrastructure/ # Infrastrukturní vrstva
│ │ └── Repository/ # Implementace repozitářů
│ │ └── DoctrineActivityRepository.php # Doctrine implementace
│ ├── Application/ # Aplikační vrstva
│ │ └── Command/ # Příkazy
│ │ ├── RecordActivity.php # Příkaz pro zaznamenání aktivity
│ │ └── RecordActivityHandler.php # Handler příkazu
│ └── Presentation/ # Prezentační vrstva
│ └── Controller/ # Kontrolery
│ └── ActivityController.php # Kontroler pro aktivity
└── Shared/ # Sdílené komponenty
├── Domain/ # Sdílená doménová logika
│ ├── Exception/ # Výjimky
│ │ └── DomainException.php # Základní doménová výjimka
│ └── Bus/ # Rozhraní pro message bus
│ ├── CommandBus.php # Rozhraní pro command bus
│ └── QueryBus.php # Rozhraní pro query bus
└── Infrastructure/ # Sdílená infrastruktura
├── Bus/ # Implementace message bus
│ ├── MessengerCommandBus.php # Implementace command bus
│ └── MessengerQueryBus.php # Implementace query bus
└── Persistence/ # Sdílená persistence
└── DoctrineTypes/ # Vlastní Doctrine typy
└── UuidType.php # Typ pro UUID
Implementace
Nyní se podíváme na implementaci některých klíčových částí systému, s důrazem na správnou aplikaci DDD principů.
Ubiquitous Language
Před zahájením implementace jsme vytvořili Ubiquitous Language (všudypřítomný jazyk) ve spolupráci s doménovými experty. Tento jazyk je používán konzistentně v kódu, dokumentaci a komunikaci. Například:
- Project - Organizační jednotka, která sdružuje související úkoly a členy týmu.
- Task - Jednotka práce, která má být dokončena v rámci projektu.
- Assignee - Člen týmu, kterému je přiřazen úkol.
- Status - Stav úkolu (To Do, In Progress, Done).
- Comment - Textová zpětná vazba k úkolu.
- Activity - Záznam o akci provedené v systému.
Doménový model: Projekt (Aggregate Root)
<?php
namespace App\ProjectManagement\Domain\Model;
use App\ProjectManagement\Domain\Event\ProjectCreated;
use App\ProjectManagement\Domain\ValueObject\ProjectId;
use App\UserManagement\Domain\ValueObject\UserId;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'projects')]
class Project
{
#[ORM\Id]
#[ORM\Column(type: 'string', length: 36)]
private string $id;
#[ORM\Column(type: 'string', length: 255)]
private string $name;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $description;
#[ORM\Column(type: 'string', length: 36)]
private string $ownerId;
#[ORM\Column(type: 'json')]
private array $memberIds = [];
#[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $createdAt;
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
private ?\DateTimeImmutable $updatedAt = null;
private array $events = [];
public function __construct(ProjectId $id, string $name, ?string $description, UserId $ownerId)
{
$this->id = $id->value();
$this->name = $name;
$this->description = $description;
$this->ownerId = $ownerId->value();
$this->memberIds = [$ownerId->value()];
$this->createdAt = new \DateTimeImmutable();
$this->recordEvent(new ProjectCreated($id, $name, $ownerId));
}
public function id(): ProjectId
{
return new ProjectId($this->id);
}
public function name(): string
{
return $this->name;
}
public function description(): ?string
{
return $this->description;
}
public function ownerId(): UserId
{
return new UserId($this->ownerId);
}
public function memberIds(): array
{
return array_map(fn($id) => new UserId($id), $this->memberIds);
}
public function addMember(UserId $userId): void
{
if (!in_array($userId->value(), $this->memberIds)) {
$this->memberIds[] = $userId->value();
$this->updatedAt = new \DateTimeImmutable();
}
}
public function removeMember(UserId $userId): void
{
if ($userId->value() === $this->ownerId) {
throw new \DomainException('Cannot remove owner from project');
}
$this->memberIds = array_filter($this->memberIds, fn($id) => $id !== $userId->value());
$this->updatedAt = new \DateTimeImmutable();
}
public function updateName(string $name): void
{
$this->name = $name;
$this->updatedAt = new \DateTimeImmutable();
}
public function updateDescription(?string $description): void
{
$this->description = $description;
$this->updatedAt = new \DateTimeImmutable();
}
public function createdAt(): \DateTimeImmutable
{
return $this->createdAt;
}
public function updatedAt(): ?\DateTimeImmutable
{
return $this->updatedAt;
}
private function recordEvent(object $event): void
{
$this->events[] = $event;
}
public function releaseEvents(): array
{
$events = $this->events;
$this->events = [];
return $events;
}
}
Doménový model: Úkol (Aggregate Root)
<?php
namespace App\TaskManagement\Domain\Model;
use App\TaskManagement\Domain\Event\TaskCreated;
use App\TaskManagement\Domain\Event\TaskAssigned;
use App\TaskManagement\Domain\Event\TaskStatusChanged;
use App\TaskManagement\Domain\ValueObject\TaskId;
use App\TaskManagement\Domain\ValueObject\TaskStatus;
use App\ProjectManagement\Domain\ValueObject\ProjectId;
use App\UserManagement\Domain\ValueObject\UserId;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'tasks')]
class Task
{
public const STATUS_TODO = 'todo';
public const STATUS_IN_PROGRESS = 'in_progress';
public const STATUS_DONE = 'done';
#[ORM\Id]
#[ORM\Column(type: 'string', length: 36)]
private string $id;
#[ORM\Column(type: 'string', length: 255)]
private string $title;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $description;
#[ORM\Column(type: 'string', length: 36)]
private string $projectId;
#[ORM\Column(type: 'string', length: 36, nullable: true)]
private ?string $assigneeId = null;
#[ORM\Column(type: 'string', length: 20)]
private string $status;
#[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $createdAt;
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
private ?\DateTimeImmutable $updatedAt = null;
private array $events = [];
public function __construct(TaskId $id, string $title, ?string $description, ProjectId $projectId)
{
$this->id = $id->value();
$this->title = $title;
$this->description = $description;
$this->projectId = $projectId->value();
$this->status = self::STATUS_TODO;
$this->createdAt = new \DateTimeImmutable();
$this->recordEvent(new TaskCreated($id, $title, $projectId));
}
public function id(): TaskId
{
return new TaskId($this->id);
}
public function title(): string
{
return $this->title;
}
public function description(): ?string
{
return $this->description;
}
public function projectId(): ProjectId
{
return new ProjectId($this->projectId);
}
public function assigneeId(): ?UserId
{
return $this->assigneeId ? new UserId($this->assigneeId) : null;
}
public function status(): string
{
return $this->status;
}
public function assign(UserId $assigneeId): void
{
$this->assigneeId = $assigneeId->value();
$this->updatedAt = new \DateTimeImmutable();
$this->recordEvent(new TaskAssigned($this->id(), $assigneeId));
}
public function unassign(): void
{
$this->assigneeId = null;
$this->updatedAt = new \DateTimeImmutable();
}
public function changeStatus(string $status): void
{
if (!in_array($status, [self::STATUS_TODO, self::STATUS_IN_PROGRESS, self::STATUS_DONE])) {
throw new \InvalidArgumentException('Invalid status');
}
$oldStatus = $this->status;
$this->status = $status;
$this->updatedAt = new \DateTimeImmutable();
$this->recordEvent(new TaskStatusChanged($this->id(), $oldStatus, $status));
}
public function updateTitle(string $title): void
{
$this->title = $title;
$this->updatedAt = new \DateTimeImmutable();
}
public function updateDescription(?string $description): void
{
$this->description = $description;
$this->updatedAt = new \DateTimeImmutable();
}
public function createdAt(): \DateTimeImmutable
{
return $this->createdAt;
}
public function updatedAt(): ?\DateTimeImmutable
{
return $this->updatedAt;
}
private function recordEvent(object $event): void
{
$this->events[] = $event;
}
public function releaseEvents(): array
{
$events = $this->events;
$this->events = [];
return $events;
}
}
Command: Vytvoření projektu (Command Pattern)
<?php
namespace App\ProjectManagement\Application\Command;
use Symfony\Component\Validator\Constraints as Assert;
class CreateProject
{
public function __construct(
#[Assert\NotBlank]
#[Assert\Length(min: 3, max: 255)]
public readonly string $name,
public readonly ?string $description,
#[Assert\NotBlank]
#[Assert\Uuid]
public readonly string $ownerId
) {
}
}
Command Handler: Zpracování vytvoření projektu (Application Service)
<?php
namespace App\ProjectManagement\Application\Command;
use App\ProjectManagement\Domain\Model\Project;
use App\ProjectManagement\Domain\Repository\ProjectRepository;
use App\ProjectManagement\Domain\ValueObject\ProjectId;
use App\UserManagement\Domain\ValueObject\UserId;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
class CreateProjectHandler
{
public function __construct(
private ProjectRepository $projectRepository
) {
}
public function __invoke(CreateProject $command): string
{
$projectId = new ProjectId();
$project = new Project(
$projectId,
$command->name,
$command->description,
new UserId($command->ownerId)
);
$this->projectRepository->save($project);
return $projectId->value();
}
}
Command: Přiřazení úkolu (Command Pattern)
<?php
namespace App\TaskManagement\Application\Command;
use Symfony\Component\Validator\Constraints as Assert;
class AssignTask
{
public function __construct(
#[Assert\NotBlank]
#[Assert\Uuid]
public readonly string $taskId,
#[Assert\NotBlank]
#[Assert\Uuid]
public readonly string $assigneeId
) {
}
}
Command Handler: Zpracování přiřazení úkolu (Application Service)
<?php
namespace App\TaskManagement\Application\Command;
use App\TaskManagement\Domain\Repository\TaskRepository;
use App\TaskManagement\Domain\ValueObject\TaskId;
use App\TaskManagement\Domain\Service\TaskAssignmentService;
use App\ProjectManagement\Domain\Repository\ProjectRepository;
use App\UserManagement\Domain\ValueObject\UserId;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
class AssignTaskHandler
{
public function __construct(
private TaskRepository $taskRepository,
private ProjectRepository $projectRepository,
private TaskAssignmentService $taskAssignmentService
) {
}
public function __invoke(AssignTask $command): void
{
$task = $this->taskRepository->findById(new TaskId($command->taskId));
if (!$task) {
throw new \DomainException('Task not found');
}
$project = $this->projectRepository->findById($task->projectId());
if (!$project) {
throw new \DomainException('Project not found');
}
$assigneeId = new UserId($command->assigneeId);
// Použití doménové služby pro přiřazení úkolu
$this->taskAssignmentService->assignTask($task, $project, $assigneeId);
// Uložení úkolu
$this->taskRepository->save($task);
}
}
Query: Získání projektů uživatele (Query Pattern)
<?php
namespace App\ProjectManagement\Application\Query;
use Symfony\Component\Validator\Constraints as Assert;
class GetProjects
{
public function __construct(
#[Assert\NotBlank]
#[Assert\Uuid]
public readonly string $userId
) {
}
}
Query Handler: Zpracování získání projektů uživatele (Read Model)
<?php
namespace App\ProjectManagement\Application\Query;
use App\ProjectManagement\Domain\Repository\ProjectRepository;
use App\ProjectManagement\Presentation\ViewModel\ProjectViewModel;
use App\UserManagement\Domain\ValueObject\UserId;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
class GetProjectsHandler
{
public function __construct(
private ProjectRepository $projectRepository
) {
}
public function __invoke(GetProjects $query): array
{
$projects = $this->projectRepository->findByMemberId(new UserId($query->userId));
$result = [];
foreach ($projects as $project) {
$result[] = new ProjectViewModel(
$project->id()->value(),
$project->name(),
$project->description(),
$project->ownerId()->value(),
count($project->memberIds()),
$project->createdAt()
);
}
return $result;
}
}
Doménová služba: Přiřazení úkolu
<?php
namespace App\TaskManagement\Domain\Service;
use App\TaskManagement\Domain\Model\Task;
use App\ProjectManagement\Domain\Model\Project;
use App\UserManagement\Domain\ValueObject\UserId;
use App\Shared\Domain\Exception\DomainException;
class TaskAssignmentService
{
public function assignTask(Task $task, Project $project, UserId $assigneeId): void
{
// Kontrola, zda je přiřazovaný uživatel členem projektu
if (!$project->isMember($assigneeId)) {
throw new DomainException('Assignee is not a member of the project');
}
// Kontrola, zda úkol patří do projektu
if (!$task->projectId()->equals($project->id())) {
throw new DomainException('Task does not belong to the project');
}
// Přiřazení úkolu
$task->assign($assigneeId);
}
}
Anti-Corruption Layer: Integrace s externím systémem
<?php
namespace App\Integration\ExternalTaskSystem;
use App\TaskManagement\Domain\Model\Task;
use App\TaskManagement\Domain\ValueObject\TaskId;
use App\Integration\ExternalTaskSystem\Client\ExternalTaskClient;
use App\Integration\ExternalTaskSystem\Translator\TaskTranslator;
class ExternalTaskAdapter
{
private ExternalTaskClient $client;
private TaskTranslator $translator;
public function __construct(ExternalTaskClient $client, TaskTranslator $translator)
{
$this->client = $client;
$this->translator = $translator;
}
public function exportTask(Task $task): void
{
$externalTask = $this->translator->toExternalTask($task);
$this->client->createOrUpdateTask($externalTask);
}
public function importTask(string $externalTaskId): Task
{
$externalTask = $this->client->getTask($externalTaskId);
return $this->translator->fromExternalTask($externalTask);
}
}
Ponaučení
Implementace systému pro správu projektů pomocí Domain-Driven Design a CQRS v Symfony 7 přinesla několik důležitých ponaučení:
- Strategický design je klíčový - Identifikace bounded contexts a jejich vztahů na začátku projektu poskytla jasný rámec pro vývoj. Definice context map pomohla předejít nedorozuměním a zajistila konzistentní integraci mezi kontexty.
- Ubiquitous Language je základem úspěchu - Vytvoření a používání společného jazyka s doménovými experty výrazně zlepšilo komunikaci a vedlo k přesnějšímu modelu. Konzistentní používání tohoto jazyka v kódu usnadnilo pochopení a údržbu.
- Agregáty a hranice transakcí - Správné definování agregátů a jejich hranic bylo klíčové pro zajištění konzistence dat. Každý agregát byl zodpovědný za udržování své vnitřní konzistence a byl měněn v rámci jedné transakce.
-
Doménové události pro integraci - Doménové události byly klíčové pro komunikaci mezi různými bounded contexts. Například, když byl vytvořen nový úkol, byla vygenerována událost
TaskCreated
, která mohla být zpracována jinými kontexty bez vytváření přímých závislostí. - CQRS pro oddělení zodpovědností - Oddělení operací čtení a zápisu vedlo k čistšímu a udržitelnějšímu kódu. Příkazy a dotazy byly jasně odděleny, což usnadnilo testování a rozšiřování. Symfony Messenger poskytl výkonnou infrastrukturu pro implementaci CQRS.
- Vertikální slice architektura pro modularitu - Organizace kódu podle funkcí (features) místo technických vrstev vedla k lepší modularitě a udržitelnosti. Každá funkce obsahovala všechny vrstvy potřebné pro její implementaci, což usnadnilo změny a rozšíření.
- Testování doménového modelu - Důraz na testování doménového modelu vedl k robustnějšímu a spolehlivějšímu systému. Testy jednotek se zaměřovaly na chování agregátů a doménových služeb, zatímco integrační testy ověřovaly spolupráci mezi různými částmi systému.
Důležité poznámky pro implementaci DDD
Při implementaci Domain-Driven Design a CQRS v Symfony 7 je důležité:
- Začít strategickým designem - identifikovat bounded contexts a jejich vztahy před zahájením implementace.
- Vytvořit a používat Ubiquitous Language ve spolupráci s doménovými experty.
- Definovat jasné hranice mezi bounded contexts a implementovat vhodné integrační vzory (Shared Kernel, Customer-Supplier, Conformist, Anti-Corruption Layer).
- Správně identifikovat agregáty a jejich hranice, aby byla zajištěna konzistence dat.
- Používat hodnotové objekty pro validaci a enkapsulaci doménových konceptů.
- Implementovat doménové události pro komunikaci mezi bounded contexts.
- Oddělovat příkazy a dotazy podle CQRS principů.
- Používat Symfony Messenger pro implementaci command a query busů.
- Testovat doménový model nezávisle na infrastruktuře.
- Používat hexagonální architekturu pro oddělení domény od infrastruktury.
- Implementovat repozitáře jako rozhraní v doménové vrstvě a jejich konkrétní implementace v infrastrukturní vrstvě.
V další kapitole se podíváme na zdroje a další četbu o DDD a CQRS v Symfony 7.