#domain-driven-design #aggregate #aggregateroot
#дизайн, управляемый доменом #агрегат #aggregateroot
Вопрос:
У меня есть пара вопросов, касающихся взаимосвязи между ссылками между двумя совокупными корнями в модели DDD. Обратитесь к типичной модели клиента / заказа, представленной на диаграмме ниже.
Во-первых, должны ли ссылки между фактической объектной реализацией агрегатов всегда выполняться через значения идентификаторов, а не ссылки на объекты? Например, если я хочу получить подробную информацию о клиенте заказа, мне нужно будет взять CustomerID и передать его в ICustomerRepository, чтобы получить клиента, а не настраивать объект Order для прямого возврата клиента правильно? Я в замешательстве, потому что возврат клиента напрямую, похоже, упростит написание кода для модели, и его не намного сложнее настроить, если я использую ORM, такой как NHibernate. Тем не менее, я совершенно уверен, что это нарушило бы границы между совокупными корнями / репозиториями.
Во-вторых, где и как должно быть применено каскадное отношение при удалении для двух совокупных корней? Например, скажем, я хочу, чтобы все связанные заказы удалялись при удалении клиента. ICustomerRepository.Метод DeleteCustomer () не должен ссылаться на IOrderRepostiory, не так ли? Похоже, что это нарушило бы границы между агрегатами / репозиториями? Должен ли я вместо этого иметь службу CustomerManagment, которая обрабатывает удаление клиентов и связанных с ними заказов, которые ссылались бы как на IOrderRepository, так и на ICustomerRepository? В таком случае, как я могу быть уверен, что люди знают, что для удаления клиентов нужно использовать Сервис, а не репозиторий. Это просто сводится к обучению их тому, как правильно использовать модель?
Ответ №1:
Во-первых, должны ли ссылки между агрегатами всегда выполняться через значения идентификаторов, а не фактические ссылки на объекты?
Не совсем — хотя некоторые внесли бы это изменение по соображениям производительности.
Например, если я хочу получить подробную информацию о клиенте заказа, мне нужно будет взять CustomerID и передать его в ICustomerRepository, чтобы получить клиента, а не настраивать объект Order для прямого возврата клиента правильно?
Как правило, вы моделируете 1 сторону взаимосвязи (например, Customer.Orders
или Order.Customer
) для обхода. Другой может быть извлечен из соответствующего репозитория (например, CustomerRepository.GetCustomerFor(Order)
или OrderRepository.GetOrdersFor(Customer)
).
Не означает ли это, что OrderRepository должен был бы знать что-то о том, как создать клиента? Разве это не выходит за рамки того, за что должен отвечать OrderRepository…
OrderRepository
Знал бы, как использовать ICustomerRepository.FindById(int)
. Вы можете внедрить ICustomerRepository
. Некоторым это может показаться неудобным, и они решат перенести это на уровень сервиса — но я думаю, что это излишне. Нет особой причины, по которой репозитории не могут знать друг о друге и использовать его.
Я в замешательстве, потому что возврат клиента напрямую, похоже, упростит написание кода для модели, и его не намного сложнее настроить, если я использую ORM, такой как NHibernate. Тем не менее, я совершенно уверен, что это нарушило бы границы между совокупными корнями / репозиториями.
Совокупным корням разрешено содержать ссылки на другие совокупные корни. Фактически, что угодно может содержать ссылку на совокупный корень. Совокупный корень не может содержать ссылку на неагрегатный корневой объект, который ему не принадлежит.
Например, Customer
не может содержать ссылку на OrderLines
— поскольку OrderLines
должным образом принадлежит как объект Order
корню aggregate.
Во-вторых, где и как должно быть применено каскадное отношение при удалении для двух совокупных корней?
Если (и я подчеркиваю, если, потому что это специфическое требование) это действительно вариант использования, это указывает на то, что Customer
должен быть ваш единственный совокупный корень. Однако в большинстве реальных систем мы фактически не стали бы удалять объекты, Customer
с которыми связаны Order
-мы можем деактивировать их, переместить их Order
в объединенные Customer
и т.д. — Но не удалять Order
их полностью.
При этом, хотя я не думаю, что это чистый DDD, большинство людей допустят некоторую снисходительность при следовании шаблону единицы работы, в котором вы удаляете Order
s, а затем Customer
(что привело бы к сбою, если бы Order
s все еще существовали). Вы могли бы даже поручить CustomerRepository
выполнить эту работу, если хотите (хотя я бы предпочел сделать это более явным сам). Также допустимо разрешить очистку потерянных Order
файлов позже (или нет). Здесь все зависит от варианта использования.
Должен ли я вместо этого иметь службу CustomerManagment, которая обрабатывает удаление клиентов и связанных с ними заказов, которые ссылались бы как на IOrderRepository, так и на ICustomerRepository? В таком случае, как я могу быть уверен, что люди знают, что для удаления клиентов нужно использовать Сервис, а не репозиторий. Это просто сводится к обучению их тому, как правильно использовать модель?
Я, вероятно, не стал бы использовать сервис для чего-то, что так тесно связано с репозиторием. Что касается того, как убедиться, что служба используется…вы просто не ставите public Delete
на CustomerRepository
. Или вы выдаете ошибку, если удаление Customer
приведет к потере Order
осиротевших ов.
Комментарии:
1. Спасибо! Это действительно очень помогает. Однако, скажем, одна сторона отношения добавляется к модели, как вы описываете, например, Order. Свойство клиента. Не означает ли это, что OrderRepository должен был бы знать что-то о том, как создать клиента? Разве это не выходило бы за рамки того, за что должен отвечать OrderRepository, и, таким образом, нарушало бы «Закон Деметры» ( bit.ly/smi5M ). Ответ на этот вопрос ( bit.ly/iuVq2k ) было то, что изначально заставило меня подумать, что ссылки на корни arggregate должны сохраняться только как идентификаторы, а не ссылки на объекты.
2. У меня есть вопрос, тесно связанный с заданным выше, предположим, что для одной стороны отношений я делаю что-то вроде
Customer.Orders
теперь порядок будет загружаться лениво, однако как насчет зависимостей, которые загрузил бы репозиторий в данном случае OrderRepository? Например, если OrderRepository будет вводить EventPublisher в каждый Order AR, это было бы упущено в этом сценарии3. @Sudarshan — CustomerRepository может (и должен) использовать OrderRepository для загрузки объекта Order. Таким образом, вы не дублируете логику между CustomerRepository и OrderRepository. Если Order также является совокупным корнем, то OrderRepository должен быть общедоступным, а метод FindOrdersByCustomerId внутренним. Если это не совокупный корень, то он должен быть внутренним или частным только для CustomerRepository — поскольку внешним клиентам не разрешается иметь прямую ссылку на неагрегатную сущность.
4. @Sudarshan — Да — кажется, в Google Groups они предлагают подход объекта значения идентификатора Юлиана. Хотя я не вижу смысла абстрагироваться от этого самостоятельно (возможно, мне нужно немного почитать). Подход, который вы определили, с customer.setOrders(OrderRepository. FindOrdersByCustomerId(2)) — это почти точно то, как я всегда это делал. Используйте отложенную загрузку, если это необходимо по причинам совершенствования (хотя вы должны позаботиться о том, чтобы ваши агрегаты не становились слишком большими, и сделать все ассоциации однонаправленными, чтобы избежать циклических ссылок).
5. rogeralsing.com/2009/11/08/two-flavors-of-ddd это попадает в самую точку 🙂 …. это в основном очень красиво излагает то, о чем говорилось в этом сообщении
Ответ №2:
Другим вариантом было бы иметь ValueObject, описывающий связь между заказом и ссылками клиента, VO, который будет содержать CustomerID и дополнительную информацию, которая вам может понадобиться — имя, адрес и т.д. (Что-то вроде ClientInfo или customerData).
Это имеет несколько преимуществ:
- Ваши AR отделены — и теперь могут быть разделены, сохранены в виде потоков событий и т.д.
- В ARS заказа вам обычно необходимо сохранить информацию, которая у вас была о клиенте на момент создания заказа, и не отражать в ней любые будущие изменения, внесенные в клиента.
- Почти во всех случаях информации в объекте value будет достаточно для выполнения операций чтения (отображения информации о клиенте вместе с заказом).
Для обработки удаления / деактивации клиента у вас есть свобода выбора любого поведения, которое вам нравится. Вы можете использовать DomainEvents и опубликовать событие CustomerDeleted, для которого у вас может быть обработчик, который перемещает заказы в архив, или удаляет их, или что вам нужно. Вы также можете выполнить более одной операции над этим событием.
Если по какой-либо причине DomainEvents не являются вашим выбором, вы можете реализовать операцию удаления как операцию службы, а не как операцию хранилища, и использовать UOW для выполнения операций над обоими AR.
Я видел много подобных проблем при попытке выполнить DDD, и я думаю, что источник проблем заключается в том, что разработчики / моделисты склонны мыслить в терминах DB. У вас (у нас 🙂 ) есть естественная тенденция устранять избыточность и нормализовать модель предметной области. Как только вы преодолеете это и позволите своей модели развиваться и вовлекать экспертов домена в ее эволюцию, вы увидите, что это не так сложно и вполне естественно.
ОБНОВЛЕНИЕ: и аналогичный VO — OrderInfo может быть размещен внутри клиентского AR, если это необходимо, с только необходимой информацией — итогом заказа, количеством элементов заказа и т.д.
Комментарии:
1. Да, я продолжаю хотеть, чтобы мои объекты соответствовали таблицам, и я забываю, что с DDD дизайн модели не обязательно всегда соответствует 1 к 1 базе данных. В вашем примере необходимо ли сохранять данные клиента в таблице «Заказы» в БД? Или реализация OrderRepository может получить доступ к таблице Customers для создания VO клиента? Скажем, если я не хочу сохранять информацию о клиенте на момент заказа, но всегда хочу иметь самую свежую информацию о клиенте?
2. Как правило, VO клиента хранится вместе с заказом, в таблице Orders или в отдельной таблице с идентификатором заказа FK. Если вам нужна последняя информация о клиентах, скорее всего, вам следует просмотреть свою модель, чтобы понять, зачем она вам нужна, и поискать недостающую концепцию. Если вы не можете найти недостающую концепцию, OrderRepository может прочитать ее из таблицы customer. Или у вас мог бы быть обработчик событий, который обновляет информацию о клиенте, хранящуюся в заказе, при изменении информации.
3. Убедитесь, что в заказе должна быть последняя информация о клиенте или текущая информация на момент заказа — это действительно вопрос дизайна. Но прав ли я, полагая, что с точки зрения DDD нормально, когда типы значений, содержащиеся в отдельных агрегатах, на самом деле хранятся в одной таблице базы данных? Было бы нормально, если бы Customer. Адреса и порядок. ShippingAddress , оба типа значений в модели, фактически хранились в одной таблице адресов? Скажем, потому что в другом контексте может возникнуть необходимость искать все заказы или клиентов по определенному адресу?
4. Думая об этом, я бы сказал, что нет, это не нормально. Причина в том, что ЛЮБЫЕ взаимодействия и бизнес-логика должны быть явно смоделированы в вашем домене. Вы не должны ссылаться на тот факт, что объекты сохраняются в одной таблице. Имейте в виду тот факт, что Customer. Адрес и порядок. ShippingAddress ДОЛЖЕН иметь то же значение, явно смоделированное в домене. Другая проблема заключается в том, что VOS должен (почти) все время быть неизменяемым. Поэтому, если клиент изменяет свой адрес, вам следует добавить новую запись в эту таблицу — и будет непросто заставить заказ ссылаться на новую запись.