Стратегии для вызова другого ограниченного контекста

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

У меня есть сценарий использования в одном из моих ограниченных контекстов, где мне нужно связаться с другим BC, чтобы проверить агрегат. На самом деле, в будущем может потребоваться несколько BC, чтобы запросить данные проверки (но пока нет).

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


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

* Таким образом, запрос проверки от BC_A до BC_B будет запрашивать обзор всех элементов совокупности из BC_A и будет возвращать отчет, содержащий некоторые спецификации о том, что делать с этими элементами (следует ли нам это сохранить или нет и почему),
* Состояние агрегата будет проходить через (скажем, "черновик", затем "проверять" после запроса, а затем, в зависимости от отправленного отчета, оно будет "действительным" или "has_error", если таковой имеется. Если впоследствии пользователь решит не следовать спецификации, он может передать состояние агрегата "контролируемому", что означает наличие некоторой ошибки, но мы не заботимся об этом.

Команда ValidateMyAggregateCommand

Вариант использования:

  1. получить целевой агрегат по идентификатору
  2. изменить его состояние на "проверка"
  3. сохранить совокупность
  4. сделать проверку вызова (в другой BC)
  5. сохранить отчет о проверке
  6. подтвердить отчет о проверке с целевым агрегатом (который снова изменит свое состояние в зависимости от результата, должен быть "ОК" или "HAS_ERROR")
  7. сохранить совокупность снова
  8. генерировать событие домена в зависимости от результата проверки

он содержит 8 шагов, возможно от 1 до 3 транзакций и более.


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

  • после вызова проверки независимо (отчет является собственным агрегатом)
  • когда я сохраняю целевой агрегат (он будет внутри него)

Я предпочитаю первый вариант (шаг 5), потому что он в большей степени отделен - даже если мы можем утверждать, что здесь есть инвариант (???) - и поэтому существует задержка согласованности между постоянством отчета и подтверждением приема агрегат.


Я на самом деле борюсь с самим вызовом (шаг 4).

Я думаю, что мог бы сделать это несколькими способами:

  • А. синхронный вызов RPC с реализацией REST
  • B. вызов без ответа (void) (запускать и забывать), позволяющий нескольким вариантам реализации на столе (sync / async)
  • C. доменное событие, переведенное в техническое событие, чтобы достичь другого BC

А. Синхронный вызов RPC

// code_fragment_a
// = ValidateMyAggregateCommandHandler
// ---
myAggregate = myAggregateRepository.find(command.myAggregateId());  // #1
myAggregate.changeStateTo(VALIDATING);                              // #2
myAggregateRepository.save(myAggregate);                            // #3

ValidationReport report = validationService.validate(myAggregate);  // #4
validationReportRepository.save(report);                            // #5

myAggregate.acknowledge(report);                                    // #6
myAggregateRepository.save(myAggregate);                            // #7
// ---

validationService является доменной службой, реализованной на уровне инфраструктуры с компонентом службы REST (может быть также локальной проверкой, но не в моем сценарии).

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

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

B. Вызов без ответа (синхронизация или асинхронность)

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

// code_fragment_b0
// = ValidateMyAggregateCommandHandler
// ---
myAggregate = myAggregateRepository.find(command.myAggregateId());  // #1
myAggregate.changeStateTo(VALIDATING);                              // #2
myAggregateRepository.save(myAggregate);                            // #3

validationRequestService.requestValidation(myAggregate);            // #4
// ---

Здесь подтверждение отчета может происходить синхронно или асинхронно, внутри или снаружи исходной транзакции.

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

Это решение позволит быстро и легко начать синхронизацию, а затем перейти к асинхронному. Так что это гибкий.

B.1. Синхронный импл

В этом случае реализация validationRequestService (на уровне инфраструктуры) выполняет прямой запрос / ответ.

// code_fragment_b1_a
// = SynchronousValidationRequestService
// ---
private ValidationCaller validationCaller;

public void requestValidation(MyAggregate myAggregate) {
        ValidationReport report = validationCaller.validate(myAggregate);

        validationReportRepository.save(report);
        DomainEventPublisher.publish(new ValidationReportReceived(report))
}
// ---

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

// code_fragment_b1_b
// = ValidationReportReceivedEventHandler
// ---
public void when(ValidationReportReceived event) {
        MyAggregate myAggregate = myAggregateRepository.find(event.targetAggregateId());
        ValidationReport report = ValidationReportRepository.find(event.reportId());

        myAggregate.acknowledge(report);                                        
        myAggregateRepository.save(myAggregate);
}
// ---

Итак, у нас есть событие от нижнего уровня до уровня приложения.

БИ 2. Асинхронный

Асинхронная версия изменила бы предыдущее решение в impl ValidationRequestService (code_fragment_b1_a). Использование компонента JMS/AMQP позволило бы отправить сообщение в первый раз, а позже получить ответ независимо.

Я полагаю, что прослушиватель сообщений будет запускать то же событие ValidationReportReceived, а остальная часть кода будет такой же для code_fragment_b1_b.

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

C. Доменные события и автобус между BC

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

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

Должен ли я перевести событие домена в техническое событие перед отправкой по назначению???

техническое событие, вроде какого-то DTO, если это была структура данных


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

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

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

Таким образом, мой фасад приложения будет предоставлять 3 типа взаимодействия: - команды - запросы - события

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


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

Так что моя проблема в том, что я борюсь с интеграцией 2 до н.э.
Различные решения:
- Сервисный RPC (#A) прост, но ограничивает масштаб,
- сервис с сообщениями (#B) кажется правильным, но мне все еще нужна обратная связь,
- и доменные события (#C), я действительно не знаю, как пересечь границы с этим.

Еще раз спасибо!

1 ответ

Решение

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

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

Другими словами, у вас могут возникнуть проблемы с применением шаблонов DDD, потому что ваше понимание реальной проблемы, которую вы пытаетесь решить, является неполным.

В качестве отступления: при обращении за помощью в DDD, вы должны как можно точнее придерживаться вашей реальной проблемы, а не пытаться сделать ее абстрактной.

Тем не менее, есть некоторые шаблоны, которые могут вам помочь. Уди Даан подробно рассказывает о них в своем выступлении о надежных сообщениях, но я расскажу о важных моментах здесь.

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

  • Упорно изменение состояния
  • Планирование побочных эффектов

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

В вашем примере мы увидим три разных транзакции на счастливом пути.

Первая транзакция обновит состояние вашего агрегата до Валидации и запланирует задачу для получения отчета о валидации.

Эта задача выполняется асинхронно, запрашивая контекст удаленного домена, затем запускает транзакцию № 2 в этом BC, который сохраняет отчет проверки и планирует вторую задачу.

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

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

Таким образом, вы, скорее всего, увидите, что это реализовано с двумя дополнительными идеями. Во-первых, введение доменного события. Доменные события - это описания изменений состояния, которые имеют особое значение. Таким образом, агрегат описывает изменение (ValidationExpired?) Вместе с локальным состоянием, необходимым для его понимания, публикуя событие асинхронно. (Другими словами, вместо асинхронного запуска произвольной задачи мы запускаем асинхронно планируемую задачу PublishEvent с произвольным событием домена в качестве полезной нагрузки).

Во-вторых, введение "менеджера процессов". Менеджер процессов подписывается на события, обновляет свой внутренний конечный автомат и планирует (асинхронные) задачи для запуска. (Эти задачи являются теми же задачами, которые агрегат планировал раньше). Обратите внимание, что у менеджера процессов нет бизнес-правил; те принадлежат в совокупностях. Но они знают, как сопоставлять команды с генерируемыми ими событиями домена (см. Главу "Обмен сообщениями" в "Шаблонах интеграции предприятия" Грегора Хопе), чтобы планировать задачи тайм-аута, которые помогают определить, какие запланированные задачи не были выполнены в их SLA и так далее.

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

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

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

Должен ли я перевести событие домена в техническое событие перед отправкой по назначению

Я понятия не имею, что вы думаете, что это значит. Событие - это сообщение, описывающее что-то, что произошло. "Событие домена" означает, что что-то произошло в домене. Это все еще сообщение для публикации.

Другие вопросы по тегам