Строительные блоки DDD

Проектирование на основе предметной области принято делить на две части — стратегическое и тактическое. Хотя они идут бок о бок и одна без другой не существует, так же как и не бывает одной стороны монеты без другой. В данной статье мы вкратце рассмотрим основные строительные блоки DDD с упором на практическую (тактическую) часть.

Предметная область

Это самое важное понятие вбирающее в себя все процессы над которыми работает команда. Например, если вы пишите систему учета сотрудников для какой-либо компании, то это ваша предметная область. В процессе проектирования предметную область обычно делят на несколько частей (подобластей).

Единый язык

Для наиболее удачного и точного описания предметной области необходимо разработать «язык» (набор терминов). Это делается совместными усилиями разработчиков ПО и доменными экспертами. Данные термины должны быть хорошо понятны как сотрудникам компании (доменным экспертам), так и команде разработки. Выработка единого языка помогает уменьшить недопонимание между доменными экспертами и разработчиками ПО при обсуждении функционала, да и именовать сущности и агрегаты системы становится на порядок проще.

Контекст

Контекст — это рамки в которых существует и предметная область и единый язык, причем в одном проекте может быть несколько контекстов. Некоторые понятия единого языка могут обозначать разные вещи в различных частях (подобластях) предметной области. К примеру, сущность «Клиент» в модуле магазина обозначает покупателя — частное лицо, а в модуле юридического отдела этой же организации «Клиент» обозначает оптовую компанию заказчика. Разные подобласти формируют разные контексты.

Сущность

Сущность (или Entity) — это объект из предметной области и по совместительству главная строительная единица DDD. Важным маркером сущности является потребность в идентификации, т.е. при его создании необходимо присвоить ему уникальный идентификатор. Обычно это первичный ключ из таблицы БД или UUID. Наглядным примером сущности является пользователь, хотя в некоторых случаях пользователи становятся агрегатами речь о которых пойдет дальше.

Пример:

<?php
 
final class User
{
    private string $id;
    private string $name;
    private string $phone;
    private string $address;
 
    public function __construct(string $id, string $name, string $phone, string $address)
    {
        if (empty($id)) {
            throw new DomainException('An empty id when creating an object.');
        }
 
        $this->id = $id;
 
        // Валидация и использование оставшихся полей...
    }
}
 
$user = new User(
    'ABCDE0123456789',
    'Ivan',
    '79991234567',
    'Россия, Москва, Тестовая улица, д. 123'
);

Объекты-значения

Объекты-значения (или Value Object) — в большинстве случаев это строительные блоки для сущностей. Но они отличаются от сущностей отсутствием идентификаторов и стабильностью содержимого на все время жизни в программе.

Пример ввода объекта-значения в сущность:

<?php
 
final class Address
{
    private string $postcode;
    private string $country;
    private string $city;
    private string $street;
    private string $building;
    private string $room;
 
    public function __construct(
        string $postcode,
        string $country,
        string $city,
        string $street,
        string $building,
        string $room = '',
    ) {
        if (strlen(preg_replace('/[^0-9]/', '', $postcode)) != 6) {
            throw new DomainException('Error postcode format.');
        }
 
        $this->postcode = $postcode;
 
        // Валидация и использование оставшихся полей...
    }
}
 
final class User
{
    private string $id;
    private string $name;
    private string $phone;
    private Address $address;
 
    public function __construct(string $id, string $name, string $phone, Address $address)
    {
        if (empty($id)) {
            throw new DomainException('An empty id when creating an object.');
        }
 
        $this->id = $id;
 
        // Валидация и использование оставшихся полей...
    }
}
 
$user = new User(
    'ABCDE0123456789',
    'Ivan',
    '79991234567',
    new Address('123456', 'РФ', 'Москва', 'Тестовая', '132')
);

Агрегаты

Агрегаты (или Aggregate) — это сложные объекты, включающие в себя сущности, объекты-значения и даже другие агрегаты. Данная конструкция необходима для реализации компонентов доменной модели и соблюдения бизнес-логики. Наглядным примером агрегата может быть счет на оплату, который включает в себя несколько товаров (сущностей). Агрегат «Счет» будет отвечать за список товаров, сумму оплаты и другие бизнес-правила, например, скидки и акции.

Агрегаты, сущности и объекты-значения желательно создавать через фабрики или фабричные методы. Особенно это касается сложных объектов, так как при их создании через конструктор приходится показывать значительную часть внутренней структуры, а это уже явное нарушение инкапсуляции, что нежелательно, но если сильно хочется, то можно 🙂

<?php
 
final class User
{
    private string $id;
    private string $name;
    private array $projects;
 
    // ...
 
    public function __construct(string $id, string $name)
    {
        $this->projects = [];
 
        if (empty($id)) {
            throw new DomainException('An empty id when creating an object.');
        }
 
        $this->id = $id;
 
        if (empty($name)) {
            throw new DomainException('An empty name when creating an object.');
        }
 
        $this->name = $name;
    }
 
    public function addProject(Project $project)
    {
        array_push($this->projects, $project);
    }
 
    // ...
}
 
final class Project
{
    private int $id;
    private string $name;
 
    // ...
 
    public function __construct(int $id, string $name)
    {
        // Валидация.
 
        $this->id = $id;
        $this->name = $name;
        // ...
    }
}
 
$user = new User('ABCDE0123456789', 'Ivan Ivanov');
$user->addProject(new Project(1, 'Проект комнаты'));
$user->addProject(new Project(2, 'Проект дома'));
$user->addProject(new Project(3, 'Проект города'));

Хранилище

Хранилище (Repository) — это механизм хранения объектов. При реализации подходов DDD хранилища применяются для работы с агрегатами. Описания (интерфейсы) репозиториев находятся в домене и не предоставляют реализации.

При введении хранилищ придется создать объекты-гидраторы для преобразования данных из конечного хранилища (например, БД или API) в объекты домена и обратно.

Пример интерфейса репозитория:

interface UserRepositoryInterface
{
    public function findByID(string $id): User;
    public function findByName(string $name): User;
 
    public function add(User $user): bool;
    public function update(User $user): bool;
    public function remove(string $id): bool;
}

Пример реализации репозитория для работы с MySQL:

<?php
 
final class User
{
    // ...
}
 
interface UserRepositoryInterface
{
    public function findByID(string $id): User;
    public function findByName(string $name): User;
 
    public function add(User $user): bool;
    public function update(User $user): bool;
    public function remove(string $id): bool;
}
 
final class MySqlUserRepository implements UserRepositoryInterface
{
    public function findByID(string $id): User { /** Поиск пользователя по id. **/ }
    public function findByName(string $name): User { /** Поиск пользователя по имени. **/ }
    public function add(User $user): bool { /** Добавление пользователя. **/ }
    public function update(User $user): bool { /** Обновление пользователя. **/ }
    public function remove(string $id): bool { /** Удаление пользователя. **/ }
}

При создании, обновлении и удалении записи есть важный момент — эти операции должны быть атомарными, т.е. эти операции либо выполняются «как нужно», либо не выполняются вообще. Это реализуется путем использования транзакций.

Сервисы

Сервисы (Service) — это объекты хранящие ту бизнес-логику, которую нельзя вместить ни в агрегаты, ни в сущности. Например, если в магазин приходит заказ, то необходимо выполнить две операции, создать пользователя (его аккаунт с данными) и создать заказ (корневая запись и список сущностей товаров). Создание пользователя нежелательно передавать агрегату заказа, а создание заказа нежелательно передавать агрегату пользователя, поэтому данная бизнес-логика выносится в отдельный класс-сервис.

Данный класс не должен иметь состояния, он реализует часть общей бизнес-логики и все. Для сервисов, так же как и для репозиториев, желательно писать интерфейсы в доменном слое, а реализации создавать в инфраструктурном.

Абстрактный пример:

<?php
 
final class Order { /* ... */ }
interface UserRepository { /* ... */ }
interface OrderRepository { /* ... */ }
 
interface OrderServiceInterface
{
    public function create(string $userID): void;
    public function addItem(string $userID, string $itemID): void;
    public function removeItem(string $userID, string $itemID): void;
    public function reject(string $orderID): void;
    public function complete(string $orderID): void;
    // ...
}
 
final class OrderService implements OrderServiceInterface
{
    private UserRepository $userRepository;
    private OrderRepository $orderRepository;
 
    public function __construct(UserRepository $userRepository, OrderRepository $orderRepository)
    {
        $this->userRepository = $userRepository;
        $this->orderRepository = $orderRepository;
    }
 
    public function create(string $userID): void
    {
        $user = $this->userRepository->findByID($userID);
 
        $order = new Order('ABCDE0123456789', $user);
 
        $this->orderRepository->create($order);
    }
 
    public function addItem(string $userID, string $itemID): void {  /* ... */  }
    public function removeItem(string $userID, string $itemID): void {  /* ... */  }
    public function reject(string $orderID): void {  /* ... */  }
    public function complete(string $orderID): void {  /* ... */  }
}

Хорошей практикой при написании сервисов является использование исключений для обработки ошибок.

Объекты для передачи данных

При четком разделении приложения на слои возникает необходимость для передачи данных между ними, например, от пользователя (браузер, API, консоль и еще что-то) в домен. Одним из самых популярных решений является специальный объект — DTO (data transfer object).

<?php
 
final class AddressDTO
{
    public string $postcode;
    public string $country;
    public string $city;
    public string $street;
    public string $building;
    public string $room;
 
    public static function createFromPOST(array $POST): self
    {
        $dto = new self();
 
        // Валидация...
 
        $dto->postcode = $POST['postcode'];
        $dto->country = $POST['country'];
        $dto->city = $POST['city'];
        $dto->street = $POST['street'];
        $dto->building = $POST['building'];
        $dto->room = $POST['room'];
 
        return $dto;
    }
}

Добавить комментарий