Как именно команда CQRS должна быть проверена и преобразована в объект домена?

22

Я уже довольно давно адаптирую 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 немного отличается от стандартного подхода обработки асинхронных команд , например, иногда возвращает результаты от команд из-за необходимости синхронной обработки команд.

Энди
источник
нужен пример кода, я думаю. как выглядят ваши командные объекты и где вы их создаете?
Эван
@Ewan Я добавлю примеры кода позже сегодня или завтра. Отъезд в поездку через несколько минут.
Энди
Будучи программистом PHP, я предлагаю взглянуть на мою реализацию CQRS + ES: github.com/xprt64/cqrs-es
Константин Гальбену,
@ConstantinGALBENU Если мы сочтем, что интерпретация CQRS Грега Янга верна (что нам, вероятно, следует), то ваше понимание CQRS неверно - или, по крайней мере, ваша реализация PHP. Команды не должны обрабатываться агрегатами напрямую. Команды должны обрабатываться обработчиками команд, которые могут вызывать изменения в агрегатах, которые затем генерируют события, которые будут использоваться для репликаций состояний.
Энди
Я не думаю, что наши интерпретации разные. Вам просто нужно больше копаться в DDD (на тактическом уровне агрегатов) или шире открывать глаза. Существует как минимум два стиля реализации CQRS. Я использую один из них. Моя реализация больше похожа на модель Actor и делает слой Application очень тонким, что всегда хорошо. Я заметил, что внутри этих сервисов приложений много дублирования кода, и решил заменить их на a CommandDispatcher.
Константин Гальбену

Ответы:

22

Следующий ответ относится к стилю CQRS, поддерживаемому cqrs.nu, в котором команды поступают непосредственно в агрегаты. В этом архитектурном стиле службы приложений заменяются компонентом инфраструктуры ( CommandDispatcher ), который идентифицирует агрегат, загружает его, отправляет команду и затем сохраняет агрегат (как последовательность событий, если используется источник событий).

Итак, реальный вопрос: где именно логика?

Существует несколько видов логики (проверки). Основная идея состоит в том, чтобы выполнить логику как можно раньше - быстро провалиться, если хотите. Итак, ситуации следующие:

  • структура самого командного объекта; конструктор команды имеет некоторые обязательные поля, которые должны присутствовать для создания команды; это первая и самая быстрая проверка; это, очевидно, содержится в команде.
  • проверка полей низкого уровня, например непустота некоторых полей (например, имя пользователя) или формат (действительный адрес электронной почты). Этот вид проверки должен содержаться внутри самой команды, в конструкторе. Существует другой стиль использования isValidметода, но он кажется мне бессмысленным, так как кто-то должен был бы не забыть вызывать этот метод, когда на самом деле должно быть достаточно успешной реализации команды.
  • отдельные command validatorsклассы, которые несут ответственность за проверку команды. Я использую этот вид проверки, когда мне нужно проверить информацию из нескольких агрегатов или внешних источников. Вы можете использовать это, чтобы проверить уникальность имени пользователя. Command validatorsМогут быть введены любые зависимости, такие как репозитории. Имейте в виду, что эта проверка в конечном итоге согласуется с совокупностью (то есть, когда пользователь создается, тем временем может быть создан другой пользователь с тем же именем пользователя)! Кроме того, не пытайтесь поместить здесь логику, которая должна находиться внутри совокупности! Валидаторы команд отличаются от менеджеров Sagas / Process, которые генерируют команды на основе событий.
  • агрегатные методы, которые получают и обрабатывают команды. Это последний (вид) проверки, который происходит. Агрегат извлекает данные из команды и использует некоторую основную бизнес-логику, которую принимает (выполняет изменения своего состояния) или отклоняет ее. Эта логика проверена в строгой последовательной манере. Это последняя линия обороны. В вашем примере правило When a car uses Electric engine the only allowed transmission type is Automaticдолжно быть проверено здесь.

Я чувствую себя так, потому что метод сохранения репозитория, скорее всего, примет агрегат, который гарантирует, что после прохождения агрегата все инварианты будут выполнены. Когда логика (например, не пустота) присутствует только в самой валидации команды, другой программист может полностью пропустить эту валидацию и вызвать метод save в UserRepository непосредственно с объектом User, что может привести к фатальной ошибке базы данных, поскольку электронная почта возможно, было слишком долго.

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

Работая в качестве разработчика PHP, отвечающего за создание систем RESTful, моя интерпретация CQRS немного отличается от стандартного подхода к обработке асинхронных команд, например, иногда возвращает результаты команд из-за необходимости синхронной обработки команд.

Я также программист PHP, и я ничего не возвращаю из своих обработчиков команд (агрегатные методы в форме handleSomeCommand). Однако я довольно часто возвращаю информацию клиенту / браузеру в виде HTTP response, например, идентификатора вновь созданного агрегатного корня или чего-либо из модели чтения, но я никогда не возвращаю (на самом деле никогда ) что-либо из моих агрегатных командных методов. Достаточно простого факта, что команда была принята (и обработана - мы говорим о синхронной обработке PHP, верно ?!).

Мы возвращаем что-то в браузер (и все еще делаем CQRS книгой), потому что CQRS не является архитектурой высокого уровня .

Пример работы валидаторов команд:

Путь команды через валидаторы команд на пути к Агрегату

Константин Гальбену
источник
Что касается вашей стратегии валидации, пункт номер два выскакивает у меня как вероятное место, где логика будет часто дублироваться. Конечно, хотелось бы, чтобы пользовательский агрегат также проверял непустую и правильно сформированную электронную почту, нет? Это становится очевидным, когда мы вводим команду ChangeEmail.
Кинг-сайд слайд
@ king-side-slide нет, если у вас есть EmailAddressобъект значения, который сам себя проверяет.
Константин Гальбену
Это совершенно правильно. Можно заключить EmailAddressв капсулу , чтобы уменьшить дублирование. Что еще более важно, при этом вы также будете перемещать логику из своей команды в свою область. Стоит отметить, что это может быть слишком далеко. Часто похожие знания (объекты стоимости) могут иметь разные требования к валидации в зависимости от того, кто их использует. EmailAddressЭто удобный пример, потому что вся концепция этого значения имеет глобальные требования проверки.
Кинг-сайд-слайд
Точно так же идея «валидатора команд» кажется ненужной. Цель состоит не в том, чтобы предотвратить создание и отправку недействительных команд. Цель состоит в том, чтобы предотвратить их выполнение. Например, я могу передать любые данные с помощью URL. Если это неверно, система отклоняет мой запрос. Команда все еще создана и отправлена. Если для проверки команды требуется несколько агрегатов (т. Е. Набор пользователей для проверки уникальности электронной почты), лучше подойдет служба домена. Такие объекты, как «x validator», часто являются признаком анемичной модели, в которой данные отделяются от поведения.
Кинг-сайд слайд
1
@ king-side-slide Конкретный пример UserCanPlaceOrdersOnlyIfHeIsNotLockedValidator. Вы можете видеть, что это отдельный домен для Order, поэтому он не может быть проверен самим OrderAggregate.
Константин Гальбену
6

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

Система CQRS (как вы правильно указали) - это деталь реализации, представляющая общий поддомен, который реализует свой собственный связный механизм. Ваша модель никоим образом не должна зависеть от какой-либо части инфраструктуры CQRS, которая будет вести себя в соответствии с вашими бизнес-правилами. Цель DDD - смоделировать поведение системы таким образом, чтобы в результате была получена полезная абстракция функциональных требований вашей основной бизнес-области. Удаление какой-либо части этого поведения из вашей модели, как бы это ни было заманчиво, снижает целостность и сплоченность вашей модели (и делает ее менее полезной).

Просто расширив ваш пример включением ChangeEmailкоманды, мы можем прекрасно проиллюстрировать, почему вам не нужна какая-либо бизнес-логика в командной инфраструктуре, поскольку вам нужно будет дублировать ваши правила:

  • адрес электронной почты не может быть пустым
  • электронная почта не может быть длиннее 100 символов
  • адрес электронной почты должен быть уникальным

Итак, теперь, когда мы можем быть уверены, что наша логика должна быть в нашей области, давайте рассмотрим вопрос «где». Первые два правила могут быть легко применены к нашей Userсовокупности, но это последнее правило немного более нюансировано; тот, который требует некоторых дополнительных знаний хруста, чтобы получить более глубокое понимание. На первый взгляд может показаться, что это правило относится к a User, но на самом деле это не так. «Уникальность» электронного письма относится к коллекции Users(согласно некоторой области).

Ах, ха! Имея это в виду, становится совершенно ясно, что ваша UserRepository(ваша коллекция в памяти Users) может быть лучшим кандидатом для применения этого инварианта. Метод «save», вероятно, является наиболее разумным местом для включения проверки (где вы можете генерировать UserEmailAlreadyExistsисключение). Кроме того, домен UserServiceможет быть привлечен к ответственности за создание новых Usersи обновление их атрибутов.

Быстрый отказ - это хороший подход, но он может быть реализован только в том случае, когда и когда он соответствует остальной части модели. Может быть очень заманчиво проверить параметры метода (или команды) службы приложения перед дальнейшей обработкой, чтобы попытаться перехватить сбои, когда вы (разработчик) знаете, что вызов потерпит неудачу где-то в глубине процесса. Но при этом вы будете иметь дублированные (и просочившиеся) знания таким образом, что при изменении бизнес-правил, вероятно, потребуется более одного обновления кода.

король-бок слайд
источник
2
Я согласен с этим. Мое чтение до сих пор (без CQRS) говорит мне, что проверка должна всегда идти в доменной модели для защиты инвариантов. Сейчас я читаю CQRS, он говорит мне поставить проверку в объектах Command. Это кажется нелогичным. Знаете ли вы какие-либо примеры, например, на GitHub, где валидация помещается в модель предметной области вместо команды? +1.
w0051977