DDD: коллекция сущностей и хранилища - PullRequest
13 голосов
/ 31 августа 2009

Предположим, у меня есть

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

Предположим, я хочу найти элемент с максимальным значением ... Я могу добавить метод 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 в Product, чтобы генерировать мою схему БД с помощью Fluent NHibernate. Не стесняйтесь отвечать в чистом контексте DDD.

ОБНОВЛЕНИЕ: здесь описан очень интересный вариант: http://devlicio.us/blogs/billy_mccafferty/archive/2007/12/03/custom-collections-with-nhibernate-part-i-the-basics.aspx, не только для работы с запросами на сбор данных, связанных с БД, но также может помочь с контролем доступа к коллекции.

Ответы [ 7 ]

5 голосов
/ 04 сентября 2009

Имея 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 и т. Д.). Хотя это удаляет прямую ссылку между сущностью и репозиторием, оно не уменьшает LoC.

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

4 голосов
/ 28 марта 2014

Я считаю, что с точки зрения 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, которые выполняют специализированные запросы для извлечения такой специализированной модели чтения. Как вы должны помнить, агрегаты существуют только с целью изменения данных. Хранилища работают только с агрегатами. Поэтому, если данные не меняются, не требуется ни Агрегаты, ни Репозитории.

3 голосов
/ 31 августа 2009

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

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

  • Думайте о своем объекте в терминах агрегатов, которые приводят к

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

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

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

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

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

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

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

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

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

2 голосов
/ 31 августа 2009

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

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

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

Итак, чтобы ответить на один из ваших вопросов: «зачем мне вообще нужна эта коллекция в Продукте?» Может быть, нет, но если вы это сделаете, это будет означать, что элементы будут всегда загружаться при загрузке продукта. Вы можете реализовать метод 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 представляет собой довольно тонкую оболочку вокруг самого репозитория, поэтому он может быть избыточным. Однако часто оказывается, что это хорошее место для добавления другого интересного поведения, на что я пытался намекнуть своим комментарием к коду.

1 голос
/ 04 сентября 2009

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

0 голосов
/ 02 ноября 2017

Теперь вы можете сделать это с 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.
...
0 голосов
/ 03 сентября 2009

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

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

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

...