Я начал играть с DDD недавно. Сегодня у меня проблема с размещением логики проверки в моем приложении. Я не уверен, какой слой мне взять. Я искал по интернету и не могу найти единого решения, которое решает мою проблему.
Давайте рассмотрим следующий пример. Сущность пользователя представлена такими объектами-значениями, как идентификатор (UUID), возраст и адрес электронной почты.
final class User
{
/**
* @var \UserId
*/
private $userId;
/**
* @var \DateTimeImmutable
*/
private $dateOfBirth;
/**
* @var \EmailAddress
*/
private $emailAddress;/**
* User constructor.
* @param UserId $userId
* @param DateTimeImmutable $dateOfBirth
* @param EmailAddress $emailAddress
*/
public function __construct(UserId $userId, DateTimeImmutable $dateOfBirth, EmailAddress $emailAddress)
{
$this->userId = $userId;
$this->dateOfBirth = $dateOfBirth;
$this->emailAddress = $emailAddress;
}
}
Проверка, не связанная с бизнес-логикой, выполняется ValueObjects. И это нормально.
У меня проблемы с размещением проверки правил бизнес-логики.
Что если, скажем, нам нужно разрешить пользователям иметь свой собственный адрес электронной почты, только если им больше 18 лет?
Мы должны были бы проверить возраст на сегодня и выдать исключение, если оно не в порядке.
Где я должен положить это?
Где разместить валидаторы, отвечающие за проверку данных в хранилище?
Как уникальность электронной почты. Я читал о спецификации. Это нормально, если я использую это непосредственно в Command Handler?
И последнее но не менее важное.
Как интегрировать это с проверкой пользовательского интерфейса?
Все, что я описал выше, касается проверки на уровне домена. Но давайте рассмотрим выполнение команд из обработчика REST-сервера. Мой клиент REST API ожидает, что я верну полную информацию о том, что пошло не так в случае ошибок ввода данных. Я хотел бы вернуть список полей с описанием ошибки.
Я могу фактически обернуть всю подготовку команды в блок try, чтобы прослушать исключения типа проверки, но главная проблема заключается в том, что это даст мне информацию о единственная ошибка, до первого исключения.
Означает ли это, что я должен дублировать мою логику проверки на уровне контроллера (т.е. с помощью zend-inputfilter — я использую ZF2 / 3)? Звучит незаметно …
Заранее спасибо.
Я постараюсь ответить на ваши вопросы один за другим, а также дать мои два цента здесь и там и как я решит проблемы.
Проверка, не связанная с бизнес-логикой, выполняется ValueObjects
Фактически ValueObjects представляют концепции из вашей бизнес-области, поэтому эти проверки также являются проверками бизнес-логики.
Entity — проверить это при создании объекта User, в конструкторе?
Да, по моему мнению, вы должны попытаться добавить такое поведение как можно глубже в Агрегаты. Если вы помещаете его в команды или обработчики команд, вы теряете связность, и бизнес-логика просачивается на прикладной уровень. И я бы даже пошел дальше. Задайте себе вопрос, есть ли в вашей модели скрытые понятия, которые не указаны явно. В вашем случае это AdultUser
и UnderagedUser
(Oни мог оба реализуют UserInterface
) это на самом деле иметь другое поведение. В этих случаях я всегда стараюсь моделировать это явно.
Как уникальность электронной почты. Я читал о спецификации. Это нормально, если я использую это непосредственно в Command Handler?
Шаблон спецификации хорош, если вы хотите иметь возможность комбинировать сложные запросы с логическими операторами (особенно для модели чтения). В твоем случае я считаю это излишним. Добавление простого containsUserForEmail($emailValueObject)
метод в UserRepositoryInterface
и вызвать это из варианта использования в порядке.
<?php
$userRepository
->containsUserForEmail($emailValueObject)
->hasOrThrow(new EmailIsAlreadyRegistered($emailValueObject));
Как интегрировать это с проверкой пользовательского интерфейса?
Поэтому, прежде всего, уже должна быть проверка на стороне клиента для рассматриваемых полей. Облегчите правильное использование вашей системы и затрудните ее неправильное использование.
Конечно, все еще должна быть проверка на стороне сервера. В настоящее время мы используем подход проверки схемы, когда у нас есть центральный реестр схем, из которого мы выбираем схему для заданной полезной нагрузки, а затем можем проверять полезную нагрузку JSON по этой схеме JSON. Если это не удается, мы возвращаем сериализованный ValidationErrors
объект. Мы также сообщаем клиенту через Content-Type: application/json; profile=https://some.schema.url/v1/user#
заголовок, как он может построить действительную полезную нагрузку.
Вы можете найти несколько хороших статей о том, как создать RESTful API поверх архитектуры CQRS. Вот а также Вот.
Просто для того, чтобы рассказать о том, что сказал tPl0ch, поскольку я нашел это полезным … Хотя я не был в стеке PHP много лет, во всяком случае, это во многом теоретическое обсуждение.
Одной из более серьезных проблем, с которыми сталкиваются при практическом применении DDD, является проблема валидации. Традиционная логика диктует, что валидация должна жить где-то там, где она действительно должна жить везде. Что, вероятно, сбило людей с толку больше, чем что-либо еще, при применении этого к DDD — это качества домена, никогда не находящегося «в недопустимом состоянии». CQRS далеко ушла, чтобы решить эту проблему, и вы используете команды.
Лично я делаю это так, что команды — единственный способ изменить состояние. Даже если мне потребуется создание доменной службы для сложной операции, именно команды будут выполнять эту работу. Традиционный обработчик команд отправит команду против агрегата и переведет агрегат в переходное состояние. Все это довольно стандартно, но я дополнительно делегирую ответственность за проверку перехода к самим командам, поскольку они уже охватывают и бизнес-логику. Например, если я создаю новую учетную запись и мне требуются имя, фамилия и адрес электронной почты, я должен проверить ее как присутствующую в команде, прежде чем пытаться применить ее к совокупности посредством обработчик команд. Таким образом, каждый из моих обработчиков команд знает не только о команде, но и о валидаторе команды.
Этот валидатор гарантирует, что состояние команды не скомпрометирует домен, что позволяет мне проверять саму команду, и в тот момент, когда я не несу дополнительных затрат, связанных с необходимостью проверки где-либо в инфраструктуре или реализации. Поскольку единственный способ изменить состояние — использовать только команды, я не помещаю эту логику непосредственно в сами объекты домена. Это не означает, что модель предметной области анемична, на самом деле далеко не так. Существует предположение, что если вы не проходите валидацию в самих объектах домена, то домен сразу становится анемичным. Но агрегат должен предоставить средства для установки этих значений — обычно с помощью метода — и команда переводится для предоставления этих значений этому методу. Один из общих подходов, которые вы видите, заключается в том, что логика помещается в установщики свойств, но поскольку вы устанавливаете только одно свойство за раз, вы могли бы легче оставить агрегат в недопустимом состоянии. Если вы посмотрите на команду как проверенную с целью преобразования этого состояния в одну операцию, вы увидите, что команда является логическим расширением совокупности (и с организационной точки зрения кода живет очень близко, если не под агрегатный).
Поскольку я имею дело только с проверкой команд на этом этапе, у меня, как правило, также будет проверка постоянства. По сути, непосредственно перед сохранением агрегата все состояние агрегата будет проверено сразу. Конечная цель — получить команду для сохранения, так что у меня будет один валидатор персистентности на агрегат, но столько же командных валидаторов, сколько у меня есть команд. Этот единственный средство проверки персистентности обеспечит безошибочную проверку того, что команда не видоизменила агрегат таким образом, который нарушает общие проблемы домена. Он также будет осознавать, что один агрегат может иметь несколько допустимых переходных состояний, что непросто поймать в команде. Под несколькими состояниями я подразумеваю, что агрегат может быть действительным для персистентности как «вставка» для персистентности, но, возможно, недопустим для операции «обновления». Самым простым примером этого было бы то, что я не мог обновить или удалить агрегат, который не был сохранен.
Все это может быть отображено в пользовательском интерфейсе в моей собственной реализации. Пользовательский интерфейс передаст данные службе приложения, служба приложения создаст команду и вызовет метод «Проверка» в моем обработчике, который будет возвращать любые ошибки проверки в команде без ее выполнения. Если присутствуют ошибки проверки, прикладная служба может уступить контроллеру, возвращая любые найденные ошибки проверки и позволить им всплыть. Кроме того, предварительно отправив данные, можно отправить их, пройти по тому же пути для проверки и вернуть эти ошибки проверки без физической отправки данных. Это лучшее из обоих миров. Нарушения команд могут происходить часто, если пользователь вводит неверные данные. С другой стороны, постоянные нарушения должны происходить редко, если вообще когда-либо, за пределами тестирования. Это может означать, что команда изменяет состояние так, что домен не поддерживается.
Наконец, после проверки команды служба приложения может выполнить ее. Способ, которым я построил свою собственную инфраструктуру, заключается в том, что обработчик команд знает, была ли команда проверена непосредственно перед выполнением. Если это не так, обработчик команды выполнит ту же проверку, которая предоставляется методом «Validate». Разница, однако, заключается в том, что это будет рассматриваться как исключение. Цель на этом этапе — остановить выполнение, поскольку недопустимая команда не может войти в домен.
Хотя примеры написаны на Java (опять же, это не моя предпочтительная платформа), я настоятельно рекомендую Вону Вернону «Внедрение доменного дизайна». Это действительно объединяет многие концепции в материале Эванса и достижения в парадигме DDD, такие как CQRS + ES. По крайней мере, для меня материал в книге Вернона, которая также является частью серии книг «DDD», изменил способ, которым я фундаментально подхожу к DDD, так же, как «Голубая книга» познакомила меня с ним.