Здравствуйте, samius, Вы писали:
S>Бизнес логики я в этом примере не увидел. Хотелось посмотреть на чуть более сложном примере, где взаимодействуют покупатель, корзина товаров, набор акций на отдельные группы товаров, накопительные бонусные программы или что-то в этом роде.
S>Я не прошу приводить код, я его довольно ясно представляю, в том числе с тестами и обилием моков.
J>>Если придется работать с внешним сервисом, придется создавать DomainService и дробить бизнес логику в rich-моделях.
S>Проблема рич в том, что чем больше сценариев, тем больше придется дробить бизнес логику в рич моделях.
S>Та же валидация внутри рич моделей говорит "ой", когда внезапно выясняется, что для одних процессов должна быть одна валидация, а для других — другая.
В DDD есть такие понятия как Entity, Value Object, Aggregate, Aggregate Root.
Entity/Value Object — это объекты. Концепция эквивалентности ссылок относится к Entity (User), в то время как структурая эквивалентность — к Value Object(Contact Information).
швабра.
Aggregate: набор Entities или Value Objects, связанных друг с другом через объект Aggregate Root.
Aggregate Root: каждый агрегат имеет корень (в примере выше: User) и границу, агрегатный корень владеет агрегатом и служит шлюзом для всех модификаций внутри агрегата,
т.е. из Application Layer нельзя написать код:
var contactInformation = new ContactInformation(...);
repo.Save(contactInformaion);
Все изменения нужно делать только через агрегат рут(User):
var user = userRepository.Load(userId);
user.UpdateInformation(userInformation.Info, userInformation.Email);
userRepository.Save(user);
Бизнес логику желательно писать в самых конечных объектах (Value Objects).
В моделях валидацию можно рассматривать как инварианты. Проверка того что объекты всегда в правильном состоянии,
весь остальной код в моделях относится только к бизнес логике.
Валидация типа
if(repo.GetUserById(id) != null)
{
throw new NotFoundException();
}
будет в Application Layer-e.
Бизнес логика — это все что говорит заказчик. Сначала определяется уникальный язык(термины) которые заказчик/менеджеры/бизнес аналитик/девы будут использовать чтобы понимать друг друга.
Потом договаривают что напр. "Хотим изменить `Info` и `email` у пользователя".
Т.к. это относится к сущности User, в моделе User создается метод `UpdateInformation(string info, string email)`, который является прокси методом к бизнес логике,
которая будет писаться во вложеных Entitites и Value Objects.
--
Покупатель, корзина товаров, набор акций — это все будут разные агрегаты/агрегат руты.
Для взаимодействия агрегатов, что бы проще было расширять бизнес логику и меньше было локов при сохранении данных в бд, в DDD есть понятие как DomainEvents.
msdn
Напр. у нас есть агрегат руты: User, Order, Product. Пользователь делает заказ.
В модель `Order` при создании будет передается 'userId' (Id агрегат рута пользователя) и кидается event создания заказа.
После мы можем добавить товар или изменить статус, при изменении статуса будет кидаться другой event.
| | код |
| | полный пример
public class Order
{
// ..
public Order(string userId, string userName, Address address, int cardTypeId, string cardNumber, string cardSecurityNumber,
string cardHolderName, DateTime cardExpiration, int? buyerId = null, int? paymentMethodId = null) : this()
{
_buyerId = buyerId;
_paymentMethodId = paymentMethodId;
_orderStatusId = OrderStatus.Submitted.Id;
_orderDate = DateTime.UtcNow;
Address = address;
// Add the OrderStarterDomainEvent to the domain events collection
// to be raised/dispatched when comitting changes into the Database [ After DbContext.SaveChanges() ]
AddOrderStartedDomainEvent(userId, userName, cardTypeId, cardNumber,
cardSecurityNumber, cardHolderName, cardExpiration);
}
public void AddOrderItem(int productId, string productName, decimal unitPrice, decimal discount, int units = 1)
{
var existingOrderForProduct = _orderItems.Where(o => o.ProductId == productId)
.SingleOrDefault();
if (existingOrderForProduct != null)
{
//if previous line exist modify it with higher discount and units..
if (discount > existingOrderForProduct.GetCurrentDiscount())
{
existingOrderForProduct.SetNewDiscount(discount);
}
existingOrderForProduct.AddUnits(units);
}
else
{
//add validated new order item
var orderItem = new OrderItem(productId, productName, unitPrice, discount, pictureUrl, units);
_orderItems.Add(orderItem);
}
}
// ...
public void SetPaidStatus()
{
if (_orderStatusId == OrderStatus.StockConfirmed.Id)
{
AddDomainEvent(new OrderStatusChangedToPaidDomainEvent(Id, OrderItems));
_orderStatusId = OrderStatus.Paid.Id;
_description = "The payment was performed at a simulated \"American Bank checking bank account ending on XX35071\"";
}
}
}
|
| | |
В ApplicationLayer-e мы можем подписаться на все эти события и на каждое событие написать свой обработчик (DomainHandler).
github
Из примеров проектов которые можно посмотреть:
https://github.com/aspnetboilerplate/aspnetboilerplate
https://github.com/zkavtaskin/Domain-Driven-Design-Example
https://github.com/dotnet-architecture/eShopOnContainers
и книга по ddd без воды: "Alexey Zimarev, Hands-On Domain-Driven Design with .NET Core — Tackling complexity in the heart of software by putting DDD principles into practice (2019)"