Как стать автором
Обновить

Строим систему доменных событий в модульном монолите

Время на прочтение10 мин
Количество просмотров18K
Всего голосов 21: ↑21 и ↓0+21
Комментарии37

Комментарии 37

НЛО прилетело и опубликовало эту надпись здесь

При выборе решения меня во втором варианте смутил лишний интерфейс у доменных объектов и необходимость либо ручками писать для каждой модели работу с коллекцией событий, либо использовать trait. Потому выбрал доменные сервисы) Когда выбирал реализацию, смотрел варианты в книжке DDD in PHP, рекомендую :) По мапингу в ОРМ - для модульного монолита придётся для каждого агрегата делать отдельный event store. Я решил хранить в одной таблице все события монолита, в каждом микросервисе тоже поднимать одну таблицу событий на микросервис.

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

А другие сервисы (не монолит) живут в том же инстансе базы? Просто не очень понятен компромис с хранением ивентов в БД:

1) Если инстанс один и тот же, то консистентность и транзакционность гарантирована, да. Но получаем БД как single point of failure (ВСЕ сервисы падают несмотря на микросервисную архитектуру) и БД заодно является performance bottleneck.

2) Если инстансы разные, то получаем опять проблему distributed storage, и можно было бы с таким же успехом сообщения сразу писать в message bus.

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

Ивенты в базе хранятся для того, чтобы быть сохранёнными в одной транзакции с агрегатами. После того, как транзакция закрывается и мы гарантированно сохранили изменения в модели, отдельная горутина (либо этот же скрипт PHP по событию закрытия транзакции) отправляет сообщения в брокер. То есть мы гарантированно сохранили изменения в базу и гарантированно, но с некоторой задержкой, отправили события в message bus. Про данный паттерн можно подробнее почитать тут - https://microservices.io/patterns/data/transactional-outbox.html Конечно же в рамках реализации есть ряд интересных моментов)

Если не ответил, пожалуйста, опишите подробнее проблему)

Спасибо, теперь понятнее.

Предполагаю, что у вас свои библиотеки/сервисы для взаимодействия с bus layer (отправка по завершению транзакции, tenant throttling, и т.п.) - не хотите что-то из этого в open-source выложить?

Не планировали, но идея хорошая, спасибо) Если доработок для обобщения будет не много, то закинем вот сюда - https://github.com/ispringtech

Не совсем понимаю почему вы рассматриваете именно статический диспатчер в 1м варианте.
Почему не заинджектить диспетчер через конструктор агрегата? Тогда и проблемы с тестированием пропадут (если ещё и события каким-то образом внедрять, а не хардкодить new SomeDomainEvent внутри метода).


class User
{
public function __construct(DomainEventDispatcherInterface $dispatcher)
    {
        $this->dispatcher = $dispatcher;
    }

public function getEventDispatcher(): DomainEventDispatcherInterface 
    {
        return $this->dispatcher;
    }

public function rename(Name $name): void
    {
        // ... переименовываем пользователя

        $this->getEventDispatcher()->dispatch(new UserRenamed($user->getId(), $name));
    }
}

В нашу предметную модель попали методы, которые не совсем относятся к предметной модели.

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


Не понимаю проблемы, что именно вас смутило?


То, что вы вынуждены создавать по отдельному сервису-обёртке чтобы агрегаты выбрасывали свои события — большее зло.


  1. Как теперь разобраться, какой из методов агрегата нужно вызывать через сервис, а какой — напрямую?
  2. Как объяснять почему событие UserRenamed не срабатывает, если мы вызываем $user->rename(), но срабатывает если вызвать $userService->rename($user, $newUserName) ?

По мере появления новых событий UserService продублирует весь публичный интерфейс агрегата User, только в сигнатуре каждого метода появится ещё User $user


interface SomeAggregate {

public function someMethod(//... someMethod arguments);

}

interface SomeAggregateService {

public function someMethod(SomeAggregate $aggregate, //... someMethod arguments);

}

Тогда уже декораторы для агрегатов сочинять (что-то вроде EventAwareUser($user,$dispatcher);), но польза, как по мне, не очевидна, а сложность растёт.

Тихие "минусы" возмущают.
Давайте договоримся: прежде чем ставить минус комментарию, вы ответите на поставленные в нём вопросы/объясните причину. Ходят легенды, что раньше на Хабре это, вроде, было даже принято.


  1. Как теперь разобраться, какой из методов агрегата нужно вызывать через сервис, а какой — напрямую?
  2. Как объяснять почему событие UserRenamed не срабатывает, если мы вызываем $user->rename(), но срабатывает если вызвать $userService->rename($user, $newUserName) ?

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

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

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

  • В первую очередь агрегат при восстановлении из базы у нас собирается в рамках монолита с помощью ORM, усложнять логику его конструирования, прокидывая актуальный dispatcher в методы восстановления агрегата, не хотелось. Наш диспатчер, ввиду multi tenant модели, инициализируется на каждый запрос с контекстом запроса и там же на него подписываются нужные handler'ы (ведь важно в рамках сервисов инициализировать, в каком tenant идёт сейчас работа и местами другие контекстные параметры). В рамках PHP монолита пришлось бы писать свои обёртки над ORM, что в последующем могло усложнить её обновление. В рамках go микросервисов мы восстанавливаем агрегаты специальными методами, данные подготавливают реализации репозиториев ручками из базы. В этом случае пришлось бы прокидывать диспатчер в репозитории. В целом, восстановление состояния агрегата тоже интересная задача со своими подводными камнями)

  • В момент создания агрегата тоже пришлось бы прокидывать этот же диспатчер, то есть опять не обойтись без доменного или app сервиса, который владеет ссылкой на диспатчер.

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

По вопросу попадания лишних методов в модель - это некоторый мой личный пуризм) Идея в том, что предметная модель отвечает за свои инварианты, наличие у неё метода "получить события" с точки зрения экспертов предметной области непонятно)

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

агрегат при восстановлении из базы у нас собирается в рамках монолита с помощью ORM
В момент создания агрегата тоже пришлось бы прокидывать этот же диспатчер

Всё упирается в сложности при создании/восстановлении инстанса агрегата и линковкой его с другими слоями. Архитектурная ошибка где-то здесь, получилась жёсткая связь, которой старались избегать, IoC нарушен: ORM диктует как создавать агрегат.


В момент создания агрегата тоже пришлось бы прокидывать этот же диспатчер, то есть опять не обойтись без доменного или app сервиса, который владеет ссылкой на диспатчер

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


Спасибо за статью! За этим путём интересно наблюдать: такие пограничные моменты авторы книжек почему-то тактично обходят стороной

ORM как раз ничего не создает, а восстанавливает из хранилища в соответствии с persistance ignorance. Если вы в коллекцию сущностей в оперативке добавите а потом запросите сущность, вы же не ожидаете что вызовется конструктор при запросе?

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

Если вы в коллекцию сущностей в оперативке добавите а потом запросите сущность, вы же не ожидаете что вызовется конструктор при запросе?

Либо я не понял вас, либо я принципиально что-то не понимаю.


Ожидаю, конечно. Как вы без new SomeEntity() создадите новый инстанс объекта (агрегат это, сущность, или любой другой объект)? Тут вопрос только в том, где и кто это будет делать. Если эти new с последующими сеттерами будут разбросаны по всему проекту — будет больно вносить изменения, если где-то в фабрике — проще.


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

Давайте на примере, не понимаю вас. Вот у нас магазин, Order — это Aggregate Root. Нужно получить 5 последних заказов, и с каждым из них провести какие-то операции (третий отменить, для второго изменить адрес доставки, по первому получить трек код, итд итп). Как вы предалаете создать Order "ровно 1 раз" для 5 заказов?


В классическом случае мы сходим в OrderRepository и получим 5 Order'ов, для каждого из них вызовется new, соответственно, и конструктор. Что предлагаете вы?

e = new entity(id: 1)

eCollection.add(e)

alsoE = eCollection.byID(1) <- создается ли тут новый инстанс entity? Если нет почему он должен создаваться когда eCollection это база данных?

Так понятно.
Нет, не должен, всё верно.

"ORM как раз ничего не создает, а восстанавливает из хранилища в соответствии с persistance ignorance." - тут всё верно. При восстановлении агрегата из базы конструктор не вызывается. Это особо важно в случаях, когда есть определённые бизнес правила создания агрегата. Потому за всё время жизни он и правда создаётся один раз. В примере с заказами - заказ создан один раз, во всех остальных use case он восстанавливается из базы в текущем состоянии.

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

"Архитектурная ошибка где-то здесь, получилась жёсткая связь". Жёсткой связи между ORM и доменом нет, как создавать агрегат решается в домене, а вот как его восстановить - это отдельный вопрос вопрос. В рамках ORM может быть прописан маппинг на базу (инфраструктурный код), в рамках go сервисов мы пробовали в домен добавлять специальный метод "восстановления" с набором параметров, мапающихся на состояние агрегата. При этом в обоих случаях надо будет вызывать дополнительно инъекцию диспатчера, если он будет полем агрегата. И вот этого как раз делать не хочется.

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

Не понимаю.
Вот вам нужно восстановить из базы агрегат User с id 5. Можете показать пример кода как это происходит?


Там же наверняка что-то вроде UserRepository->byId(5), в UserRepository через DI заинджекчена UserFactory, внутри которой и происходит пресловутый new Domain\User()

На примере используемой нами Doctrine: https://www.doctrine-project.org/projects/doctrine-orm/en/2.9/tutorials/getting-started.html

Doctrine ORM does not use any of the methods you defined: it uses reflection to read and write values to your objects, and will never call methods, not even __construct.

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

рамках go сервисов мы пробовали в домен добавлять специальный метод "восстановления" с набором параметров, мапающихся на состояние агрегата.

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

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

Интеграционные события у нас не обрабатываются синхронно даже в рамках монолита. Они всегда проходят через брокер и обрабатываются асинхронно. Если же надо обновить данные модуля А на изменения модели из модуля Б синхронно, то тут обычно делается сага через оркестрацию с синхронным вызовом методов модулей А и Б и с компенсирующими действиями. Мы реализуем это либо отдельно написанной сагой, либо путём вызова из app сервиса модуля Б через anti corruption layer методов API модуля А. И тут в целом не важно, оба модуля в монолите, или один из них уже перенесён в микросервис. Но важно помнить, что синхронный вызов может увеличивать связанность модулей.

А насчёт получения данных из домена для отдачи в ответ на запрос - тут просто добавляется подписка на доменное событие и данные, полученные из него, возвращаются наружу)

Уважаемый автор!

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

На вопрос "откуда" - одного конкретного источника не укажу)

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

Реализация всех слоёв в рамках модуля позволяет нам легко вынести контекст в отдельный микросервис и изолирует код контекстов. Используя deptrac мы настроили правила, по которым модули друг от друга не зависят ни на одном из уровней, кроме anti corruption layer.

Ну вот смотрите.


  1. Если бизнес-логика растекается по слоям (например, обработка события UserRenamed, которая происходит на уровне приложения; то есть смотря только на домен нельзя понять что происходит когда переименовывается пользователь), то в чем здесь фокусировка на домене? Есть бизнес требование "Когда пользователь переименовывается, должно произойти N", и смотря на модель мы только видим что "Когда пользователь переименовывается, выбрасывается событие, а кто его поймает (и поймает ли) и что произойдёт (и произойдёт ли) — непонятно". Бизнес требование потеряно из домена, и по-моему, по самой сути DDD, этого происходить не должно, но происходит во всех реализациях доменных событий в DDD что я видел.


  2. Раз интерфейс сущностей растекается по сервисам (например, реализация метода User->rename() утекает из сущности User в UserRenameService, или, что ещё хуже, в UserService), то чем эта модель rich (чем она лучше анемичной)? Таким образом можно весь интерфейс растащить в сервисы, и тогда сущность превратится в попу POPO (Plain Old PHP Object), только ещё из без сеттеров (потому что инварианты, констистентность итп).


  3. Если в бизнес слой проникают чужеродные для бизнеса (инженерные, т.е. из мира software engineering) понятия вроде EventDispatcher, то где здесь "единый язык" (ubiquitous language)?


  4. Если в рамках контекста домена вы реализуете все нижележащие слои, то чем это отличается от разаработки разных приложений в рамках одной инфраструктуры?



Это не столько вопросы именно к вашей реализации (не поймите неправильно, я вовсе не пытаюсь вас поставить в тупик/поумничать), а к самому DDD, к его концептам и предлагаемым реализациям.


После синей книжки Эванса читаю красную книжку Вернона, и чем больше читаю и смотрю листинги, тем больше мне кажется, что DDD — это не про домен драйвен дизайн, а что-то вроде "Двигаем Депенденсис на ваши Деньги".


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

Выскажу свое мнение. По поводу пункта 2. Доменные сервисы, согласно DDD, вообще-то должны быть очень редким явлением. Например, когда нужно реализовать какую-то логику, требующую взаимодействия агрегатов, например. Или когда эта бизнес-логика не является частью какого-либо агрегата (пример искать лень :). В данной реализации, по большому счету, они являются middleware, основная задача которых диспатчинг событий. Мне этот вариант тоже кажется переусложнением, но автор объяснил, почему он так сделал. Вы же можете реализовывать хоть через агрегаты, хоть через poco+services, самое главное, как мне кажется, чтобы был один и только один способ вызова нужного поведения (или мутации) объекта. В данном варианте, как и в варианте с poco+services это "гарантируется" договоренностями. В случае агрегатов это гарантировано самой реализацией. Однако, из своей практики я делаю вывод, что реализация poco+services является наиболее простой и гибкой.

Доменные сервисы, согласно DDD, вообще-то должны быть очень редким явлением

Да, и вот как раз у вернона:


Чрезмерное увлечение СЛУЖБАМИ обычно приводит к нега­тивным последствиям и созданию АНЕМИЧНОЙ МОДЕЛИ ПРЕДМЕТНОЙ ОБЛАСТИ [Fowler, Anemic], в которой вся логика предметной области заключена в службах, а не распределена по СУЩНОСТЯМ и ОБЪЕКТАМ-ЗНАЧЕНИЯМ

Примерно об этом же я и говорил


Знание, относящееся исключительно к предметной области, никогда не
должно уходить к клиентам
. Даже если клиентом является ПРИКЛАДНАЯ СЛУЖ­
БА (Application Service)

И в этом моя главная претензия. Все реализации доменных событий что я видел нарушают этот принцип.


Если моделировать pub/sub внутри домена — неизбежно получается EventDispatcher, который нарушает единый язык. А если избавиться от событий, и хардкодить внутри методов (например, прям из метода агрегата отправлять email) — то получается нарушение SRP, лишние зависимости, грязь, ужас, проблемы.


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

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

Насчёт того, что EventDispatcher нарушает единый язык - ну это уж совсем, извиняюсь, ddd головного мозга ​

Вот из статьи microsoft:

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

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

Насчёт того, что EventDispatcher нарушает единый язык — ну это уж совсем, извиняюсь, ddd головного мозга ​

вовсе нет. EventDispatcher — термин из мира разработки софта, инженерный, а бизнес (не знаю, сеть магазина цветов) об этом термине знать не должен.


отправка уведомления пользователям — это бизнес-логика или нет

конечно это бизнес логика.


  1. это прямой контакт с клиентом
  2. уведомление — часть сервиса (в смысле обслуживание клиента)

если вам важно что-то делать на уровне домена, то почему бы и нет

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


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


Надо руководствоваться больше здравым смыслом

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

EventDispatcher — термин из мира разработки софта, инженерный, а бизнес (не знаю, сеть магазина цветов) об этом термине знать не должен.

Это, наверно, в идеальном мире, где бизнес читает код :) А в реальном мире бизнес про него и не узнает ;) Мне кажется, что самое важное, чтобы все говорили на едином языке и понимали его одинаково, и в коде использовали те же термины. Но это совсем не исключает возможности введения доп/вспомогательных терминов, имхо. В конце концов, читать и поддерживать этот код будут программисты, а не аналитики или владельцы бизнеса. ДДД не должно быть самоцелью, вы же не курсовой проект на эту тему пишите и не будете его защищать Вернону или Эвансу.

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

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

Насчёт API. При вынесении модуля в микросервис с его стороны нужно будет дописать только код по преобразованию ответов, например, в json в случае с JSON API . Со стороны модулей-клиентов надо будет поменять код только в anti-corruption layer, а по сути заменить вызов php классов на вызов удаленных эндпойнтов и парсинг ответов. Далее этот слой и так преобразует входные данные в модели модуля-клиента. Это гораздо быстрее и проще, чем заменять вызовы методов по всему приложению.

Возможно, вы как-то по-другому представляете устройство нашего модуля.

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

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


Пример:


Бизнес требование: Когда заказ отменяется, клиенту отправляется email "Заказ #14 отменён"

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


Агрегат Order выбрасывает событие OrderCancelled, событие попадает в шину, из шины его подхватывает кто-то и делает что-то. В рамках домена неизвестно кто и что сделает с этим событием.


Знание о том, что при отмене заказа клиенту отправляется email теряется из домена.


Чтобы вернуть это знание в домен, вам придётся в рамках домена сделать что-то вроде UserNotiferService->sendNotification(NotificationFactory::fromEvent(OrderCancelled), $user), а чтобы сделать это в рамках домена придётся поступиться с каким-то из принципов: метод отмены заказа станет зависим от службы уведомлений, а служба уведомлений от инфраструктурных деталей.

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

Если в рамках контекста домена вы реализуете все нижележащие слои, то чем это отличается от разаработки разных приложений в рамках одной инфраструктуры?

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

Один модуль полностью реализует весь домен (а не только доменный слой)

Так понятнее, только решительно непонятно причем тут DDD. DDD оно про 1 домен, который делится на поддомены и контексты.


Если у вас несколько доменов (вы их называете модулями) — это несколько разных приложений.


Яндекс.Поиск, Яндекс.Музыка и Яндекс.Маркет — это не разные контексты домена, не поддомены одного домена, а разные бизнесы, следовательно разные домены, следовательно разные приложения. Каждое из них отдельно может быть спроектировано по DDD, все вместе в одном (как поддомены/модули) — нет, потому что области проблем совершенно разные.


Если мы сойдёмся с вами здесь — то да, мы понимаем друг друга :)


Один модуль полностью реализует весь домен (а не только доменный слой)
Если я назову это не контекстом домена, а модулем

То возникает терминологическая путаница


Следует иметь в виду, что ОГРАНИЧЕННЫЕ КОНТЕКСТЫ нельзя считать заменой МОДУЛЕЙ. МО­ДУЛИ используются для агрегации связанных объектов предметной области и отделения от объектов, которые не являются связанными или являются слабо связанными

Так...началось жонглирование терминами :) хорошо, если говорить на строгом языке ддд, то модуль у нас - это контекст/поддомен. Говоря, домен, я говорю "контекст/поддомен", ибо мало кто вникает в разницу, да и в рамках разговора про монолит мне казалось это очевидным :) В моем комментарии была важна разница между доменом (поддоменом) и доменным слоем. Вы же не думаете, что кто-то пихает в монолит разные "бизнесы"?

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

Сейчас, возможно, я бы не заморачивалась с разделением на app и domain слои, а сделала бы все в одном сервисном слое с poco объектами. Но это, как водится, не точно, и в процессе реализации мое мнение могло бы измениться ​ делайте как проще и как считаете разумнее.

если говорить на строгом языке ддд, то модуль у нас — это контекст/поддомен. Говоря, домен, я говорю "контекст/поддомен", ибо мало кто вникает в разницу, да и в рамках разговора про монолит мне казалось это очевидным :)

Камень в огород DDD: границы понятий настолько нечеткие, что разработчики между собой договориться не могут :) Какой там uniquitous language!

Вы помещаете Event Dispatcher и Event Handler в слой application layer. Но генерация и потребление доменных событий целиком относятся только к слою domain layer. Поэтому не вижу никакой причины выносить какую-либо часть этого функционала в другой слой.

Интерфейс находится на уровне домена, чтобы оттуда выбрасывать доменные события.

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

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий