My title might be a bit unclear so I start with a sample.
Domain example
Let's say we are in the domain of eCommerce. We have a shopping cart and items in the shopping cart. The shopping cart becomes the aggregate root. The item is just an entity. The shopping cart contains a total price which is calculated based on the prices of underlying items.
After adding an item we can still change the quantity, and also the price of the cart must be recalculated. Here lies the difficulty. I'm a bit unsure about how to best approach this in my code design.
What I have now
ShoppingCart
public class ShoppingCart: IAggregateRoot
{
private List<ShoppingCartItem> _items = new();
// other properties and factory methods here
public IReadOnlyCollection<ShoppingCartItem> ShoppingCartItems => _items.AsReadOnly();
public decimal TotalPrice { get; private set; }
public void AddShoppingCartItem(ShoppingCartItem shoppingCartItem)
{
_shoppingCartItems.Add(shoppingCartItem);
CalculateTotalPrice();
}
public void RemoveAddShoppingCartItem(ShoppingCartItem _shoppingCartItems)
{
_shoppingCartItems.Remove(_shoppingCartItems);
CalculateTotalPrice();
}
private void CalculateTotalPrice()
{
TotalPrice = _shoppingCartItems.Sum(x => x.TotalPrice);
}
}
ShoppingCartItem
public class ShoppingCartItem
{
public int Id { get; private set; }
public decimal Quantity { get; private set; }
public decimal UnitPrice { get; private set; }
public decimal TotalPrice { get; private set; }
// Other properties and factory methods here
public void SetQuantity(decimal quantity)
{
Quantity = quantity;
RecalculatePrices();
}
private void RecalculatePrices()
{
TotalPrice = Quantity * UnitPrice;
}
}
My current code that doesn't trigger a recalculate of the shopping cart price
Updating my shopping cart item recalculates that specific item, and the change is persisted in the database via the repository, but my shopping cart total is incorrect since no recalculation is executed there.
public class UpdateShoppingCartItemCommandHandler : IRequestHandler<UpdateShoppingCartItemCommand>
{
private readonly IShoppingCartRepository _shoppingCartRepository;
public UpdateShoppingCartItemCommandHandler(IShoppingCartRepository shoppingCartRepository)
{
_shoppingCartRepository= shoppingCartRepository;
}
public async Task<Unit> Handle(UpdateShoppingCartItemCommand request, CancellationToken cancellationToken)
{
var shoppingCart = await _shoppingCartRepository.GetById(request.ShoppingCartId);
var shoppingCartItem = shoppingCart.ShoppingCartItems.First(x => x.Id == request.ShoppingCartItemId);
shoppingCartItem.SetQuantity(request.Model.Quantity);
await _shoppingCartRepository.Update(shoppingCart);
return Unit.Value;
}
}
Possible solution 1 - Add update logic of items in the shopping cart itself
public class ShoppingCart: IAggregateRoot
{
...
public void UpdateShoppingCartItemQuantity(int shoppingCartItemId, decimal quantity)
{
var shoppingCartItem = _shoppingCartItems.First(x => x.Id == shoppingCartItemId);
shoppingCartItem.SetQuantity(quantity);
CalculateTotalPrice();
}
}
I'm keeping the whole example small for demo purposes, but the shopping cart item will contain a lot of methods to modify properties. It doesn't feel right to add for each method another method in the ShoppingCart class, so I'm not convinced here.
Possible solution 2 - Trigger the recalculate
Make the CalculateTotalPrice method public on the ShoppingCart.
public class ShoppingCart: IAggregateRoot
{
public void CalculateTotalPrice()
{
TotalPrice = _shoppingCartItems.Sum(x => x.TotalPrice);
}
}
And then trigger the calculation from the command handler.
public class UpdateShoppingCartItemCommandHandler : IRequestHandler<UpdateShoppingCartItemCommand>
{
...
public async Task<Unit> Handle(UpdateShoppingCartItemCommand request, CancellationToken cancellationToken)
{
var shoppingCart = await _shoppingCartRepository.GetById(request.ShoppingCartId);
var shoppingCartItem = shoppingCart.ShoppingCartItems.First(x => x.Id == request.ShoppingCartItemId);
shoppingCartItem.SetQuantity(request.Model.Quantity);
shoppingCart.CalculateTotalPrice();
await _shoppingCartRepository.Update(shoppingCart);
return Unit.Value;
}
}
This also doesn't feel right to me. If we have the situation that the number can be changed in several places, then we must never forget to use the CalculateTotalPrice, which sooner or later will be forgotten.
Anyone can share their 2 cents? Thanks!
CodePudding user response:
Admittedly, I'm not familiar with what's the convention in DDD in the .Net community, but it certainly appears to me that your handler is outside the aggregate and therefore should not be manipulating entities within the aggregate.
Accordingly, I would suggest that your proposed solution one is the way to go. Changing the count or price of an item in the cart is an operation on the cart.
CodePudding user response:
Context first: the implementation patterns of domain driven design are largely "object oriented programming done right" -- where "object oriented" should be understood within the frame of Java circa 2003, rather than, say, Smalltalk.
One of the important ideas was that we should avoid leaking details across object boundaries -- see Parnas 1971 on information hiding. The point being that we should be able to change the "implementation details" of an object without requiring changes to the client code already written for that object.
In other words, the data structure of the object is hidden behind the facade that is it's role in the contract(s) it serves.
In your example, notice that we don't need to know the implementation details of shoppingCart._items; it might be a linked list, or an array list, or a doubly linked list, or a ring.... Who cares? we send it messages, and the list uses its own knowledge of its data structure to sort everything out.
So you wouldn't normally have command handlers poking around in the data structures; instead you would pass information to the object, and let the object poke around its own data structures.
That might look something like:
var shoppingCart = await _shoppingCartRepository.GetById(request.ShoppingCartId);
shoppingCart.updateItem(request.shoppingCartItemId(), request.quantity());
await _shoppingCartRepository.Update(shoppingCart);
So, broad rule: all of your business policies, and the object facades of the data structures that they act upon, belong in the "domain layer". Your application code only talks to the facades published as part of the domain model interface.
The basic motivation is still what's described by Parnas: we're creating firewalls between modules that limit the amount of work we need to do when the internal details change.