Являются ли агрегаты DDD действительно хорошей идеей в веб-приложении?

40

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

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

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

Если я понимание Аггрегатов правильно, я обычно использую шаблон репозитория вернуть OrderAggregate , который будет содержать элементы GetAll, GetByID, Deleteи Save. ОК, это звучит хорошо. Но...

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

Я что-то пропустил? Или какой-то уровень оптимизации вы бы использовали здесь? Я не могу представить, чтобы кто-то выступал за возвращение всей совокупности информации, когда она вам не нужна.

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

Кто-нибудь может уточнить это для меня?

РЕДАКТИРОВАТЬ:

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

Фаулер определяет хранилище как хранилище данных, которое использует семантику коллекции и обычно хранится в памяти. Это означает создание целого графа объектов.

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

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

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

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

Эрик Фанкенбуш
источник
4
«Думаю, ответ в том, что это гораздо более сложная концепция, чем я сначала думал». Это очень верно.
Квентин Старин
в вашей ситуации вы можете создать прокси-сервер для совокупного корневого объекта, который выборочно извлекает и кэширует данные только тогда, когда они запрашиваются
Стивен А. Лоу
Мое предложение - реализовать ленивую загрузку в ассоциациях корневых агрегатов. Таким образом, вы можете получить список корней, не загружая слишком много объектов.
Джулиано
4
спустя почти 6 лет все еще хороший вопрос. После прочтения главы в Красной книге я бы сказал: не делайте свои агрегаты слишком большими. Соблазнительно выбрать какую-то концепцию верхнего уровня в вашем домене и объявить ее «рут-править ими». Все, кроме DDD, выступают за меньшие агрегаты. И уменьшает неэффективность, подобную той, что вы описали выше.
Cpt. Senkfuss
Ваши агрегаты должны быть как можно меньше, при этом быть естественными и эффективными для домена (задача!). Кроме того, он прекрасно подходит и желателен для вашей РЕПО иметь весьма специфические методы.
Тимо

Ответы:

30

Не используйте вашу модель домена и агрегаты для запросов.

На самом деле, то, что вы спрашиваете, является достаточно распространенным вопросом, чтобы был создан набор принципов и шаблонов, чтобы избежать именно этого. Это называется CQRS .

Квентин-starin
источник
2
@Mystere Man: Нет, это для обеспечения минимальных необходимых данных. Это одна из больших целей отдельной модели чтения. Это также помогает решить некоторые проблемы параллелизма. CQRS имеет несколько преимуществ при применении к DDD.
Квентин-старин
2
@Mystere: Я очень удивлен, что вы пропустите это, если прочитаете статью, на которую я ссылаюсь. См. Раздел «Запросы (отчеты)»: «Когда приложение запрашивает данные ... это должно быть сделано за один вызов уровня Query, а взамен он получит один DTO, содержащий все необходимые данные.» Причина этого в том, что данные обычно запрашиваются во много раз больше, чем выполняется поведение домена, поэтому, оптимизируя это, вы улучшите воспринимаемую производительность системы ».
Квентин Старин
4
Вот почему мы не будем использовать репозиторий для чтения данных в системе CQRS. Мы бы написали простой класс для инкапсуляции запроса (используя любую технологию, которая была бы удобна или необходима, часто это прямо ADO.Net, Linq2Sql или SubSonic, что хорошо подходит здесь), возвращая только (все) данные, необходимые для поставленной задачи, чтобы избежать перетаскивание данных через все нормальные слои репозитория DDD. Мы будем использовать репозиторий только для извлечения агрегата, если мы хотим отправить команду в домен.
Квентин-старин
9
«Я не могу себе представить, чтобы кто-то выступал за возвращение всей совокупности информации, когда она вам не нужна». Я пытаюсь сказать, что вы абсолютно правы с этим утверждением. Не извлекайте всю совокупность информации, когда она вам не нужна. Это самое ядро ​​CQRS, применяемое к DDD. Вам не нужен агрегат для запроса. Получите данные с помощью другого механизма, а затем делайте это последовательно.
Квентин Старин
2
@qes Действительно, лучшее решение - не использовать DDD для запросов (читай) :) Но вы все равно используете DDD в командной части, то есть для хранения или обновления данных. Итак, у меня к вам вопрос, всегда ли вы используете репозитории с сущностями, когда вам нужно обновить данные в БД? Допустим, вам нужно изменить только одно маленькое значение в столбце (какой-то переключатель), вы все еще загружаете целый объект на уровне приложения, меняете одно значение (свойство) и затем сохраняете весь объект обратно в БД? Немного излишеств, тоже?
Андрей
8

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

  1. Хранилище должно быть простым; он отвечает только за хранение доменных объектов и их получение. Вся остальная логика должна быть в других объектах, таких как фабрики и доменные службы.

  2. Хранилище ведет себя как коллекция, как если бы она была в коллекции совокупных корней в памяти.

  3. Репозиторий не является универсальным DAO, каждый репозиторий имеет свой уникальный и узкий интерфейс. Хранилище часто имеет специальные методы поиска, которые позволяют вам искать коллекцию в терминах домена (например: дать мне все открытые ордера для пользователя X). Сам репозиторий может быть реализован с помощью универсального DAO.

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

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

Kdeveloper
источник
Это определенно сложный предмет наверняка. Трудно превратить теорию в практику, особенно когда она объединяет две разные теории в одну практику.
Себастьян Паттен
6

Я не думаю, что ваш метод GetOrderHeaders побеждает цель хранилища вообще.

DDD занимается (помимо прочего) обеспечением того, чтобы вы получали то, что вам нужно, через совокупный корень (например, у вас не было бы OrderDetailsRepository), но он не ограничивает вас в способе упоминания.

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

Эрик Кинг
источник
Возможно, я путаю понятия здесь, но мое понимание шаблона репозитория состоит в том, чтобы отделить постоянство от домена, используя стандартный интерфейс для постоянства. Если вам нужно добавить пользовательские методы для конкретной функции, это, кажется, снова связывает вещи.
Эрик Фанкенбуш
1
Постоянство механизм отсоединяется от домена, но не то , что в настоящее время сохраняется. Если вы обнаружите, что говорите что-то вроде «нам нужно перечислить здесь заголовки заказов», то вам нужно смоделировать OrderHeader в вашем домене и предоставить способ извлечь их из вашего хранилища.
Эрик Кинг,
Кроме того, не зацикливайтесь на «стандартном интерфейсе для постоянства». Нет такого понятия, как универсальный шаблон репозитория, который будет достаточен для всех возможных приложений. Каждое приложение будет иметь много методов репозитория, кроме стандартных «GetById», «Save» и т. Д. Эти методы являются отправной точкой, а не конечной точкой.
Эрик Кинг,
4

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

  • Агрегированный корень имеет связанный репозиторий
  • Связанный репозиторий используется только этим совокупным корнем (он не является общедоступным)
  • Хранилище может содержать запросы запросов (например, GetAllActiveOrders, GetOrderItemsForOrder)
  • Сервис предоставляет доступ к общедоступному подмножеству репозитория и другим несложным операциям (например, перевод денег с одного банковского счета на другой, LoadById, поиск / поиск, CreateEntity и т. Д.).
  • Я использую Root -> Service -> Repository stack. Сервис DDD предполагается использовать только для того, на что Entity не может ответить сам (например, LoadById, TransferMoneyFromAccountToAccount), но в реальном мире я склонен также придерживаться других связанных с CRUD сервисов (Save, Delete, Query), даже если root должен уметь «отвечать / выполнять» сами. Обратите внимание, что нет ничего плохого в том, чтобы предоставить объекту доступ к другому агрегированному корневому сервису! Однако помните, что вы не включили бы в службу (GetOrderItemsForOrder), а включили бы ее в репозиторий, чтобы агрегированный корень мог использовать ее. Обратите внимание, что сервис не должен предоставлять какие-либо открытые запросы, как это может делать хранилище.
  • Я обычно определяю хранилище абстрактно в модели предметной области (через интерфейс) и предоставляю отдельную конкретную реализацию. Я полностью определяю сервис в доменной модели, внедряемый в конкретный репозиторий для его использования.

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

В вашем примере я бы, вероятно, предоставил бы вызов репозитория, который принес бы только заголовки заказа, если бы я хотел загрузить детали по отдельному вызову. Обратите внимание, что, имея «OrderHeader», мы фактически вводим дополнительную концепцию в домен.

Майк Роули
источник
3

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

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

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

Наконец, шаблон Repository не просто коллекция, но также допускает декларативные запросы. В C # вы можете использовать LINQ в качестве объекта запроса , или большинство других O / RM также предоставляют спецификацию объекта запроса.

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

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

Надеюсь, это поможет прояснить ситуацию.

Майкл Браун
источник
0

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

Когда я делаю репозиторий, он обычно включает некоторые кэшированные запросы.

Фаулер определяет хранилище как хранилище данных, которое использует семантику коллекции и обычно хранится в памяти. Это означает создание целого графа объектов.

Храните эти репозитории в своих серверах. Они не просто передают объекты в базу данных!

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

На данный момент у вас есть два варианта.

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

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

На самом деле, как вы реализуете, это просто, и детали реализации. Если у моего крупнейшего пользователя 10 заказов, я, вероятно, захочу перейти к варианту 2. Если я говорю 10 000 заказов, тогда нужен вариант 1. В обоих вышеупомянутых случаях и во многих других случаях я хочу, чтобы хранилище скрывало детали реализации.

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

По моему опыту доменные агрегаты предлагают огромные преимущества.

  • Это огромное повторное использование кода, которое действительно работает.
  • Они упрощают код, сохраняя вашу бизнес-логику прямо на уровне ядра, вместо того, чтобы проходить через уровень инфраструктуры, чтобы заставить сервер SQL это делать.
  • Они также могут значительно ускорить ваше время отклика, уменьшив количество запросов, которые вам нужно сделать, поскольку вы можете легко их кэшировать.
  • SQL, который я пишу, часто более удобен в обслуживании, так как я часто просто спрашиваю обо всем и вычисляю на стороне сервера.
WhiteleyJ
источник