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

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

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

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

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

3 ответа

Решение

Ваш агрегат заказа должен быть полностью инкапсулирован. Следовательно, он должен быть в состоянии определить, является ли допустимым добавление элемента, т. Е. Превышен ли кредит клиента. Есть несколько способов сделать это, но все они зависят от репозитория 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 действует как интерфейс маркера для вашего репозитория Customer, сообщая ему, для чего вам нужен клиент, так что он может создавать соответствующую сущность клиента, и у него есть методы и / или свойства для поддержки конкретной роли. Что-то вроде:

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 класс, так как этот ответ уже довольно длинный! Если вы хотите узнать больше о том, как это сделать, я предлагаю вам опубликовать еще один вопрос, посвященный этой конкретной проблеме, и я подробно остановлюсь на нем;-)

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

Ваш класс заказа будет иметь 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> делегировать. Делегат или событие могут быть полезны для уменьшения суммы кредита после размещения строки заказа или запуска некоторых функций, если клиент превышает свой кредитный лимит в заказе.

Удачи!

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

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

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

Другие вопросы по тегам