Доменно-ориентированный дизайн (DDD): обработчики событий домена - где их разместить?

Я не понимаю, где обрабатывать события домена в приложении, основанном на гексагональной архитектуре. Я говорю о событиях внутренней области ограниченного контекста, а не о межконтекстной интеграции / приложениях / публичных событиях.

Фон

Насколько я понимаю, логика приложения (то есть логика сценария использования, логика рабочего процесса, взаимодействие с инфраструктурой и т. Д.) - это то, к чему принадлежат обработчики команд, потому что они специфичны для определенного дизайна приложения и / или дизайна пользовательского интерфейса. Затем обработчики команд вызывают уровень домена, где находится вся логика домена (службы домена, агрегаты, события домена). Уровень домена не должен зависеть от конкретных рабочих процессов приложения и / или дизайна пользовательского интерфейса.

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

Вопрос

Я считаю знание о том, какие события предметной области и какие эффекты влияют на другие агрегаты, является неотъемлемой частью самих знаний предметной области. Если бы я удалил все, кроме уровня домена, разве не следует где-то сохранить эти знания? На мой взгляд, мы должны размещать обработчики событий домена непосредственно на уровне домена:

  • Это могут быть службы домена, которые получают как событие домена, так и агрегат, на который оно может повлиять, и преобразовывают событие домена в один или несколько вызовов методов.

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

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

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

РЕДАКТИРОВАТЬ: Примеры

Начнем с этого часто используемого подхода:

// in application layer service (called by adapter)
public void HandleDomainEvent(OrderCreatedDomainEvent event) {
    var restaurant = this.restaurantRepository.getByOrderKind(event.kind);
    restaurant.prepareMeal(); // Translate the event into a (very different) command - I consider this important business knowledge that now is only in the application layer.
    this.mailService.notifyStakeholders();
}

Как насчет этого вместо этого?

// in application layer service (called by adapter)
public void HandleDomainEvent(OrderCreatedDomainEvent event) {
    var restaurant = this.restaurantRepository.getByOrderKind(event.kind);
    this.restaurantDomainService.HandleDomainEvent(event, restaurant);
    this.mailService.notifyStakeholders();
}

// in domain layer handler (called by above)
public void HandleDomainEvent(OrderCreatedDomainEvent event, Restaurant restaurant) {
    restaurant.prepareMeal(); // Now this translation knowledge (call it policy) is preserved in only the domain layer.
}



Ответы (3)


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

Однако ничто не мешает вам писать независимые от технологий обработчики и использовать технологически ориентированные адаптеры, которые отправляют им сообщения.

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

В итоге я создал класс ActionRequiredPolicy в домене, в котором были методы обработки событий, такие как void when(CaseAssigned event), и у меня был даже обработчик на уровне инфраструктуры, который просто информировал политику.

Я думаю, еще одна причина, по которой люди помещают их на уровни infrastructure или application, заключается в том, что часто политики реагируют на события, вызывая новые команды. Иногда этот подход кажется естественным, но в других случаях вы хотите явно указать, что действие должно произойти в ответ на событие, а в противном случае не может произойти: преобразование событий в команды делает его менее явным.

Вот более старый вопрос, который я задал по этому поводу.

person plalx    schedule 21.04.2021
comment
Я бы поспорил в пользу явных обработчиков событий домена просто из-за того, что некоторые команды DO выполняются в ответ на событие, независимо от того, могут ли они также произойти в других случаях использования. - person domin; 21.04.2021
comment
Я также немного запутался в терминологии, используемой в шторме событий. Например, стикеры, представляющие команды, вероятно, не являются командами уровня приложения, а скорее простыми общедоступными методами самих агрегатов. Таким образом, при преобразовании события в одну или несколько команд (в случае штурма событий) это может быть реализовано просто как обработка события домена на уровне приложения / домена с последующим вызовом метода на агрегате. - person domin; 21.04.2021

Ваше описание очень похоже на поиск событий.

Если источник событий (состояние агрегата определяется исключительно событиями домена), то обработчик событий находится на уровне домена, и на самом деле общая тенденция состоит в том, чтобы излучать уровень порта / адаптера / уровня защиты от коррупции. команды; затем обработчик команды для агрегата (при необходимости) использует обработчик событий для получения состояния агрегата, затем на основе состояния, и команда генерирует события, которые сохраняются, чтобы обработчик событий мог получить следующее состояние. Обратите внимание, что здесь обработчик событий определенно принадлежит к уровню домена, и обработчик команд, вероятно, тоже.

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

Стоит отметить, что событие в некотором смысле часто является просто повторным вызовом метода для агрегата.

person Levi Ramsey    schedule 18.04.2021
comment
Спасибо за ответ. Я считаю, что поиск событий полностью отличается от событий предметной области. При поиске событий ваши события являются техническими / механическими / внутренними, они просто должны указывать, что изменилось от состояния X к следующему состоянию Y. События домена, с другой стороны, гораздо более описательны и также избирательны. Совокупность событий предметной области не дает вам ничего близкого к текущему состоянию агрегата. Преобразование события домена в команду - это именно то, что я считаю частью логики домена, а не логики приложения. - person domin; 19.04.2021

Я следую этой стратегии управления событиями домена:

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

Предположим, у нас есть командная шина:

  • Я поместил вокруг него декоратор, который сохраняет события, сгенерированные командой.
  • Рабочий обрабатывает хранилище событий и публикует события вне ограниченного контекста (BC).
  • Другие БК (или те, которые его опубликовали), заинтересованные в мероприятии, подписываются на него. Обработчики событий похожи на обработчики команд, они принадлежат к прикладному уровню.

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

person choquero70    schedule 23.04.2021
comment
Спасибо, что поделились своим подходом. Я согласен с тем, что это допустимая стратегия, однако мой вопрос нацелен на более конкретную часть: как выглядят обработчики событий на уровне вашего приложения? Они напрямую переводят событие в совокупные команды / методы или событие передается на уровень домена? Мое предложение состояло в том, чтобы передать событие полностью от уровня адаптера (гексагонального) через уровень приложения (который запускает транзакции и загружает агрегаты) в назначенный обработчик событий домена, который, наконец, обрабатывает событие. - person domin; 23.04.2021
comment
Обработчик событий выглядит как обработчик команд. Он принимает DTO (издатель преобразовал событие домена в DTO) и выполняет любое действие в ответ на событие. Действие может включать в себя вызов домена или действие, выполняемое инфраструктурой (например, отправка электронного письма). Если вам нужно сделать несколько вещей, используйте обработчик для каждой из них. - person choquero70; 23.04.2021
comment
Я говорю о представлении обработчика событий домена на самом уровне домена (public void HandleDomainEvent(DomainEventXY event)). Уровень приложения также имеет соответствующий обработчик, который вместо этого использует класс событий DTO. Он выполняет всю инфраструктуру / рабочий процесс (отправка писем, загрузка данных и т. Д.), Но, что наиболее важно, вызывает вышеупомянутый HandleDomainEvent на уровне домена (раньше он должен преобразовать DTO в исходный класс, хотя я бы рассмотрел исходный доменное событие в любом случае как POCO). Теперь мы сохранили все важное в домене. - person domin; 23.04.2021
comment
введение обработчика событий домена в самом слое домена ... Я бы этого не делал ... Я цитирую здесь ответ Во Вернона, когда я спросил его об этом несколько лет назад ... В случае ограниченного контекста A публикация событий домена и ограниченный контекст B, использующий эти события, контекст B обычно переводит события в адаптере порта во что-то, что понимает внутренняя модель B. Это инструмент для десериализации событий безопасным для типов способом. - person choquero70; 23.04.2021
comment
Мы говорим здесь о разных вещах. Я говорю только о внутренних событиях домена ограниченного контекста. Я прямо заявил об этом во втором предложении вопроса. Несомненно, интеграционные мероприятия должны проходить через антикоррупционный слой. Тем не менее, все еще есть варианты, чтобы преобразовать их во внутренние вызовы командных портов или событийных портов. - person domin; 23.04.2021
comment
Я понимаю, что вы говорите. Но нет двух типов событий (интеграция и предметная область). Есть только один тип: события домена, которые сериализуются в сообщения (dtos), а затем подписчик BC реагирует на это, но внутри домена нет обработчика событий домена. Это не я говорю это, это Вон Вернон. - person choquero70; 23.04.2021
comment
Ладно, но это не похоже на мнение Вона Вернона - это чистая правда и единственный способ сделать это. ;) В Интернете есть множество литературы, которая утверждает обратное. Кроме того, в своей красной книге Вернон приводит примеры, когда он регистрирует обработчики событий локально в обработчиках команд, что, честно говоря, является худшей идеей IMO. - person domin; 23.04.2021
comment
Меня интересуют конкретные аргументы против (или даже за) моего предложения. Аргументы авторитетов или распространенного мнения для меня не в счет. Но, тем не менее, я ценю, что вы поделились своим мнением, так что спасибо за это! - person domin; 23.04.2021
comment
Тогда, если вы не возражаете против полномочий, я, Джон Доу :), буду возражать против вашего предложения: обработчику событий, возможно, придется организовать некоторые объекты домена, такие как обработчик команд, или выполнить некоторую инфраструктуру, независимую от домена. Таким образом, он должен принадлежать к более высокому уровню, клиенту домена. Этот более высокий уровень - прикладной уровень. - person choquero70; 23.04.2021
comment
Оно делает! См. Мой исходный пост. И затем он также появляется непосредственно на один уровень ниже - уровень домена - снова (либо как служба домена, либо как агрегированный метод), принимая то же самое событие домена в качестве входных данных и выполняя в ответ только логику домена. Таким образом, я обрабатываю событие явно на обоих уровнях, делая то, что применимо только на соответствующем уровне. То есть: логика рабочего процесса и инфраструктуры, такая как отправка писем или загрузка агрегатов на уровне приложения и применение бизнес-политик на уровне домена. - person domin; 24.04.2021
comment
Я добавил пример в исходный пост, который, надеюсь, проясняет мою точку зрения. - person domin; 24.04.2021
comment
Прежде всего, приготовление еды и отправка электронной почты - это две разные вещи, которые вы должны делать в качестве реакции на событие домена, поэтому вам нужно 2 обработчика (принцип единой ответственности). Независимо от этого, знание того, что делать, когда происходят события домена, принадлежит уровню приложения, потому что может подразумевать много вещей (или то, что управляет многими объектами домена, которые не являются знаниями предметной области, а знаниями уровня приложения) - person choquero70; 24.04.2021
comment
Помимо моего последнего комментария, обработчик событий вашего домена, получающий 2 аргумента, неверен. Что, если вы назовете это рестораном в качестве второго аргумента, который не является рестораном мероприятия? Ресторан, который должен приготовить еду, указан в событии. - person choquero70; 24.04.2021
comment
Представьте, что в качестве реакции на событие вашего домена может подразумеваться несколько ресторанов, потому что каждый обслуживает часть заказа, или вам нужно искать ближайший к клиенту, или что-то еще, это бизнес-логика, принадлежащая уровню приложения, например В случае использования реакция на событие предметной области не является знанием предметной области. Вы просто разделяете этап реакции (поток, принадлежащий уровню приложения), который является призывом приготовить еду, но вы не знаете, какое место этот этап занимает во всей реакции. - person choquero70; 24.04.2021
comment
Я думаю, вы правы! Однако проблема возникает из-за того, что доменные службы не могут загружать агрегаты сами по себе. Если бы им было разрешено напрямую общаться с репозиториями (что является допустимой альтернативной схемой, также упомянутой Верноном в его книге, но мне это не очень нравится), тогда вы можете обрабатывать весь поток внутри одного обработчика событий домена. Но если нет, то вы вынуждены играть в пинг-понг между слоями, что, согласен, совершенно бесполезно. Я предполагаю, что когда вы решаете оставить репозитории вне домена, вы должны полностью обрабатывать события на уровне приложения. - person domin; 24.04.2021
comment
Но разве эта проблема не присутствует и для любого другого типа доменной службы, которая действует на несколько агрегатов? Решение о том, какие агрегаты загружать и работать с ними, всегда будет частью уровня приложения, даже если можно принять во внимание эту важную бизнес-логику. - person domin; 24.04.2021
comment
Вызов репозиториев для загрузки / сохранения агрегатов не является логикой домена, поэтому он не должен быть частью службы домена. Точка двух аргументов в вашем обработчике домена также важна. - person choquero70; 24.04.2021
comment
Не обязательно. Вернон, например, проводит различие между репозиториями на основе коллекций и репозиториями на основе персистентности (о которых мы говорим здесь). Первые разрешено использовать в доменных службах, поскольку они рассматриваются как чистые коллекции в памяти. Что касается двух аргументов: у вас есть эта проблема для любой службы домена, это не имеет ничего общего с обработчиками событий. В моем примере, если вы хотите убедиться, что вызывающий абонент правильно использует службу, просто выполните проверку предварительных условий в методе. Хорошее прочтение: medium.com/swlh/ - person domin; 24.04.2021
comment
У вас есть эта проблема для любой службы домена ... Я не согласен. Например, в банковском домене служба домена будет переводить сумму с банковского счета на другой ... перевод (Счет с, Счет на, Сумма денег) - person choquero70; 24.04.2021
comment
Первые разрешено использовать в доменных службах, поскольку они обрабатываются как чистые коллекции в памяти ... поэтому вы не можете сказать универсальную истину, что обработчики событий домена могут быть доменными службами. Вы должны сказать это на всякий случай, если ваши репозитории основаны на коллекциях. - person choquero70; 24.04.2021
comment
Первые разрешено использовать в доменных службах, поскольку они обрабатываются как чистые коллекции в памяти ... у вас может быть интерфейс репозитория на основе коллекции, но реализованный с помощью устройства сохранения, например db, и тогда это не настоящая коллекция inmemory. - person choquero70; 24.04.2021
comment
Как насчет вашего примера с услугой перевода, когда обе учетные записи принадлежат одному и тому же физическому лицу? Вуаля, у вас есть ограничение, которое необходимо установить перед вызовом метода. Конечно, всегда есть образцы, которые достаточно просты, чтобы больше не иметь ограничений, но дело не в этом. - person domin; 24.04.2021
comment
это не имеет ничего общего с обработчиками событий ... это все, что нужно, поскольку события могут содержать совокупный идентификатор, и поэтому вам нужен второй аргумент, чтобы не загружать агрегат из идентификатора - person choquero70; 24.04.2021
comment
В конце концов, я думаю, что есть несколько конкурирующих сил. Я по-прежнему считаю свое желание инкапсулировать логику обработки доменных событий на уровне предметной области как оправданную силу. В конце концов, как всегда, это будет компромисс. Но сейчас я определенно различаю этот вопрос. Благодарю вас за ваше время и ваши реальные аргументы, это мне помогло! ;) Если хотите, мы можем продолжить наедине. - person domin; 24.04.2021
comment
если вы хотите убедиться, что вызывающий абонент правильно использует службу, просто выполните проверку предварительного условия в методе ... эта проверка будет означать загрузку агрегата из идентификатора ресторана события, чтобы сравнить его со вторым аргументом . Так почему второй аргумент? Вы могли получить ресторан с мероприятия - person choquero70; 24.04.2021
comment
Как насчет вашего примера с услугой перевода, когда обе учетные записи принадлежат одному и тому же физическому лицу? Вуаля, у вас есть ограничение, которое вам нужно установить перед вызовом метода ... проблема заключалась в том, чтобы добавить ненужные аргументы, а не выполнять проверки, конечно, передача требует проверок - person choquero70; 24.04.2021
comment
Благодарю вас за ваше время и ваши реальные аргументы, это мне помогло! Мы можем продолжить наедине, если хотите ... Я думаю, что я привел вам много аргументов (как Джон Доу, как вы утверждали, а не как авторитет), но я думаю, что вы никогда не сдадитесь. И даже напоследок вы тоже процитировали авторитет :) - person choquero70; 24.04.2021
comment
Событие не содержит совокупного идентификатора, кстати. Сумма определяется видом заказа. Но это не относится к обсуждению. Если вы считаете, что полное событие содержит слишком много деталей, тогда хорошо, сначала преобразуйте его в более тонкое событие или просто извлеките нужные вам значения. Главное, чтобы вы не создавали команды, если событие выходит за рамки логики домена, поскольку этот перевод, как я утверждаю, должен быть частью логики домена. Если вы расцените это как борьбу, то я сдамся здесь и сейчас. - person domin; 24.04.2021
comment
Я имею в виду, что у вас был обработчик событий домена, который не использовал событие для реакции на него, но использовал добавленный второй аргумент. В этом нет смысла. Что касается события, имеющего идентификатор ресторана или вид заказа, я не знал, я хотел сказать, что, возможно, вам придется каким-то образом искать совокупность по атрибутам события. Что касается борьбы и сдачи или нет, это была просто метафора, шутка. Обсуждения хорошие. - person choquero70; 24.04.2021
comment
Наше обсуждение действительно упустило суть моего вопроса, но вместо этого подняло некоторые другие важные вопросы. Поэтому вместо этого я создал еще один вопрос: stackoverflow.com/questions/67254749/ - person domin; 25.04.2021