Получение дополнительных данных для объекта домена

У меня есть агрегат домена, назовите его «Заказ», который содержит список строк заказа. Заказ отслеживает сумму Суммы в строках заказа. У клиента есть текущий «кредитный» баланс, который он может использовать для заказа, который рассчитывается путем суммирования истории транзакций их базы данных. Как только они израсходуют все деньги в «пуле», они не могут больше заказывать продукты.

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

Вопрос в том, с точки зрения DDD, как мне получить эту сумму, если я не хочу загрязнять свой уровень домена проблемами DataContext (здесь используется L2S). Поскольку я не могу просто запросить базу данных из домена, как мне получить эти данные, чтобы проверить бизнес-правило?

Это тот случай, когда используются события домена?


person jlembke    schedule 07.08.2009    source источник


Ответы (3)


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

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

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

Например:

public interface IOrder
{
  IList<LineItem> LineItems { get; }
  // ... other core order "stuff"
}

public interface IAddItemsToOrder: IOrder
{
  void AddItem( LineItem item );
}

public interface IOrderRepository
{
  T Get<T>( int orderId ) where T: IOrder;
}

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

public class CartService
{
  public void AddItemToOrder( int orderId, LineItem item )
  {
    var order = orderRepository.Get<IAddItemsToOrder>( orderId );
    order.AddItem( item );
  }
}

Затем вашему классу Order, который реализует IAddItemsToOrder, нужен объект клиента, чтобы он мог проверить кредитный баланс. Таким образом, вы просто каскадируете ту же технику, определяя конкретный интерфейс. Репозиторий заказов может вызывать репозиторий клиентов, чтобы вернуть объект клиента, который выполняет эту роль, и добавить его в агрегат заказов.

Таким образом, у вас будет базовый ICustomer интерфейс, а затем явная роль в виде ICustomerCreditBalance интерфейса, который происходит от него. ICustomerCreditBalance действует как интерфейс маркера для вашего репозитория клиентов, чтобы сообщить ему, для чего вам нужен клиент, чтобы он мог создать соответствующий объект клиента, и у него есть методы и / или свойства для поддержки конкретной роли. Что-то вроде:

public interface ICustomer
{
  string Name { get; }
  // core customer stuff
}

public interface ICustomerCreditBalance: ICustomer
{
  public decimal CreditBalance { get; }
}

public interface ICustomerRepository
{
  T Get<T>( int customerId ) where T: ICustomer;
}

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

Обратите внимание, что в этом случае я поместил свойство CreditBalance в интерфейс ICustomerCreditBalance. Однако с таким же успехом он может быть на базовом ICustomer интерфейсе, а ICustomerCreditBalance тогда станет пустым интерфейсом «маркера», чтобы репозиторий знал, что вы собираетесь запрашивать кредитный баланс. Все дело в том, чтобы репозиторий знал, какую роль вы хотите получить для объекта, который он возвращает.

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

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

person Mike Scott    schedule 06.09.2009
comment
Майк, это отличный ответ с точки зрения совокупного дизайна. Если у меня будет возможность в следующем спринте, я постараюсь провести рефакторинг до того, что есть у вас здесь. Я решил эту проблему, используя события домена, аналогичные недавней статье Уди Дахана [udidahan.com/2009/06/14/domain-events-salvation/]. Когда запускается событие, означающее добавление строки, обработчик событий проверяет баланс и устанавливает внутреннее свойство для агрегата. Теперь бизнес-правило проверяет это свойство и сообщает клиенту об успехе или неудаче. - person jlembke; 07.09.2009
comment
Я думаю, что события домена следует использовать для уведомления в своего рода режиме «выстрелил и забыл». Мотивация для их наличия - разрешить уведомление / обратный вызов без привязки одной стороны к другой. Ваше решение с доменными событиями вполне жизнеспособно, но оно использует их как своего рода независимый вызов удаленной процедуры. Я думаю, что лучше зарезервировать события домена для одностороннего уведомления, используя метафору обмена сообщениями. Это значительно помогает масштабируемости - вы можете реализовать их, например, с помощью служебной шины. - person Mike Scott; 07.09.2009
comment
Я забыл сказать: предыдущие две статьи Уди о событиях в домене (udidahan.com/2008/02/29/) также ответьте на ваш вопрос - он использует события домена, чтобы сигнализировать об ошибке проверки и технику, которую я здесь рекомендую для проведения внешних проверок. Таким образом, вы получаете 2 по цене 1 в статьях о событиях домена Udi ;-) - person Mike Scott; 07.09.2009
comment
@Mike Scott: Так вы проверяете T в Repository.Get ‹T›, чтобы определить подходящую функциональность? Если так ... это, наряду с пустыми интерфейсами маркеров, меня немного смущает. Мне действительно нравится эта концепция. Однако я могу реализовать это по-другому. Дает мне пищу для размышлений. Спасибо! - person Kevin Swiber; 08.09.2009
comment
Что вас беспокоит по поводу пустых интерфейсов? Вам не нужно делать явную проверку типа. Вместо этого я создаю общий интерфейс для заданного поведения, например для получения стратегий у меня есть IFetchingStrategy ‹T›, а затем у меня есть класс, который его реализует, например CustomerCreditBalanceFetchingStrategy: IFetchingStrategy ‹ICustomerCreditBalance›, который предоставляет информацию, необходимую для управления тем, что загружается быстро или медленно и т. Д. Репозиторий может получить это различными способами, например запрос IFetchingStrategy ‹ICustomerCreditBalance› из контейнера IoC или локатора службы. - person Mike Scott; 10.09.2009
comment
Все это основано на работе Уди Дахана. Эта слайд-презентация раскрывает суть этого: cid -c8ad44874742a74d.skydrive.live.com/self.aspx/Blog/. Также udidahan.com/2007/04/23/fetching-strategy- дизайн - person Mike Scott; 10.09.2009

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

У вашего класса Order будет Predicate<T>, который используется для определения того, достаточно ли велика кредитная линия клиента для обработки строки заказа.

public class Order
{
    public Predicate<decimal> CanAddOrderLine;

    // more Order class stuff here...

    public void AddOrderLine(OrderLine orderLine)
    {
        if (CanAddOrderLine(orderLine.Amount))
        {
            OrderLines.Add(orderLine);
            Console.WriteLine("Added {0}", orderLine.Amount);
        }
        else
        {
            Console.WriteLine(
                "Cannot add order.  Customer credit line too small.");
        }
    }
}

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

// App code.
var customerService = new CustomerService();
var customer = new Customer();
var order = new Order();
order.CanAddOrderLine = 
    amount => customerService.GetAvailableCredit(customer) >= amount;

order.AddOrderLine(new OrderLine { Amount = 5m });
customerService.DecrementCredit(5m);

Несомненно, ваш реальный сценарий будет сложнее этого. Вы также можете проверить делегата Func<T>. Делегат или событие могут быть полезны для уменьшения суммы кредита после размещения строки заказа или активации некоторых функций, если клиент превышает свой кредитный лимит в заказе.

Удачи!

person Kevin Swiber    schedule 07.08.2009
comment
Мне нравится, что. Что я сейчас делаю, так это встраиваю сервис в объект домена, чтобы он не знал, откуда он получает сумму. По концепции это похоже на то, что вы делаете здесь. Я попробую это и посмотрю, как мне это понравится. Спасибо, Кевин! - person jlembke; 08.08.2009
comment
Ничего страшного, если это сработает для вас. Альтернативой является сохранение вашей модели домена на более низком уровне, чем доменные службы. Это похоже на лучшее разделение проблем и помогает с тестированием. Мне очень нравится использовать луковую архитектуру Джеффри Палермо. Вам стоит это увидеть. jeffreypalermo.com/blog/the-onion-architecture-part-1 - person Kevin Swiber; 08.08.2009
comment
Сущности предметной области должны быть в состоянии выполнять эти бизнес-требования, не требуя специальной инъекции интерфейсов, предикатов и т. Д. Ключ состоит в том, чтобы ваши репозитории возвращали объекты, специально предназначенные для конкретной роли, для которой они необходимы в данный момент. Это полностью освобождает код вашей службы и приложения от необходимости знать что-либо о логике домена, которая полностью инкапсулирована в сущности вашего домена. Таким образом, ваш код становится более ясным по своему назначению, без груза грязного багажа. - person Mike Scott; 06.09.2009
comment
@Kevin, Domain Events (спасибо Udi Dahan) вытащили меня из ситуации с введенным сервисом. Красиво работает. Разделение задач в стиле DDD / Onion Architecture вызвало у меня дискомфорт при внедрении сервиса. - person jlembke; 07.09.2009
comment
@jlembke: Уди - гений. Похоже, подходит! Вы должны опубликовать свое решение и ответить на свой вопрос. - person Kevin Swiber; 08.09.2009

Помимо проблемы получения значения «пула» (где я бы запросил значение с помощью метода в OrderRepository), учли ли вы последствия блокировки для этой проблемы?

Если «пул» постоянно меняется, есть ли шанс, что кто-то другой транзакции проникнет сразу после того, как ваше правило пройдет, но незадолго до того, как вы зафиксируете свои изменения в базе данных?

Эрик Эванс обращается к этой самой проблеме в главе 6 своей книги («Агрегаты»).

person Vijay Patel    schedule 11.08.2009
comment
Виджей, вы на 100% правы. Это следующая проблема, из-за которой мы теряем сон. Я пропустил эту часть книги. Я прочитаю это сейчас !! - person jlembke; 07.09.2009