Получение дополнительных данных для доменного объекта - PullRequest
2 голосов
/ 07 августа 2009

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

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

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

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

Ответы [ 3 ]

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

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

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

Удачи!

1 голос
/ 12 августа 2009

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

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

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

...