C# DDD:更改可用的购物篮项目数量
C# DDD: Change Basket Item quantity if avaiable
我尝试按照第一个 DDD 项目进行设计。我在 BasketItem 中创建了一个方法来更改我购物篮中当前商品的数量,只有在仓库中有足够的商品时才能更改数量。
我在应用程序服务中进行了此检查,但我不喜欢,因为 BasketItem.ChangeQuantity 是一个 public 方法,任何人都可以在不检查数量的情况下调用此方法。
为了防止输入不正确的数量,我想将控件移至 ChangeQuantity。这个解决方案正确吗?考虑到 SOLID,该方法会承担太多责任吗?
你推荐哪条路?
public class Basket : Entity, IAggregateRoot
{
public long BasketId { get; protected set; }
public long UserId { get; protected set; }
private List<BasketItem> _basketItems;
public virtual IReadOnlyCollection<BasketItem> BasketItems => basketItems?.ToList();
}
protected Basket() { }
public class BasketItem : Entity
{
public Guid Guid { get; protected set; }
public decimal Price { get; protected set; }
public int Quantity { get; protected set; }
public virtual Basket Basket { get; protected set; }
public virtual Product Product { get; protected set; }
protected BasketItem() { }
public IStatusGeneric ChangeQuantity(int quantity)
{
var errorStatus = new StatusGenericHandler();
//How to get avaiable quantity from WarehouseProduct?????????
//var avaiable = Basket.Warehouse.ProductQuantityAvaiable(Product);
if (avaiable < quantity)
{
var errorStatusMessage = $"There are only {avaiable} avaiable product of {Product.Code}.";
if (productQuantityInBasket > 0)
{
errorStatusMessage += $"Current basket have already {productQuantityInBasket}";
}
errorStatus.AddError(errorStatusMessage);
return errorStatus;
}
haveProduct.ChangeQuantity(quantity);
return errorStatus;
}
}
public class Warehouse : Entity, IAggregateRoot
{
public long WarehouseId { get; protected set; }
public string Name { get; protected set; }
private List<WarehouseProduct> _warehouseProducts;
public virtual IReadOnlyCollection<WarehouseProduct> WarehouseProducts => _warehouseProducts?.ToList();
}
public class WarehouseProduct : Entity
{
public Guid Guid { get; protected set; }
public int Quantity { get; protected set; }
public virtual Warehouse Warehouse { get; protected set; }
public virtual Product Product { get; protected set; }
private WarehouseProduct() { }
public void ChangeQuantity(int deltaQuantity)
{
Quantity += deltaQuantity;
}
}
第二个解决方案是将仓库带入篮球。这样我就可以访问仓库的所有元素以查找可用数量。这也可以让我将篮球与特定仓库相关联,这样我就可以管理多个仓库
public class Basket : Entity, IAggregateRoot
{
public long BasketId { get; protected set; }
public long UserId { get; protected set; }
public Warehouse Warehouse { get; protected set; }
private List<BasketItem> _basketItems;
public virtual IReadOnlyCollection<BasketItem> BasketItems => _basketItems?.ToList();
}
传递一个允许 AR 查询给定项目库存的服务如何,如 basket.changeItemQuantity(itemId, quantity, inventoryService)
?
有些人不喜欢这种风格,更喜欢在应用程序服务中解析库存,然后将其提供给 AR,例如basket.changeItemQuantity(itemId, qty, qtyOnHand)
。我更喜欢这里的第一种方法,尽管单元测试会稍微复杂一些。
第三种方法是实施域服务来编排逻辑。您将应用程序服务中当前拥有的域逻辑提取到该域服务,并将用例处理从应用程序服务委托给域服务。就这条规则而言,它可能有点矫枉过正,只会让你走向贫血模式。
关于您的设计方案的几点说明:
您不应分发非根实体引用。篮子应该提供改变数量的接口。
从篮子 AR 中保存对仓库 AR 的引用的第二个提议也将违反 AR 设计规则:AR 应仅通过 ID 引用其他 AR。这有助于保持 AR 小而集中,并让开发人员了解每个 AR 真正拥有和保护的数据。您也将不太想在同一笔交易中更改两个 AR。
从 AR 边界外引用的数据被认为是陈旧的。您可能想探索将商品添加到购物车的场景,但片刻之后才发现库存已经用完。那你怎么办呢?考虑这些场景将引导您转向更面向业务的模型,该模型可以处理此类复杂问题,而不是试图使所有内容都具有高度一致的不变性,最终导致与领域需求不一致的不切实际的模型。
好的,很好的 DDD 问题。还有一些非常有趣的讨论。
我目前正在编写一个 Shop 应用程序 - 所以这对我和我的团队来说是一个非常有用的问题。
首先我要说的是,您应该与仓库、销售和管理团队讨论这个问题,以便更好地了解场景。如果 2 个销售人员同时致电仓库,询问当前库存情况以及他们是否可以出售 - 对您的实施将产生重大影响。
技术/代码
一般性讨论
- 如果将一个项目添加到购物篮中 - 库存实际上应该减少吗?假设您有 5 件库存商品。 5 个篮子有 1 个物品;没有人可以将此商品添加到他们的购物篮中 - 如果他们真的准备购买怎么办 - 现有的购物篮必须有多陈旧?
考虑:剩余 5 件商品,1 个月前添加到购物篮的 5 件商品。这应该阻止准备购买的用户吗?
这是我的技术观点(很高兴进一步讨论)...
@plalx 有一些很棒的想法,在我看来它实际上介于两者之间。
您真的只想问一个问题 - 我可以将此数量的商品添加到此购物篮中吗...
Basket basket = _basketStore.Get(1);
basket.ChangeQuantity(item, quantity, availability);
// 定义
public interface IAvailabilityProcessor {
bool AvailableForBasket(item, quantity);
}
public class Basket {
public void ChangeQuantity (Item item, int quantity, IAvailabilityProcessor processor) {
var canBeAdded = processor.AvailableForBasket(item, quantity);
if (canBeAdded) {
this.Add(item, quantity);
}
}
我可以而且通常会花几个小时讨论这些事情的确切实现,但我现在就这样了 - 请反馈或提问
我的做法是不做。我会听从 mrdnk 的建议,询问利益相关者,并尝试很好地理解该功能背后的商业原因。
可能存在此功能未涵盖的业务影响的指标之一是竞争条件的可能性。无论您如何实施此检查,您最终都会遇到竞争条件,尤其是在工作时间(和 race conditions don't exist):您在仓库中有 1 个项目,您并行收到 100 个请求,并且 100 个用户设法将其放入篮子里的物品。或者你的仓库里有一辆法拉利,一个正在玩耍的孩子把它放在篮子里,而真正的买家却买不到它,因为没有存货。或者有库存,只有一个买家把它放在篮子里,但是仓库做库存并重新调整库存不匹配,你没有足够的物品了。清单还在继续...
我看到的另一种气味是需要从篮子中调用仓库。这些很可能在两个独立的有界上下文中,理想情况下,您不想将它们耦合得太多。
显然每个领域都是不同的,如前所述,您必须与利益相关者一起解决这个问题。他们是如何解决的"in real life"?如果这是一家现有企业,那么订单进来但没有库存的情况肯定已经发生过不止一次了。他们是怎么处理的?
在网上商店处理这种情况的一种方法是不要试图阻止客户根据库存进行购买。尤其是无论如何都不在篮筐里。想象一下亚马逊,当您将一件商品放在购物篮中几天后,即使在多个仓库中,库存也可能会发生数百次变化。
相反,您可以在产品视图和购物篮视图中向用户显示(陈旧的)库存量。例如,在 "Add to Basket" 按钮附近,您可以显示“3 件库存商品”或 "Low Stock"。您不需要将篮子与仓库耦合来实现它。 UI 可以在加载页面时直接调用仓库以获取其显示的项目的库存。
一旦最终下了订单,仓库就可以锁定该订单的商品、订购更多商品、通知客户会有延误等。
我尝试按照第一个 DDD 项目进行设计。我在 BasketItem 中创建了一个方法来更改我购物篮中当前商品的数量,只有在仓库中有足够的商品时才能更改数量。
我在应用程序服务中进行了此检查,但我不喜欢,因为 BasketItem.ChangeQuantity 是一个 public 方法,任何人都可以在不检查数量的情况下调用此方法。
为了防止输入不正确的数量,我想将控件移至 ChangeQuantity。这个解决方案正确吗?考虑到 SOLID,该方法会承担太多责任吗?
你推荐哪条路?
public class Basket : Entity, IAggregateRoot
{
public long BasketId { get; protected set; }
public long UserId { get; protected set; }
private List<BasketItem> _basketItems;
public virtual IReadOnlyCollection<BasketItem> BasketItems => basketItems?.ToList();
}
protected Basket() { }
public class BasketItem : Entity
{
public Guid Guid { get; protected set; }
public decimal Price { get; protected set; }
public int Quantity { get; protected set; }
public virtual Basket Basket { get; protected set; }
public virtual Product Product { get; protected set; }
protected BasketItem() { }
public IStatusGeneric ChangeQuantity(int quantity)
{
var errorStatus = new StatusGenericHandler();
//How to get avaiable quantity from WarehouseProduct?????????
//var avaiable = Basket.Warehouse.ProductQuantityAvaiable(Product);
if (avaiable < quantity)
{
var errorStatusMessage = $"There are only {avaiable} avaiable product of {Product.Code}.";
if (productQuantityInBasket > 0)
{
errorStatusMessage += $"Current basket have already {productQuantityInBasket}";
}
errorStatus.AddError(errorStatusMessage);
return errorStatus;
}
haveProduct.ChangeQuantity(quantity);
return errorStatus;
}
}
public class Warehouse : Entity, IAggregateRoot
{
public long WarehouseId { get; protected set; }
public string Name { get; protected set; }
private List<WarehouseProduct> _warehouseProducts;
public virtual IReadOnlyCollection<WarehouseProduct> WarehouseProducts => _warehouseProducts?.ToList();
}
public class WarehouseProduct : Entity
{
public Guid Guid { get; protected set; }
public int Quantity { get; protected set; }
public virtual Warehouse Warehouse { get; protected set; }
public virtual Product Product { get; protected set; }
private WarehouseProduct() { }
public void ChangeQuantity(int deltaQuantity)
{
Quantity += deltaQuantity;
}
}
第二个解决方案是将仓库带入篮球。这样我就可以访问仓库的所有元素以查找可用数量。这也可以让我将篮球与特定仓库相关联,这样我就可以管理多个仓库
public class Basket : Entity, IAggregateRoot
{
public long BasketId { get; protected set; }
public long UserId { get; protected set; }
public Warehouse Warehouse { get; protected set; }
private List<BasketItem> _basketItems;
public virtual IReadOnlyCollection<BasketItem> BasketItems => _basketItems?.ToList();
}
传递一个允许 AR 查询给定项目库存的服务如何,如 basket.changeItemQuantity(itemId, quantity, inventoryService)
?
有些人不喜欢这种风格,更喜欢在应用程序服务中解析库存,然后将其提供给 AR,例如basket.changeItemQuantity(itemId, qty, qtyOnHand)
。我更喜欢这里的第一种方法,尽管单元测试会稍微复杂一些。
第三种方法是实施域服务来编排逻辑。您将应用程序服务中当前拥有的域逻辑提取到该域服务,并将用例处理从应用程序服务委托给域服务。就这条规则而言,它可能有点矫枉过正,只会让你走向贫血模式。
关于您的设计方案的几点说明:
您不应分发非根实体引用。篮子应该提供改变数量的接口。
从篮子 AR 中保存对仓库 AR 的引用的第二个提议也将违反 AR 设计规则:AR 应仅通过 ID 引用其他 AR。这有助于保持 AR 小而集中,并让开发人员了解每个 AR 真正拥有和保护的数据。您也将不太想在同一笔交易中更改两个 AR。
从 AR 边界外引用的数据被认为是陈旧的。您可能想探索将商品添加到购物车的场景,但片刻之后才发现库存已经用完。那你怎么办呢?考虑这些场景将引导您转向更面向业务的模型,该模型可以处理此类复杂问题,而不是试图使所有内容都具有高度一致的不变性,最终导致与领域需求不一致的不切实际的模型。
好的,很好的 DDD 问题。还有一些非常有趣的讨论。
我目前正在编写一个 Shop 应用程序 - 所以这对我和我的团队来说是一个非常有用的问题。
首先我要说的是,您应该与仓库、销售和管理团队讨论这个问题,以便更好地了解场景。如果 2 个销售人员同时致电仓库,询问当前库存情况以及他们是否可以出售 - 对您的实施将产生重大影响。
技术/代码
一般性讨论
- 如果将一个项目添加到购物篮中 - 库存实际上应该减少吗?假设您有 5 件库存商品。 5 个篮子有 1 个物品;没有人可以将此商品添加到他们的购物篮中 - 如果他们真的准备购买怎么办 - 现有的购物篮必须有多陈旧? 考虑:剩余 5 件商品,1 个月前添加到购物篮的 5 件商品。这应该阻止准备购买的用户吗?
这是我的技术观点(很高兴进一步讨论)... @plalx 有一些很棒的想法,在我看来它实际上介于两者之间。
您真的只想问一个问题 - 我可以将此数量的商品添加到此购物篮中吗...
Basket basket = _basketStore.Get(1);
basket.ChangeQuantity(item, quantity, availability);
// 定义
public interface IAvailabilityProcessor {
bool AvailableForBasket(item, quantity);
}
public class Basket {
public void ChangeQuantity (Item item, int quantity, IAvailabilityProcessor processor) {
var canBeAdded = processor.AvailableForBasket(item, quantity);
if (canBeAdded) {
this.Add(item, quantity);
}
}
我可以而且通常会花几个小时讨论这些事情的确切实现,但我现在就这样了 - 请反馈或提问
我的做法是不做。我会听从 mrdnk 的建议,询问利益相关者,并尝试很好地理解该功能背后的商业原因。
可能存在此功能未涵盖的业务影响的指标之一是竞争条件的可能性。无论您如何实施此检查,您最终都会遇到竞争条件,尤其是在工作时间(和 race conditions don't exist):您在仓库中有 1 个项目,您并行收到 100 个请求,并且 100 个用户设法将其放入篮子里的物品。或者你的仓库里有一辆法拉利,一个正在玩耍的孩子把它放在篮子里,而真正的买家却买不到它,因为没有存货。或者有库存,只有一个买家把它放在篮子里,但是仓库做库存并重新调整库存不匹配,你没有足够的物品了。清单还在继续...
我看到的另一种气味是需要从篮子中调用仓库。这些很可能在两个独立的有界上下文中,理想情况下,您不想将它们耦合得太多。
显然每个领域都是不同的,如前所述,您必须与利益相关者一起解决这个问题。他们是如何解决的"in real life"?如果这是一家现有企业,那么订单进来但没有库存的情况肯定已经发生过不止一次了。他们是怎么处理的?
在网上商店处理这种情况的一种方法是不要试图阻止客户根据库存进行购买。尤其是无论如何都不在篮筐里。想象一下亚马逊,当您将一件商品放在购物篮中几天后,即使在多个仓库中,库存也可能会发生数百次变化。
相反,您可以在产品视图和购物篮视图中向用户显示(陈旧的)库存量。例如,在 "Add to Basket" 按钮附近,您可以显示“3 件库存商品”或 "Low Stock"。您不需要将篮子与仓库耦合来实现它。 UI 可以在加载页面时直接调用仓库以获取其显示的项目的库存。
一旦最终下了订单,仓库就可以锁定该订单的商品、订购更多商品、通知客户会有延误等。