将实体更新为聚合根中集合的一部分的最佳实践



我的标题可能有点不清楚,所以我从一个示例开始。

域示例

假设我们是在电子商务领域。我们有一辆购物车和购物车里的商品。购物车成为聚合根。项目只是一个实体。购物车包含基于基础商品价格计算的总价。

添加商品后,我们仍然可以更改数量,而且必须重新计算购物车的价格。困难就在这里。我有点不确定如何在我的代码设计中最好地实现这一点。

我现在拥有的东西

购物车

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);
}
}

购物车项目

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;
}
}

我的当前代码不会触发购物车价格的重新计算

更新我的购物车项目会重新计算该特定项目,并且更改会通过存储库保存在数据库中,但我的购物篮总数是不正确的,因为那里没有执行重新计算。

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;
}
}

可能的解决方案1-添加购物车中商品的更新逻辑

public class ShoppingCart: IAggregateRoot
{
...
public void UpdateShoppingCartItemQuantity(int shoppingCartItemId, decimal quantity) 
{
var shoppingCartItem = _shoppingCartItems.First(x => x.Id == shoppingCartItemId);
shoppingCartItem.SetQuantity(quantity);
CalculateTotalPrice();
}
}

为了演示的目的,我将整个示例保持较小,但购物车项目将包含许多修改属性的方法。在ShoppingCart类中为每个方法添加另一个方法是不对的,所以我在这里不相信。

可能的解决方案2-触发重新计算

在ShoppingCart上公开CalculateTotalPrice方法。

public class ShoppingCart: IAggregateRoot
{      
public void CalculateTotalPrice()
{
TotalPrice = _shoppingCartItems.Sum(x => x.TotalPrice);
}
}

然后从命令处理程序触发计算。

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;
}
}

我觉得这也不对。如果我们遇到数字可以在几个地方更改的情况,那么我们永远不要忘记使用CalculateTotalPrice,它迟早会被忘记。

有人可以分享他们的2美分吗?谢谢

诚然,我不熟悉中DDD的惯例。Net社区,但在我看来,您的处理程序确实在聚合之外,因此不应该操纵聚合中的实体。

因此,我建议你提出的解决方案一是要走的路。更改购物车中商品的数量或价格是对购物车的操作。

上下文优先:领域驱动设计的实现模式在很大程度上是"面向对象编程做得好"——其中";"面向对象";应该在大约2003年的Java框架内理解,而不是Smalltalk。

其中一个重要的想法是,我们应该避免跨越对象边界泄露细节——参见Parnas 1971关于信息隐藏的文章。重点是我们应该能够改变";实现细节";而不需要更改已经为对象编写的客户端代码。

换言之,对象的数据结构隐藏在它所服务的合同中的门面后面。

在您的示例中,请注意,我们不需要知道shoppingCart的实现细节_项目;它可能是链表、数组列表、双链表或环。。。。谁在乎?我们向它发送消息,列表使用自己对数据结构的了解来对所有内容进行排序。

因此,您通常不会在数据结构中插入命令处理程序;相反,您可以将信息传递给对象,并让对象浏览自己的数据结构。

这可能看起来像:

var shoppingCart = await _shoppingCartRepository.GetById(request.ShoppingCartId);
shoppingCart.updateItem(request.shoppingCartItemId(), request.quantity());
await _shoppingCartRepository.Update(shoppingCart);

因此,宽泛的规则是:您的所有业务策略,以及它们所作用的数据结构的对象外观,都属于";域层";。您的应用程序代码只与作为域模型接口一部分发布的facade进行对话。

基本动机仍然是Parnas所描述的:我们正在模块之间创建防火墙,当内部细节发生变化时,这些防火墙限制了我们需要做的工作量。

最新更新