Интерфейс находится на уровне домена, чтобы оттуда выбрасывать доменные события.
Для обработки события в этом же контексте в другом агрегате используется уровень приложения, именно там происходит "подписывание" агрегатов на разные события.
Можно ли это сделать только на уровне домена - думаю да. Но появится код, который должен будет заниматься подпиской при инициализации/создании агрегатов на доменные события :) если на вашем языке/фреймворке это можно сделать красиво - ок :)
Думаю позже выложу в отдельной статье :) пока дам время решить тем, кому интересно :) если актуально решающим, могу выкладывать подсказки раз в неделю, как и планировал изначально :)
Да, всего подсказок было 6, причём две самые большие и выданные коллегам последними были в видео, ссылка на которое в статье :) итого статья содержит 3 из 6 подсказок. Коллегам в момент вручения футболок я озвучил только 1 подсказку про надпись на футболках :)
На вопрос "откуда" - одного конкретного источника не укажу)
Если смотреть с точки зрения микросервисной архитектуры - то каждый микросервис реализует свой небольшой ограниченный контекст, и в каждом микросервисе реализованы все слои, не только доменный. В рамках монолита с несколькими ограниченными контекстами ваш вариант возможен, но мы выбрали реализацию со всеми слоями в рамках одного модуля и разделения на модули по ограниченным контекстам. Подробнее о деталях нашей реализации можете почитать в этой статье)
Реализация всех слоёв в рамках модуля позволяет нам легко вынести контекст в отдельный микросервис и изолирует код контекстов. Используя deptrac мы настроили правила, по которым модули друг от друга не зависят ни на одном из уровней, кроме anti corruption layer.
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 использует рефлексию для восстановления объектов. Потому что в рамках конструкторов и сеттеров возможны бизнес правила, проверки, выброс исключений, а при восстановлении проверять ничего не надо.
"ORM как раз ничего не создает, а восстанавливает из хранилища в соответствии с persistance ignorance." - тут всё верно. При восстановлении агрегата из базы конструктор не вызывается. Это особо важно в случаях, когда есть определённые бизнес правила создания агрегата. Потому за всё время жизни он и правда создаётся один раз. В примере с заказами - заказ создан один раз, во всех остальных use case он восстанавливается из базы в текущем состоянии.
При этом использовать конструктор для восстановления можно, но надо рассматривать каждый конкретный случай. Лучше ввести единый подход по восстановлению без конструктора, потому как обязанности создания и восстановления отличаются.
"Архитектурная ошибка где-то здесь, получилась жёсткая связь". Жёсткой связи между ORM и доменом нет, как создавать агрегат решается в домене, а вот как его восстановить - это отдельный вопрос вопрос. В рамках ORM может быть прописан маппинг на базу (инфраструктурный код), в рамках go сервисов мы пробовали в домен добавлять специальный метод "восстановления" с набором параметров, мапающихся на состояние агрегата. При этом в обоих случаях надо будет вызывать дополнительно инъекцию диспатчера, если он будет полем агрегата. И вот этого как раз делать не хочется.
Интеграционные события у нас не обрабатываются синхронно даже в рамках монолита. Они всегда проходят через брокер и обрабатываются асинхронно. Если же надо обновить данные модуля А на изменения модели из модуля Б синхронно, то тут обычно делается сага через оркестрацию с синхронным вызовом методов модулей А и Б и с компенсирующими действиями. Мы реализуем это либо отдельно написанной сагой, либо путём вызова из app сервиса модуля Б через anti corruption layer методов API модуля А. И тут в целом не важно, оба модуля в монолите, или один из них уже перенесён в микросервис. Но важно помнить, что синхронный вызов может увеличивать связанность модулей.
А насчёт получения данных из домена для отдачи в ответ на запрос - тут просто добавляется подписка на доменное событие и данные, полученные из него, возвращаются наружу)
Только добрался до вашего вопроса) за минусы других людей не могу отвечать)
По поводу инъекции диспатчера в модель - такой вариант имеет место быть и я его тоже рассматривал, но не использовал последующим причинам:
В первую очередь агрегат при восстановлении из базы у нас собирается в рамках монолита с помощью ORM, усложнять логику его конструирования, прокидывая актуальный dispatcher в методы восстановления агрегата, не хотелось. Наш диспатчер, ввиду multi tenant модели, инициализируется на каждый запрос с контекстом запроса и там же на него подписываются нужные handler'ы (ведь важно в рамках сервисов инициализировать, в каком tenant идёт сейчас работа и местами другие контекстные параметры). В рамках PHP монолита пришлось бы писать свои обёртки над ORM, что в последующем могло усложнить её обновление. В рамках go микросервисов мы восстанавливаем агрегаты специальными методами, данные подготавливают реализации репозиториев ручками из базы. В этом случае пришлось бы прокидывать диспатчер в репозитории. В целом, восстановление состояния агрегата тоже интересная задача со своими подводными камнями)
В момент создания агрегата тоже пришлось бы прокидывать этот же диспатчер, то есть опять не обойтись без доменного или app сервиса, который владеет ссылкой на диспатчер.
В итоге, решение было таким, что конструирование агрегата должно быть без внешних зависимостей, только на основе данных самого агрегата. Но ваш вариант тоже имеет место быть, если есть пример подобного реализованного решения, я бы с удовольствием его посмотрел)
По вопросу попадания лишних методов в модель - это некоторый мой личный пуризм) Идея в том, что предметная модель отвечает за свои инварианты, наличие у неё метода "получить события" с точки зрения экспертов предметной области непонятно)
По поводу дублирования методов агрегата в сервисе - это тот самый компромисс, который был принят, в итоге данное решение зафиксировано у нас на уровне архитектурного стандарта) Плюс, похожий подход описывается в книге DDD in PHP. В рамках статьи у меня не было намерения показать, что выбранные мной методы единственно верные, хотелось поделиться опытом, поскольку в целом вопрос с доменными событиями мне интересен, а в процессе реализации возникло много нюансов, и ответы на возникающие вопросы приходилось искать по нескольким книжкам по DDD, сайту microsoft и различным статьям) В итоге собрал различные нюансы по событиям в рамках статьи)
У нас на каждый микросервис своя база данных. У монолита тоже отдельная база, с таблицами под каждый модуль и правилом не читать из "чужих" таблиц.
Ивенты в базе хранятся для того, чтобы быть сохранёнными в одной транзакции с агрегатами. После того, как транзакция закрывается и мы гарантированно сохранили изменения в модели, отдельная горутина (либо этот же скрипт PHP по событию закрытия транзакции) отправляет сообщения в брокер. То есть мы гарантированно сохранили изменения в базу и гарантированно, но с некоторой задержкой, отправили события в message bus. Про данный паттерн можно подробнее почитать тут - https://microservices.io/patterns/data/transactional-outbox.html Конечно же в рамках реализации есть ряд интересных моментов)
Если не ответил, пожалуйста, опишите подробнее проблему)
При выборе решения меня во втором варианте смутил лишний интерфейс у доменных объектов и необходимость либо ручками писать для каждой модели работу с коллекцией событий, либо использовать trait. Потому выбрал доменные сервисы) Когда выбирал реализацию, смотрел варианты в книжке DDD in PHP, рекомендую :) По мапингу в ОРМ - для модульного монолита придётся для каждого агрегата делать отдельный event store. Я решил хранить в одной таблице все события монолита, в каждом микросервисе тоже поднимать одну таблицу событий на микросервис.
Все варианты имеют свои плюсы и минусы. В итоге в любом случае придётся идти на некоторые компромиссы) Ваше вариант имеет отличный плюс - модель сама отвечает за всю доменную логику, включая отправку событий)
Отлично написано! Круто, что не слово в слово с доклада, интересно читать! А файл для deptrac с последней версии взят? Там вроде немного формат регулярок изменился
Не было в планах, но могу помочь с организацией ;) пока из-за карантина оффлайна почти нет, не считая beer php/go митапов :) в телеграмм есть канал всех проводимых митапов в йо, про которые я знаю — it_yola :)
Интерфейс находится на уровне домена, чтобы оттуда выбрасывать доменные события.
Для обработки события в этом же контексте в другом агрегате используется уровень приложения, именно там происходит "подписывание" агрегатов на разные события.
Можно ли это сделать только на уровне домена - думаю да. Но появится код, который должен будет заниматься подпиской при инициализации/создании агрегатов на доменные события :) если на вашем языке/фреймворке это можно сделать красиво - ок :)
Отличная получилась футболка! 🍻
Думаю позже выложу в отдельной статье :) пока дам время решить тем, кому интересно :) если актуально решающим, могу выкладывать подсказки раз в неделю, как и планировал изначально :)
Поздравляю! Шифр с третьей футболки верно разгадан! :) сейчас с вами свяжемся по размерам :)
Похоже вы на верном пути :) Но ответ неправильный(
Да, всего подсказок было 6, причём две самые большие и выданные коллегам последними были в видео, ссылка на которое в статье :) итого статья содержит 3 из 6 подсказок. Коллегам в момент вручения футболок я озвучил только 1 подсказку про надпись на футболках :)
Поздравляю! Шифр со второй футболки верно разгадан! :) сейчас с вами в лс свяжемся вопросам доставки и размеров :)
Поздравляю! Ответ верный :) сегодня с вами свяжемся вопросам доставки и размеров!
Три разных сообщения)
В статье картинки в исходном разрешении, для разгадки этого разрешения достаточно :)
На вопрос "откуда" - одного конкретного источника не укажу)
Если смотреть с точки зрения микросервисной архитектуры - то каждый микросервис реализует свой небольшой ограниченный контекст, и в каждом микросервисе реализованы все слои, не только доменный. В рамках монолита с несколькими ограниченными контекстами ваш вариант возможен, но мы выбрали реализацию со всеми слоями в рамках одного модуля и разделения на модули по ограниченным контекстам. Подробнее о деталях нашей реализации можете почитать в этой статье)
Реализация всех слоёв в рамках модуля позволяет нам легко вынести контекст в отдельный микросервис и изолирует код контекстов. Используя deptrac мы настроили правила, по которым модули друг от друга не зависят ни на одном из уровней, кроме anti corruption layer.
На примере используемой нами Doctrine: https://www.doctrine-project.org/projects/doctrine-orm/en/2.9/tutorials/getting-started.html
Doctrine использует рефлексию для восстановления объектов. Потому что в рамках конструкторов и сеттеров возможны бизнес правила, проверки, выброс исключений, а при восстановлении проверять ничего не надо.
"ORM как раз ничего не создает, а восстанавливает из хранилища в соответствии с persistance ignorance." - тут всё верно. При восстановлении агрегата из базы конструктор не вызывается. Это особо важно в случаях, когда есть определённые бизнес правила создания агрегата. Потому за всё время жизни он и правда создаётся один раз. В примере с заказами - заказ создан один раз, во всех остальных use case он восстанавливается из базы в текущем состоянии.
При этом использовать конструктор для восстановления можно, но надо рассматривать каждый конкретный случай. Лучше ввести единый подход по восстановлению без конструктора, потому как обязанности создания и восстановления отличаются.
"Архитектурная ошибка где-то здесь, получилась жёсткая связь". Жёсткой связи между ORM и доменом нет, как создавать агрегат решается в домене, а вот как его восстановить - это отдельный вопрос вопрос. В рамках ORM может быть прописан маппинг на базу (инфраструктурный код), в рамках go сервисов мы пробовали в домен добавлять специальный метод "восстановления" с набором параметров, мапающихся на состояние агрегата. При этом в обоих случаях надо будет вызывать дополнительно инъекцию диспатчера, если он будет полем агрегата. И вот этого как раз делать не хочется.
Интеграционные события у нас не обрабатываются синхронно даже в рамках монолита. Они всегда проходят через брокер и обрабатываются асинхронно. Если же надо обновить данные модуля А на изменения модели из модуля Б синхронно, то тут обычно делается сага через оркестрацию с синхронным вызовом методов модулей А и Б и с компенсирующими действиями. Мы реализуем это либо отдельно написанной сагой, либо путём вызова из app сервиса модуля Б через anti corruption layer методов API модуля А. И тут в целом не важно, оба модуля в монолите, или один из них уже перенесён в микросервис. Но важно помнить, что синхронный вызов может увеличивать связанность модулей.
А насчёт получения данных из домена для отдачи в ответ на запрос - тут просто добавляется подписка на доменное событие и данные, полученные из него, возвращаются наружу)
Только добрался до вашего вопроса) за минусы других людей не могу отвечать)
По поводу инъекции диспатчера в модель - такой вариант имеет место быть и я его тоже рассматривал, но не использовал последующим причинам:
В первую очередь агрегат при восстановлении из базы у нас собирается в рамках монолита с помощью ORM, усложнять логику его конструирования, прокидывая актуальный dispatcher в методы восстановления агрегата, не хотелось. Наш диспатчер, ввиду multi tenant модели, инициализируется на каждый запрос с контекстом запроса и там же на него подписываются нужные handler'ы (ведь важно в рамках сервисов инициализировать, в каком tenant идёт сейчас работа и местами другие контекстные параметры). В рамках PHP монолита пришлось бы писать свои обёртки над ORM, что в последующем могло усложнить её обновление. В рамках go микросервисов мы восстанавливаем агрегаты специальными методами, данные подготавливают реализации репозиториев ручками из базы. В этом случае пришлось бы прокидывать диспатчер в репозитории. В целом, восстановление состояния агрегата тоже интересная задача со своими подводными камнями)
В момент создания агрегата тоже пришлось бы прокидывать этот же диспатчер, то есть опять не обойтись без доменного или app сервиса, который владеет ссылкой на диспатчер.
В итоге, решение было таким, что конструирование агрегата должно быть без внешних зависимостей, только на основе данных самого агрегата. Но ваш вариант тоже имеет место быть, если есть пример подобного реализованного решения, я бы с удовольствием его посмотрел)
По вопросу попадания лишних методов в модель - это некоторый мой личный пуризм) Идея в том, что предметная модель отвечает за свои инварианты, наличие у неё метода "получить события" с точки зрения экспертов предметной области непонятно)
По поводу дублирования методов агрегата в сервисе - это тот самый компромисс, который был принят, в итоге данное решение зафиксировано у нас на уровне архитектурного стандарта) Плюс, похожий подход описывается в книге DDD in PHP. В рамках статьи у меня не было намерения показать, что выбранные мной методы единственно верные, хотелось поделиться опытом, поскольку в целом вопрос с доменными событиями мне интересен, а в процессе реализации возникло много нюансов, и ответы на возникающие вопросы приходилось искать по нескольким книжкам по DDD, сайту microsoft и различным статьям) В итоге собрал различные нюансы по событиям в рамках статьи)
Не планировали, но идея хорошая, спасибо) Если доработок для обобщения будет не много, то закинем вот сюда - https://github.com/ispringtech
У нас на каждый микросервис своя база данных. У монолита тоже отдельная база, с таблицами под каждый модуль и правилом не читать из "чужих" таблиц.
Ивенты в базе хранятся для того, чтобы быть сохранёнными в одной транзакции с агрегатами. После того, как транзакция закрывается и мы гарантированно сохранили изменения в модели, отдельная горутина (либо этот же скрипт PHP по событию закрытия транзакции) отправляет сообщения в брокер. То есть мы гарантированно сохранили изменения в базу и гарантированно, но с некоторой задержкой, отправили события в message bus. Про данный паттерн можно подробнее почитать тут - https://microservices.io/patterns/data/transactional-outbox.html Конечно же в рамках реализации есть ряд интересных моментов)
Если не ответил, пожалуйста, опишите подробнее проблему)
При выборе решения меня во втором варианте смутил лишний интерфейс у доменных объектов и необходимость либо ручками писать для каждой модели работу с коллекцией событий, либо использовать trait. Потому выбрал доменные сервисы) Когда выбирал реализацию, смотрел варианты в книжке DDD in PHP, рекомендую :) По мапингу в ОРМ - для модульного монолита придётся для каждого агрегата делать отдельный event store. Я решил хранить в одной таблице все события монолита, в каждом микросервисе тоже поднимать одну таблицу событий на микросервис.
Все варианты имеют свои плюсы и минусы. В итоге в любом случае придётся идти на некоторые компромиссы) Ваше вариант имеет отличный плюс - модель сама отвечает за всю доменную логику, включая отправку событий)
Отлично написано! Круто, что не слово в слово с доклада, интересно читать! А файл для deptrac с последней версии взят? Там вроде немного формат регулярок изменился
Не было в планах, но могу помочь с организацией ;) пока из-за карантина оффлайна почти нет, не считая beer php/go митапов :) в телеграмм есть канал всех проводимых митапов в йо, про которые я знаю — it_yola :)