DDD: сбор сущностей и репозитории

Предположим, что

public class Product: Entity
{
   public IList<Item> Items { get; set; }
}

Предположим, что я хочу найти элемент с max что-то... Я могу добавить метод Product.GetMaxItemSmth() и сделать это с Linq (from i in Items select i.smth).Max()) или с помощью ручного цикла или чего-то еще. Теперь проблема заключается в том, что это загрузит полную коллекцию в память.

Правильным решением будет выполнение конкретного запроса БД, но сущности домена не имеют доступа к репозиториям, правильно? Так что либо я делаю

productRepository.GetMaxItemSmth(product)

(что является уродливым, нет?), или даже если сущности имеют доступ к репозиториям, я использую IProductRepository из объекта

product.GetMaxItemSmth() { return Service.GetRepository<IProductRepository>().GetMaxItemSmth(); }

который также является уродливым и является дублированием кода. Я даже могу пошутить и сделать расширение

public static IList<Item> GetMaxItemSmth(this Product product)
{
   return Service.GetRepository<IProductRepository>().GetMaxItemSmth();
}

что лучше только потому, что на самом деле он не мешает сущности с репозиторием... но все равно дублирует метод.

Теперь проблема заключается в том, следует ли снова использовать Product.GetMaxItemSmth() или productRepository.GetMaxItemSmth(product).... Я что-то пропустил в DDD? Что здесь правильно? Просто используйте productRepository.GetMaxItemSmth(product)? Это то, что все используют и довольны?

Я просто не чувствую, что это правильно... если я не могу получить доступ к продукту Items из самого продукта, зачем мне эта коллекция в Product вообще? И тогда, может ли Product делать что-нибудь полезное, если он не может использовать конкретные запросы и получать доступ к своим коллекциям без хитов производительности?

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

Одна вещь, о которой стоит упомянуть, может быть, это не совсем DDD... но мне нужно IList в продукте, чтобы получить мою схему БД, сгенерированную с помощью Fluent NHibernate. Не стесняйтесь отвечать в чистом контексте DDD.

UPDATE: здесь описывается очень интересная опция: http://devlicio.us/blogs/billy_mccafferty/archive/2007/12/03/custom-collections-with-nhibernate-part-i-the-basics.aspx, а не только для обработки запросов на сборку, связанных с DB, но также может помощь с контролем доступа к коллекции.

Ответ 1

Наличие коллекции Items и методы GetXXX() являются правильными.

Чтобы быть чистым, ваши объекты не должны иметь прямой доступ к Хранилищам. Однако они могут иметь косвенную ссылку через Спецификацию запросов. Посмотрите страницу 229 книги Эрика Эванса. Что-то вроде этого:

public class Product
{
    public IList<Item> Items {get;}

    public int GetMaxItemSmth()
    {
        return new ProductItemQuerySpecifications().GetMaxSomething(this);
    }
}

public class ProductItemQuerySpecifications()
{
   public int GetMaxSomething(product)
   {
      var respository = MyContainer.Resolve<IProductRespository>();
      return respository.GetMaxSomething(product);
   }
}

Как вы получаете ссылку на репозиторий - это ваш выбор (DI, Service Locator и т.д.). Хотя это устраняет прямую ссылку между Entity и Respository, это не уменьшает LoC.

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

Ответ 2

(Отказ от ответственности, я только начинаю понимать DDD или хотя бы полагаю, что это делаю:))

Я буду отмечать Mark на этом и подчеркнуть 2 пункта, которые потребовались мне несколько раз, чтобы понять.

  • Подумайте о своем объекте с точки зрения агрегатов, что приведет к

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

Трудная часть состоит в том, чтобы подумать о совокупности вашей проблемы, а не о том, чтобы сосредоточить структуру БД, поддерживающую ее.
Пример, который подчеркивает этот момент: я customer.Orders. Вам действительно нужны все заказы вашего клиента для добавления нового заказа? обычно нет. что, если у нее есть 1 миллион из них?
Для выполнения некоторых сценариев, таких как AcceptNewOrder или ApplyCustomerCareProgram, вам может понадобиться нечто вроде OutstandingAmount или AmountBuyedLastMonth.

  • Является ли продукт настоящим совокупным корнем для вашего sceanrio?

    Что делать, если продукт не является совокупным корнем?

то есть. вы собираетесь манипулировать товаром или продуктом?
Если это продукт, вам нужен ItemWithMaxSomething или вам нужен MaxSomethingOfItemsInProduct?

  • Другой миф: PI означает, что вам не нужно думать о БД

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

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

надеюсь, что это поможет:)

Ответ 3

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

Ключом к одному ответу является анализ агрегатов и ассоциаций, как обсуждалось в Domain-Driven Design. Дело в том, что либо вы загружаете детей вместе с родителем, либо загружаете их отдельно.

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

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

Что делать, если продукт не является совокупным корнем? Ну, первое, что нужно сделать, это удалить свойство Items из Product. Затем вам понадобится какая-то Служба, которая может получить Элементы, связанные с Продуктом. Такая служба также может иметь метод GetMaxItemSmth.

Что-то вроде этого:

public class ProductService
{
    private readonly IItemRepository itemRepository;

    public ProductService (IItemRepository itemRepository)
    {
        this.itemRepository = itemRepository;
    }

    public IEnumerable<Item> GetMaxItemSmth(Product product)
    {
        var max = this.itemRepository.GetMaxItemSmth(product);
        // Do something interesting here
        return max;
    }
}

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

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

Ответ 4

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

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

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

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

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

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

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

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

Теперь, как только вы косвенные ссылки на элементы, как вы выполняете GetMaxItemSmth()?

В этом случае я считаю, что лучший способ - использовать шаблон двойной отправки. Вы создаете класс ItemProcessor:

public class ItemProcessor
{
    private readonly IItemRepository _itemRepo;
    public ItemProcessor(IItemRepository itemRepo)
    {
        _itemRepo = itemRepo;
    }

    public Item GetMaxItemSmth(Product product)
    {
        // Here you are free to implement the logic as performant as possible, or as slowly
        // as you want.

        // Slow version
        //Item maxItem = _itemRepo.GetById(product.Items[0]);
        //for(int i = 1; i < product.Items.Length; i++)
        //{
        //    Item item = _itemRepo.GetById(product.Items[i]);
        //    if(item > maxItem) maxItem = item;
        //}

        //Fast version
        Item maxItem = _itemRepo.GetMaxItemSmth();

        return maxItem;
    }
}

И соответствующий интерфейс:

public interface IItemProcessor
{
    Item GetMaxItemSmth(Product product);
}

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

Чем вы добавляете в свой объект Product:

public class Product
{
    private List<string> _items; // indirect reference to the Items Product is associated with
    public List<string> Items
    {
        get
        {
            return _items;
        }
    }

    public Product(List<string> items)
    {
        _items = items;
    }

    public Item GetMaxItemSmth(IItemProcessor itemProcessor)
    {
        return itemProcessor.GetMaxItemSmth(this);
    }
}

Примечание: Если вам нужно только запросить элементы Max и вернуть значение, а не Entity, вы должны полностью обходить этот метод. Создайте IFinder, который имеет GetMaxItemSmth, который возвращает вашу специализированную модель чтения. Хорошо иметь отдельную модель только для запросов и набор классов Finder, которые выполняют специализированные запросы для получения такой специализированной модели чтения. Как вы помните, Агрегаты существуют только для целей изменения данных. Репозитории работают только с агрегатами. Поэтому, если никакие данные не изменяются, нет необходимости в агрегатах или репозиториях.

Ответ 5

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

Ответ 6

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

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

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

Ответ 7

Теперь вы можете сделать это с NHibernate 5 напрямую без специального кода! Он не будет загружать всю коллекцию в память.

См. https://github.com/nhibernate/nhibernate-core/blob/master/releasenotes.txt

Build 5.0.0
=============================

** Highlights
...
    * Entities collections can be queried with .AsQueryable() Linq extension without being fully loaded.
...