Как стать автором
Обновить

Комментарии 19

Приходилось ли вам в коде видеть что-то подобное?

Приходилось

Надеюсь, что данная статья сможет внести свой небольшой вклад и прояснить некоторые моменты.

Хотелось все таки увидеть практические примеры как сделать пример лучше.

У меня складывается ощущение, что это достигается денормализацией.
В момент формирования заказа, который уже включает подструкруту адреса, order.addressDelivery однократно заполняется из client.addressDelivery. После чего объект order может быть избавлен от такой связи с клиентом. Во всяком случае от необходимости обращаться к client с таким запросом в run-time.
http://geek-and-poke.com/geekandpoke/2013/8/20/strong-vs-weak-preferences
Но, конечно, можно напороться на одну из двух "величайших проблем программирования".

Самый простой способ — инкапсулировать цепочку типа payment.GetOrder().getAccount().getClient().getAddress() в метод getOrderAccountClientAdress() { return this.GetOrder().getAccount().getClient().getAddress(); } корня агрегата Payment (над именем метода надо подумать :) ). Мы скрываем от клиентов класса Payment его внутреннее устройство и хоть вызовы можем делать, хоть денормализацию, хоть к стороннему сервису обращаться.

Вряд ли будет так, что Payment может агрегировать сразу столько объектов и ордер и аккаунт и клиент и адрес.

Пример не я придумал ) Пускай будет Order.getAccount().getClient().getAddress()

Я к тому, что Payment в принципе не может быть агрегатом для Order скорее наоборот. Либо как независимые агрегаты.

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

payment.GetOrder().getAccount().getClient().getAddress()


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

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

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

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

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

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

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

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


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

Можно сделать lazy load, пусть ORM разруливает. Другое дело что это палка о двух концах, и легко получить проблему n+1.

Можно. Собственно это моё дефолтное разруливание таких холиваров. По дефолту делаем ленивую загрузку, рискуя только деградацией производительности вплоть до DoS, но гарантированно не ломаем инварианты агрегата. А по мере появления n+1 и подобных проблем делаем точечные решения для обхода

Это performance hack, и как любой хак он опасен тем, что о его наличии надо всегда помнить. Проблема тут не столько во внутреннем состоянии агрегата (его, в конце-концов, всегда можно пересчитать), а в событиях, которые не будут raised (как это по-русски?) при манипуляциях в обход агрегата. А события там могут быть (в том числе в будущем появиться) какие угодно.


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


Плюс частый случай, этакий антипаттерн, это когда загружают агрегат только ради того, чтобы подергать геттеры. Так делать не надо, надо делать read models.

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

Не просто геттеры, а цепочку типа payment.GetOrder().getAccount().getClient().getAddress() :(

Это уже совсем жестоко.
Я вообще стараюсь делать 0 гетеров, и если хочется добавить, прохожусь по чеклисту:


  1. То, что мы делаем — это команда или запрос?
  2. Если это запрос, почему у нас тут вообще агрегат, а не read model?
  3. Если это команда, отражающая бизнес-действие, почему ее реализация вне агрегата?
  4. Если это непонятно что, то как мы вообще дошли до жизни такой, где упустили необходимую декомпозицию?
НЛО прилетело и опубликовало эту надпись здесь
Полностью согласен. DDD это как раз то внутреннее качество продукта, которое подчас не замечают и не считают нужным тратить на это время. Плюс, чтобы слепить хоть какую-то модель нужно немного поразмышлять и начинать с бумаги и ручки, а у нас больше принято сразу прикидывать схему базы данных и писать сходу код.

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации