Я уже довольно давно адаптирую CQRS 1 для бедного человека, потому что мне нравится его гибкость, позволяющая хранить детализированные данные в одном хранилище данных, предоставляя большие возможности для анализа и, таким образом, увеличивая ценность для бизнеса и, при необходимости, другую для операций чтения, содержащих денормализованные данные, для повышения производительности. ,
Но, к сожалению, с самого начала я боролся с проблемой, где именно я должен разместить бизнес-логику в архитектуре этого типа.
Из того, что я понимаю, команда является средством сообщения о намерениях и сама по себе не связана с доменом. Они в основном данные (тупые - если хотите) объекты передачи. Это позволяет легко переносить команды между различными технологиями. То же самое относится к событиям как ответы на успешно завершенные события.
В типичном приложении DDD бизнес-логика находится в сущностях, объектах значений, совокупных корнях, они богаты как данными, так и поведением. Но команда не является объектом домена, поэтому ее не следует ограничивать представлениями данных в домене, поскольку это создает для них слишком большую нагрузку.
Итак, реальный вопрос: где именно логика?
Я обнаружил, что чаще всего сталкиваюсь с этой борьбой, пытаясь построить довольно сложный агрегат, который устанавливает некоторые правила относительно комбинаций его значений. Кроме того, при моделировании доменных объектов мне нравится следовать парадигме отказоустойчивости , зная, что когда объект достигает метода, он находится в допустимом состоянии.
Допустим, агрегат Car
использует два компонента:
Transmission
,Engine
,
Как Transmission
и Engine
объекты значений представлены в виде супер типов и имеют в соответствии типа суб, Automatic
и Manual
передачи, или Petrol
и Electric
двигатели соответственно.
В этой области вполне успешно жить самостоятельно созданный Transmission
, будь то Automatic
или Manual
, или любой тип Engine
. Но Car
агрегат вводит несколько новых правил, применимых только тогда , когда Transmission
и Engine
объекты используются в том же контексте. А именно:
- Когда автомобиль использует
Electric
двигатель, единственным разрешенным типом трансмиссии являетсяAutomatic
. - Когда автомобиль использует
Petrol
двигатель, он может иметь любой типTransmission
.
Я мог бы уловить это нарушение комбинации компонентов на уровне создания команды, но, как я уже говорил, из того, что я понимаю, этого не следует делать, поскольку команда тогда будет содержать бизнес-логику, которая должна быть ограничена уровнем домена.
Один из вариантов - перенести проверку бизнес-логики на сам валидатор команд, но это тоже не совсем правильно. Такое чувство, что я буду деконструировать команду, проверять ее свойства, полученные с помощью геттеров, сравнивать их в валидаторе и проверять результаты. Это кричит как нарушение закона Деметры для меня.
Отказ от упомянутой опции проверки, потому что она не кажется жизнеспособной, кажется, что нужно использовать команду и построить агрегат из нее. Но где должна существовать эта логика? Должен ли он быть внутри обработчика команд, ответственного за обработку конкретной команды? Или это должно быть в валидаторе команд (мне тоже не нравится этот подход)?
В настоящее время я использую команду и создаю агрегат из нее в ответственном обработчике команд. Но когда я сделаю это, если у меня будет валидатор команды, он вообще ничего не будет содержать, потому что, если CreateCar
команда существует, она будет содержать компоненты, которые, как я знаю, действительны в отдельных случаях, но агрегат может отличаться.
Давайте представим другой сценарий, смешивающий разные процессы проверки - создание нового пользователя с помощью CreateUser
команды.
Команда содержит Id
пользователей, которые будут созданы, и их Email
.
Система устанавливает следующие правила для адреса электронной почты пользователя:
- Должно быть уникальным,
- не должно быть пустым,
- должно содержать не более 100 символов (максимальная длина столбца в БД).
В этом случае, хотя наличие уникального электронного письма является бизнес-правилом, проверка его в совокупности не имеет большого смысла, поскольку мне нужно было бы загрузить весь набор текущих электронных писем в системе в память и проверить электронную почту в команде. против совокупности ( Eeeek! Что-то, что-то, производительность.). Из-за этого я перенесу эту проверку в валидатор команды, который примет UserRepository
зависимость и использует репозиторий, чтобы проверить, существует ли уже пользователь с адресом электронной почты, присутствующим в команде.
Когда дело доходит до этого, внезапно имеет смысл поместить и другие два правила электронной почты в валидатор команд. Но я чувствую, что правила должны действительно присутствовать в User
агрегате, и что валидатор команд должен проверять только уникальность, и если проверка прошла успешно, я должен приступить к созданию User
агрегата в CreateUserCommandHandler
и передать его в репозиторий для сохранения.
Я чувствую себя так, потому что метод сохранения репозитория, скорее всего, примет агрегат, который гарантирует, что после прохождения агрегата все инварианты будут выполнены. Когда логика (например непустота) присутствует только внутри самой команды проверки другой программист может полностью пропустить эту проверку и вызовите метод сохранения в UserRepository
с User
объектом непосредственно , что может привести к фатальной ошибке базы данных, так как электронная почта может иметь было слишком долго
Как вы лично обрабатываете эти сложные проверки и преобразования? Я в основном доволен своим решением, но мне кажется, что мне нужно подтверждение, что мои идеи и подходы не совсем глупы, чтобы быть вполне довольными выбором. Я полностью открыт для совершенно разных подходов. Если у вас есть что-то, что вы лично попробовали и работали очень хорошо для вас, я хотел бы увидеть ваше решение.
1 Работая в качестве разработчика PHP, отвечающего за создание систем RESTful, моя интерпретация CQRS немного отличается от стандартного подхода обработки асинхронных команд , например, иногда возвращает результаты от команд из-за необходимости синхронной обработки команд.
CommandDispatcher
.Ответы:
Следующий ответ относится к стилю CQRS, поддерживаемому cqrs.nu, в котором команды поступают непосредственно в агрегаты. В этом архитектурном стиле службы приложений заменяются компонентом инфраструктуры ( CommandDispatcher ), который идентифицирует агрегат, загружает его, отправляет команду и затем сохраняет агрегат (как последовательность событий, если используется источник событий).
Существует несколько видов логики (проверки). Основная идея состоит в том, чтобы выполнить логику как можно раньше - быстро провалиться, если хотите. Итак, ситуации следующие:
isValid
метода, но он кажется мне бессмысленным, так как кто-то должен был бы не забыть вызывать этот метод, когда на самом деле должно быть достаточно успешной реализации команды.command validators
классы, которые несут ответственность за проверку команды. Я использую этот вид проверки, когда мне нужно проверить информацию из нескольких агрегатов или внешних источников. Вы можете использовать это, чтобы проверить уникальность имени пользователя.Command validators
Могут быть введены любые зависимости, такие как репозитории. Имейте в виду, что эта проверка в конечном итоге согласуется с совокупностью (то есть, когда пользователь создается, тем временем может быть создан другой пользователь с тем же именем пользователя)! Кроме того, не пытайтесь поместить здесь логику, которая должна находиться внутри совокупности! Валидаторы команд отличаются от менеджеров Sagas / Process, которые генерируют команды на основе событий.When a car uses Electric engine the only allowed transmission type is Automatic
должно быть проверено здесь.Используя вышеупомянутые методы, никто не может создавать недопустимые команды или обходить логику внутри агрегатов. Валидаторы команд автоматически загружаются + вызываются,
CommandDispatcher
поэтому никто не может отправить команду напрямую в агрегат. Можно было бы вызвать метод для агрегата, передавшего команду, но не смог сохранить изменения, поэтому это было бы бессмысленно / безвредно.Я также программист PHP, и я ничего не возвращаю из своих обработчиков команд (агрегатные методы в форме
handleSomeCommand
). Однако я довольно часто возвращаю информацию клиенту / браузеру в видеHTTP response
, например, идентификатора вновь созданного агрегатного корня или чего-либо из модели чтения, но я никогда не возвращаю (на самом деле никогда ) что-либо из моих агрегатных командных методов. Достаточно простого факта, что команда была принята (и обработана - мы говорим о синхронной обработке PHP, верно ?!).Мы возвращаем что-то в браузер (и все еще делаем CQRS книгой), потому что CQRS не является архитектурой высокого уровня .
Пример работы валидаторов команд:
источник
EmailAddress
объект значения, который сам себя проверяет.EmailAddress
в капсулу , чтобы уменьшить дублирование. Что еще более важно, при этом вы также будете перемещать логику из своей команды в свою область. Стоит отметить, что это может быть слишком далеко. Часто похожие знания (объекты стоимости) могут иметь разные требования к валидации в зависимости от того, кто их использует.EmailAddress
Это удобный пример, потому что вся концепция этого значения имеет глобальные требования проверки.UserCanPlaceOrdersOnlyIfHeIsNotLockedValidator
. Вы можете видеть, что это отдельный домен для Order, поэтому он не может быть проверен самим OrderAggregate.Одной из фундаментальных предпосылок DDD является то, что модели предметной области сами себя проверяют. Это очень важная концепция, поскольку она повышает ваш домен как ответственную сторону за обеспечение соблюдения ваших бизнес-правил. Это также держит вашу модель предметной области как фокус для развития.
Система CQRS (как вы правильно указали) - это деталь реализации, представляющая общий поддомен, который реализует свой собственный связный механизм. Ваша модель никоим образом не должна зависеть от какой-либо части инфраструктуры CQRS, которая будет вести себя в соответствии с вашими бизнес-правилами. Цель DDD - смоделировать поведение системы таким образом, чтобы в результате была получена полезная абстракция функциональных требований вашей основной бизнес-области. Удаление какой-либо части этого поведения из вашей модели, как бы это ни было заманчиво, снижает целостность и сплоченность вашей модели (и делает ее менее полезной).
Просто расширив ваш пример включением
ChangeEmail
команды, мы можем прекрасно проиллюстрировать, почему вам не нужна какая-либо бизнес-логика в командной инфраструктуре, поскольку вам нужно будет дублировать ваши правила:Итак, теперь, когда мы можем быть уверены, что наша логика должна быть в нашей области, давайте рассмотрим вопрос «где». Первые два правила могут быть легко применены к нашей
User
совокупности, но это последнее правило немного более нюансировано; тот, который требует некоторых дополнительных знаний хруста, чтобы получить более глубокое понимание. На первый взгляд может показаться, что это правило относится к aUser
, но на самом деле это не так. «Уникальность» электронного письма относится к коллекцииUsers
(согласно некоторой области).Ах, ха! Имея это в виду, становится совершенно ясно, что ваша
UserRepository
(ваша коллекция в памятиUsers
) может быть лучшим кандидатом для применения этого инварианта. Метод «save», вероятно, является наиболее разумным местом для включения проверки (где вы можете генерироватьUserEmailAlreadyExists
исключение). Кроме того, доменUserService
может быть привлечен к ответственности за создание новыхUsers
и обновление их атрибутов.Быстрый отказ - это хороший подход, но он может быть реализован только в том случае, когда и когда он соответствует остальной части модели. Может быть очень заманчиво проверить параметры метода (или команды) службы приложения перед дальнейшей обработкой, чтобы попытаться перехватить сбои, когда вы (разработчик) знаете, что вызов потерпит неудачу где-то в глубине процесса. Но при этом вы будете иметь дублированные (и просочившиеся) знания таким образом, что при изменении бизнес-правил, вероятно, потребуется более одного обновления кода.
источник