Применение инвариантов, охватывающих несколько агрегатов (проверка набора) в доменно-ориентированном проектировании

Чтобы проиллюстрировать проблему, мы используем простой случай: есть два агрегата - Lamp и Socket. Следующее бизнес-правило должно выполняться всегда: ни Lamp, ни Socket нельзя подключать более одного раза одновременно. Чтобы предоставить соответствующую команду, мы задумали Connector-службу с Connect(Lamp, Socket)-методом для их подключения.

Поскольку мы хотим соответствовать правилу, согласно которому одна транзакция должна включать только один агрегат, не рекомендуется устанавливать связь для обоих агрегатов в Connect-транзакции. Итак, нам нужен промежуточный агрегат, который символизирует сам Connection. Таким образом, Connect-транзакция просто создаст новый Connection с указанными компонентами. К сожалению, здесь начинаются неприятности; как мы можем обеспечить согласованность состояния подключения? Может случиться так, что многие одновременные пользователи захотят подключить одни и те же компоненты в одно и то же время, поэтому наша «проверка согласованности» не отклонит запрос. Новые Connection-агрегаты будут сохранены, потому что мы блокируем только агрегатный уровень. Система была бы непоследовательной, даже не зная об этом.

Но как мы должны установить границу наших агрегатов, чтобы обеспечить соблюдение нашего бизнес-правила? Мы могли бы представить Connections-aggregate, который собирает все активные соединения (как Connection-entity), тем самым активируя наш алгоритм блокировки, который должным образом отклонял бы повторяющиеся Connect-запросы. С другой стороны, этот подход неэффективен и не масштабируется, а также противоречит интуиции с точки зрения языка предметной области.

Вы знаете, что мне не хватает?

Изменить: Чтобы подвести итог проблемы, представьте себе совокупность User. Поскольку определение агрегата - это единица на основе транзакции, мы можем обеспечить соблюдение инвариантов, заблокировав эту единицу для каждой транзакции. Все в порядке. Но теперь возникает бизнес-правило: имя пользователя должно быть уникальным. Следовательно, мы должны каким-то образом согласовать наши совокупные границы с этим новым требованием. Если предположить, что одновременно регистрируются миллионы пользователей, это становится проблемой. Мы стараемся обеспечить этот инвариант в незаблокированном состоянии, поскольку несколько пользователей означают несколько агрегатов.

Согласно книге Эрика Эванса «Дизайн, управляемый предметной областью», следует применять конечную согласованность, как только несколько агрегатов задействованы в одной транзакции. Но так ли это на самом деле и имеет ли смысл?

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

Думаю, я вообще о чем-то забываю, но не знаю что. Мне кажется, что мне нужно что-то вроде инвариантов на Repository-уровне.


person Luca Nate Mahler    schedule 31.05.2017    source источник
comment
Вы пытались увидеть, как обстоят дела в реальной жизни? DDD касается Домена, и в этой проблеме Домен - это Реальность, в которой мы живем.   -  person Constantin Galbenu    schedule 31.05.2017
comment
Я думаю, вам следует переименовать вопрос, поскольку двунаправленные ассоциации относятся к другому объекту в DDD, а именно к объектам, которые ссылаются друг на друга. Здесь мы говорим о транзакциях и инвариантах, охватывающих несколько агрегатов (возможно, не только двух), независимо от того, имеют ли объекты двусторонние ссылки.   -  person guillaume31    schedule 31.05.2017


Ответы (3)


Мы могли бы представить агрегат Connections, который собирает все активные соединения (как объект Connection), тем самым активируя наш алгоритм блокировки, который должным образом отклонял бы повторяющиеся запросы Connect. С другой стороны, этот подход неэффективен и не масштабируется, кроме того, он противоречит интуиции с точки зрения языка предметной области.

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

Но применение этого подхода ко второму примеру заставит вас спросить себя, что представляет собой совокупность «соединения» в этом случае, то есть внутри какой области имя пользователя является уникальным. В Company? Для данного Tenant или Customer? Для всего <whatever-subdomain-youre-in>System? Найдите имя области и вот оно: Агрегировать, чтобы обеспечить неизменное уникальное имя. Тщательно выбирайте имя, и если оно еще не существует на повсеместном языке, придумайте новую концепцию с помощью эксперта в предметной области. DDD - это не только соблюдение существующих условий домена, но и возможность вводить новые, когда прорывы достигнуты.

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

person guillaume31    schedule 31.05.2017
comment
Хорошо, это хороший момент. Пример с лампами и розетками, по общему признанию, не очень полезен. Но вы бы предложили определить область, в которой они находятся, и получить из нее новый агрегат, например LightSystem, в котором размещаются соединения (возможно, как другой агрегат)? - person Luca Nate Mahler; 31.05.2017
comment
Абсолютно. Точно так же, как вы сказали в своем Q - Мы могли бы создать агрегат Connections [...] Это должен быть новый агрегат, потому что он будет обеспечивать соблюдение инвариантов, которые сохраняются в области видимости, ранее не учитываемой другими агрегаты. Это новое транзакционное пространство. - person guillaume31; 31.05.2017
comment
И нам нужно было оценить частоту транзакций и количество агрегатов. Если команды Connect и Disconnect будут вызываться очень часто и / или количество компонентов будет превышать какой-то предел, мы должны пересмотреть этот подход и, возможно, искать решение, обеспечивающее конечную согласованность. Но если оба вопроса не касаются, этот подход подходит лучше всего. Что бы это изменилось, если бы мы хотели придерживаться согласованности в конечном итоге? Не могли бы мы исключить агрегат соединений и определить агрегат соединений (с несколькими экземплярами), который впоследствии обрабатывает разногласия (например, на основе событий)? - person Luca Nate Mahler; 31.05.2017
comment
Да, конечная согласованность часто принимает форму внеполосного процесса, что означает, что вы, вероятно, можете избавиться от предыдущей формы агрегирования. Однако указание границы, в которой выполняется инвариант (или правило, которое мы в конечном итоге компенсируем), по-прежнему имеет значение. Если вы нашли хорошее имя, возможно, вы захотите повторно использовать его как часть имени внешнего процесса. - person guillaume31; 31.05.2017
comment
Но подождите минутку. Событием с UserDirectory-aggregate мы нарушаем правило одной транзакции, включающей только агрегат, или нет? - person Luca Nate Mahler; 01.06.2017
comment
У нас вроде нет, поскольку создание нового AR не подвержено тем же проблемам параллелизма, что и изменение существующего AR. См. Также udidahan.com/2009/06/29/dont-create -агрегатные-корни - person guillaume31; 01.06.2017
comment
OK. В общем, действительно ли полезны соединения с агрегатом пула? Вы действительно могли бы поговорить с бизнес-аналитиками о соединениях, а не о простом соединении между лампой и розеткой? Вы должны были объяснить, что это необходимо для того, чтобы компоненты можно было подключить только один раз. Я думаю, что это просто не очень чисто и уместно с точки зрения предметной области. - person Luca Nate Mahler; 01.06.2017

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

Но допустим ...

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

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

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

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

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

имя пользователя должно быть уникальным

Это единственный наиболее часто задаваемый вариант задачи проверки набора.

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

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

Думаю, я вообще о чем-то забываю, но не знаю что.

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

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

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

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

Однако следует отметить, что если у вас действительно есть агрегат, вы можете масштабировать его независимо от остальной части вашей системы. Один монстр-бокс предназначен для управления установленным агрегатом и ничем другим (аналог: СУБД, предназначенная для управления одной таблицей).

Более вероятным выбором будет сегментирование по области / экземпляру / whatzit; в этом случае у вас будет меньший совокупный набор для каждого экземпляра области.

person VoiceOfUnreason    schedule 31.05.2017
comment
Большое спасибо за ответ. Вы упомянули, чтобы сравнить, как это будет в реальном мире. В частности, это касается примера Lamp-Socket, но давайте забудем об этом, потому что я построю систему, которая не отражает реальную композицию слов. Если мы планируем онлайн-игру, например, с миллионами пользователей, которые постоянно запрашивают игры, это настоящая проблема. Не могли бы вы предложить для подобных проблем разрешить несогласованность, а затем решить проблему, обсужденную с бизнес-экспертами, или создать совокупность множеств? - person Luca Nate Mahler; 31.05.2017

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

Если, однако, вы напишете команду «ChangeEmailForContact», тогда вы уже измените только это одно поле и не будете конфликтовать с изменением имени, которое аналогично будет командой «Name» или «RenameContact».

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

Теперь примените это к своей «операции по установке». В первый раз, когда лампочка перемещается в «соединение» (возможно, я бы назвал это приспособлением), она перемещается из неназначенного в connection1. Затем, когда лампочка перемещается, ее нужно переместить, скажем, с соединения 1 на соединение 2. Теперь вы можете проверить, назначена ли эта лампочка, назначена ли она для connection1 или за это время что-то изменилось.

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

person Arwin    schedule 06.06.2017