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