Pull to refresh

Comments 17

Спасибо! Не знал о таком подходе.

Я же правильно понимаю, что внешний агрегат нужен только в случае большего числа внешних зависимостей? Если вернуться к примеру с почтой, то там можно иньектировать репозитарий и никакого профита от внешнего агрегата нет?

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

Ну и это не внешний агрегат скорее внешний фасад который враппит другие зависимости

Стоит добавить пару слов про dependency inversion

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

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

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

  1. Агрегат имеет несколько внешних зависимостей, которые содержат методы с сайд эффектам которые он в теории может вызывать. Например вытащить из репозитория другой агрегат и изменить его состояние.

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

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

  1. Изменения интерфейса этих зависимостей не контролируется агрегатом, и может потребовать изменения внутренней логики агрегата.

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

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

Это не так, по описанным в предыдущем пункте причинам: эти интерфейсы и так всегда проектируются исходя из потребностей и удобства их использования бизнес-логикой. И да, эти интерфейсы вполне нормально объявлять как только они понадобились для реализации бизнес-логики, в момент когда ещё никаких реализаций этих интерфейсов не существует.

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

Это правда, но это же не проблема, на самом-то деле!

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

Во-вторых, проще один раз нарисовать фикстуру данных "точно как получаем из внешнего мира" и использовать её в тестах всех агрегатов, чем подготавливать десяток её урезанных вариантов индивидуально для разных агрегатов.

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

Вообще-то именно так и нужно писать весь код в слое домена. До слоя инфраструктуры и не завися от него насколько это вообще возможно. Именно в этом смысл гексагональной/луковой/чистой архитектур, вне зависимости от наличия или отсутствия DDD.

  • У агрегата есть только одна внешняя зависимость максимально заточенная под потребности агрегата, интерфейс который он сам и определяет

Тут такое дело. Если разным агрегатам нужен один и тот же функционал из слоя инфраструктуры но представленный заметно разными способами, то есть высокая вероятность, что были некорректно определены границы Bounded Context. Потому что внутри одного контекста должен использоваться один Ubiquitous Language и в нём должно быть только одно название для одного и того же функционала.

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

  • Создание мока для этой зависимости намного проще чем создание моков для нескольких зависимостей которые напрямую не связанны с агрегатом.

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

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

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

Целью примера было показать как абстрагировать получение внешних данных для принятия бизнес-решения. Агрегат не может инициировать изменение состояния внешних сервисов напрямую через outside. Одной из целей outside как раз и является не позволить агрегату изменять состояние внешних сервисов. Резервация нужной суммы в банке должна происходить только после сохранения состояния агрегата в хранилище. Как правило это происходит после того как состояние агрегата записалось в хранилище и было опубликовано соответствующее доменное событие.

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

Смотря как подходить к изоляции. Мы подходим исходя из принципа Домен/Ограниченный Контекст/Агрегат. В этом случае на уровне слоя инфраструктуры мы объявляем интерфейс который являться anti-corruption layer по отношению к внешним сервисам (в данном примере к сервисам банков). Этот интерфейс описывает все методы которые требует данный ограниченный контекст и включает себя как методы для получения состояния так и для изменения. Интерфейс описывает требования всего ограниченного контекста и может использоваться несколькими агрегатами этого ограниченного контекста, каждый из которых может требовать только какое-то подмножество данных, которые возвращает данный интерфейс. И однозначно не требует ни одного метода который может изменить состояние внешнего сервиса. Кроме этого агрегату может потребоваться информация из нескольких внешних сервисов. Например для принятия решения агрегату нужны какие-то данные из банка который обслуживает клиента и из его страховой компании. Outside служит как раз для того чтобы абстрагировать эти внешние источники. Для агрегата это просто информация из внешнего мира, он не знает ничего о банках и страховых компаниях.

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

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

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

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

Классический пример из книги IDDD - метод репозитория nextIdentity(), некоторые реализации которого вполне себе может вносить изменения в БД, и при этом он может вызываться из любой фабрики, некоторые из которых вполне могут быть и методами агрегата. Но помимо него в жизни немало ситуаций, когда для принятия решения бизнес-логике необходимо сначала попытаться что-то изменить во внешней системе. Тот же курс обмена на бирже обычно зависит от текущего состояния стакана, т.е. от доступного объёма ликвидности предлагаемого с разными уровнями цены. И если нет механизма резервирования, то нет и гарантий, что обмен удастся выполнить (как по определённому курсу, так и в принципе что на него хватит ликвидности).

Можно, конечно, устроить целую сагу с доменными событиями, eventual consistency, несколькими шагами и компенсацией если следующий шаг провалился. Это будет очень DDD-шно, но ещё это будет очень больно. При этом мы имеем сторонний сервис, который предоставляет простое API: резервируем сумму для обмена, а в ответ он возвращает курс обмена. И если мы не выполним операцию обмена в течении X минут то резервирование будет автоматически отменено. И вот как рекомендуется в такой ситуации поступить? С одной стороны у нас есть простой способ выполнить требования бизнес-логики, но он требует использовать из агрегата внешее API изменяющее данные… а с другой стороны есть ограничение, что доступ к внешнему API должен быть через outside, и никакие изменяющие что-либо API через outside не должны быть доступны.



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


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

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

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

Можно, конечно, устроить целую сагу с доменными событиями, eventual consistency, несколькими шагами и компенсацией если следующий шаг провалился

Пример который я привел придуманный и сильно упрощен в целях демонстрации принципа outside а не работы банковской системы. Если перед принятием бизнес решения нужно изменить состояние внешней системы, то это изменение нужно производить за пределами агрегата, как реакцию на доменное событие. Грубо говоря, если мы можем узнать курс только как результат создания заявки в банке, то мы вначале создаем заявку в своем контексте без курса, только с информацией о банке в котором нужно сделать заявку, а запрос на создание заявки в банке шлем уже в подписчике на событие BidCreated, и в случае успеха записываем этот курс в методе Bid::confirm(float $echangeRate, DateTime $expiresAt);
если же по какой-то причине заявку в банке создать не удалось то выполняем метод Bid::cancel()

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

Любые операции между Bounded Context должны сохранять целостность всего проекта, хотя бы eventually. Так что я не очень понял суть уточнения "по отношению к агрегату" - не поясните, что Вы имели в виду?

Если нам важна целостность на уровне "каждой заявке в нашей системе должна соответствовать попытка создания заявки в банке", то мы не можем слать запрос в банк перед тем как наша заявка сохранится в базе, если не важна, то можем и тогда outside может иметь сайд эффект.

Для наглядности и более легкого понимания, было бы круто, если бы была диаграмма)

Ооо сколько разработчиков грезят о DDD и ни одного интерпрайс проекта с DDD я не встречал за последние лет 10 разработки, онион архитектура это максимум. Потому что именно богатая доменная модель, как по мне, это и есть основное ограничение методологии. В больших проектах методы моделей, изменяющие их состояние, могут быть настолько большими и сложными, что должны выносится в отдельные классы. Одинственное, что можно сделать это повыносить в отдельные валью обжекты такою логику и декомпозировать сущность, НО:
1. Встает технический вопрос персиста и накладки чтения таких сущностей
2. Модели могут разойтись с реальным бизнесом, что нарушает Ubiquitous Language

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

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

Стоит отметить, что сказанное Вами касается только тактических паттернов DDD. А вот стратегические паттерны DDD практически на 100% применяются в большинстве адекватно спроектированных микросервисных проектов, даже если их архитекты ничего не слышали про DDD и не называют свои микросервисы Bounded Context. Потому что все стратегические паттерны DDD - очень адекватные и жизненные, рано или поздно к ним приходишь в любом случае.

Но тактическая часть DDD - действительно сложная и не особо очевидная для большинства тема. И да, как уже упомянул @mike_shapovalovпорог входа в неё действительно высокий. В частности, для понимания некоторых вещей полезно знать реалии IT 20-ти летней давности, в которых это всё было рождено. Плюс нужно потратить две-три недели целиком на погружение в тему, читая основные книги и смотря доклады на конференциях. Добавим, что, в отличие от стратегической части, тактическая уместна в значительно меньшем числе проектов: практически любому микросервисному проекту пойдёт на пользу стратегия DDD, но вот в реализации большинства - если не всех! - микросервисов этого же проекта тактика DDD вполне может оказаться излишней.

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

Sign up to leave a comment.

Articles