Задать вопрос
  • Как подружить REST API и концепцию DDD?

    @vova07
    Приветствую!

    Попробую как можно короче объяснить суть и упущенные моменты которые наблюдаются в вашем примере.

    Ответ 1:
    Прочитайте про так называемые доменные сервисы (Domain Services) и приложенческие сервисы (Application Services). В других статьях ответ на ваш вопрос люди называют Use Cases.
    Юз кйэс (Use Case) - это отдельный сервис который не нарушает stateless и у которого одна конкретная задача.

    Я напишу все примеры на PHP (это самый простой язык который вероятнее всего знаете и вы - извиняюсь за это но думаю лучше Go, Swift, Haskell для этого случая):

    final class CreateCustomer
    {
    	/**
    	 * @var IWriteRepository Интерфейс репозитория, где на самом деле хранистя конкретная имплементация записи сущности.
    	 */
    	private $repository;
    
    	public function __construct(IWriteRepository $repositorym Validator $validator)
    	{
    		$this->repository = $repository;
    		...
    	}
    
    	/**
    	 * @param string $id ИД будущей записи.
    	 * @param array $data ПОТ данные которые мы получаем в момент АПи запроса.
    	 */
    	public function handle(string $id, array $data) : void
    	{
    		/**
    		 * Валидация выбрасывает исключение если данные не валидны или возвращает массив валидных данных.
    		 */
    		$dto = $this->validator->validate($data);
    
    		$customer = new CustomerEntity(new UUID($id));
    		/* Ентити это сущность которое содержить исключительно только бизнес логику и безнесс поведения. 
    		 * В вашем примере у вас были разные сеттеры, в правильном подходе это лишенно смысла,
    		 * и совсем неправильно. Думайте об методах аггрегата (Сушность с которой работает репозиторий это ни * что инное как аггрегат) как проекция бизесс логики. 
    		 * для примера в реальном мире принято говорить `Новый клиент зарегистрировался в системе`, мы никогда * не перечисляем цепь проделанных событий. Кто-то говорит `Клиент заполнил свой аддрес, ФИО, потом
    		 * телефон, и отправил данные` ?!
    		 * 
    		 * Если вы заметили правильно, то в конструкторе сущности передается только идентификатор.
    		 * Такой подход похож на реальную ситуацию, что позволяет легко проектировать бизнес логику.
    		 * регистрация клиента можно сравнить с регистрацией карточки пользователя в магазине.
    		 * - Берём бумагу (или специальный блокнот)
    		 * - Указываем номер клиента (обычно его как-то генерят например дата, время, или номер карточки 
    		 * которую ему выдают)
    		 * - начинаем спрашивать клиента кго персональные данные и заполняем все в ОДНОМ процессе.
    		 * - Потом кидаем блокнот/бумагу оратно или в колецию где лежит другая похожая инфа.
    		 */
    		$customer->register($dto['name'], ...);
    
    		/**
    		 * Именно этот метод делает сохранение нового клиента.
    		 * Сама реализация интереса живет в персистентном слое (`persistence layer`).
    		 */
    		$this->repository->store($customer);
    	}
    }


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

    Примечание: Если обратите внимание то юз кйэс в своем методе "handle" принимает только скалярные данные, это важный момент который позволяет писать реюзабельные юз кйэсы. Самый простой пример это веб АПИ и консольная команда которая вводится в терминале.
    Валидация данных делается в том же классе (это не отменяет доменную валидацию но показывает один из хороших мест это можно делать в слое приложения.)
    Другое примечание сводится к тому что очень важно соблюдать простое правило: (SRP) - один юз кэйс одна операция.

    Ответ на вопрос который вероятнее всего будет: Как быть если нам надо создать клиента и вернуть ответ, то есть в обычной REST ситуации ?

    final class PostController extend Controller
    {
    	public function __construct(IWriteRepository, CreateCustomer $createCustomUseCase, RetrieveCustomer $retrieveCustomerUseCase)
    	{
    		...
    	}
    
    	public function __invoke(array $data)
    	{
    		$id = $this->repository->pickNextId();
    		
    		$this->createCustomUseCase->handle($id, $data);
    
    		return $this->retrieveCustomerUseCase->handle($id);
    	}
    }


    Примечание: В реальном мире есть два типа репозиторием, один который записывает данные и делает все манипуляции с данными - WriteRepository и второй Тольятти для чтения который используется во всех других юз кйэсах где нужно только выбирать данные - ReadRepository

    Ответ 2:
    REST API - не подразумевает то что вы описываете.
    В REST идеологии на запрос клиента надо вернуть тот же ответ. То есть сокращенно это подразумевает получение целого ресурса (ресурс - так называемая схема запроса/ответа) и возврат его же. (Ответ может содержать дополнительные данные но они вспомогательные, такие как хедеры, пагинация, и так далее.)

    Ответ 3:
    Все что не является бизнес логикой надо старится вынести из сущности.
    Сущность это состояние безнос процесса.
    Если некие атрибуты не имеют смысла для бизнеса, то их надо избегать.

    Если дадите конкретные примеры таких атрибутов, чтоы я мог понять то я смогу более детально написать ответ по их решению, по другому мне сложно это выразить.

    Если там входит логи, временные тэги, дополнительные данные для других последующих операций, то сущность спроектирована неправильно.

    В одной сущности (Entity) могут быть только атрибуты для удовлетворения поведений этой сущности, все остальное это не часть сущности.
    Сеттеры это анти-паттерн который надо всегда избегать.

    Проектируете сущности так чтобы присутствовали только поведения бизнеса. В реальном коде это методы которые проектируют одно поведение. В итоге мы получаем конструктор, и много методов которые следуют строго бизнесс процессам, сторонние сеттеры признак проблемной архитектуры.

    Итог:
    разные сущности могут содержать в себе (инкапсулировать) разные Value Objects разделение классов по строгим типам очень важно. Если есть цикл жизни и идентификатор то это сущность, если нету цикла то это ВО (VO = Value Object).

    Ваш CustomerState не имеет цикла жизни что сразу ограничивает вас в использовании сущности. Это сирого ВО. А если в логике появляются разные "if", "else" (лично мое мнение которое разделяют другие разработчики) то это признак плохой архитектуры, и повод задуматься над разделением Во на два или на подтипы. "VipState", "RegularState" но это больше правда для ситуаций с количеством от Ю 2 значений, если их два, то имеет место сделать белковый флаг который будет указывать на то или другое значение.

    П.С: Чтобы облегчить вашу жизнь в будущем, попробуйте писать код в котором никогда не будет дефолтных значений в методах, это поменяет ваше восприятие кода, и поможет писать лучше.

    П.С.С: Все выше сказанное это личные наблюдения, и персональная интерпретация теории. Вы МОЖЕТЕ учитывать мои ИДЕИ но НЕ НАДО СЛЕДОВАТЬ моим интерпретациям.
    ДДД это принцип мышления, еще никто в мире не смог доказать и описать 100% правильный подход, который бы должен быть быть эталоном, даже автор мышления.
    Пробуйте, экспериментируйте и делитесь опытом!

    Удачи!
    Ответ написан
    Комментировать