首先,让我们说一个电子商务网站中有两个独立的汇总购物篮和订单。
购物篮集合具有两个实体购物篮(这是聚合根)和 BaskItem ,定义如下(我已删除工厂和其他聚合简单的方法):
public class Basket : BaseEntity, IAggregateRoot
{
public int Id { get; set; }
public string BuyerId { get; private set; }
private readonly List<BasketItem> items = new List<BasketItem>();
public IReadOnlyCollection<BasketItem> Items
{
get
{
return items.AsReadOnly();
}
}
}
public class BasketItem : BaseEntity
{
public int Id { get; set; }
public decimal UnitPrice { get; private set; }
public int Quantity { get; private set; }
public string CatalogItemId { get; private set; }
}
第二个汇总为 Order ,订单以汇总根为根, OrderItem 作为实体,地址和 CatalogueItemOrdered 作为定义如下的值对象:
public class Order : BaseEntity, IAggregateRoot
{
public int Id { get; set; }
public string BuyerId { get; private set; }
public readonly List<OrderItem> orderItems = new List<OrderItem>();
public IReadOnlyCollection<OrderItem> OrderItems
{
get
{
return orderItems.AsReadOnly();
}
}
public DateTimeOffset OrderDate { get; private set; } = DateTimeOffset.Now;
public Address DeliverToAddress { get; private set; }
public string Notes { get; private set; }
}
public class OrderItem : BaseEntity
{
public int Id { get; set; }
public CatalogItemOrdered ItemOrdered { get; private set; }
public decimal Price { get; private set; }
public int Quantity { get; private set; }
}
public class CatalogItemOrdered
{
public int CatalogItemId { get; private set; }
public string CatalogItemName { get; private set; }
public string PictureUri { get; private set; }
}
public class Address
{
public string Street { get; private set; }
public string City { get; private set; }
public string State { get; private set; }
public string Country { get; private set; }
public string ZipCode { get; private set; }
}
现在,如果用户想在将多个项目添加到购物篮后结帐,则应执行以下操作:
更新购物篮(某些商品的数量可能已更改)
添加/设置新订单
删除购物篮(或在数据库中标记为已删除)
使用特定的付款网关通过信用卡付款。
如我所见,应该执行多个事务,因为根据每个事务中的DDD,只应更改一个聚合。
那么您能指导我如何以一种不违反DDD原则的方式实现该目标(也许通过使用最终一致性)吗?
PS:
我感谢任何参考或资源
答案 0 :(得分:2)
模型缺少的最重要的是行为。您的课程只保存数据,有时会保存不公开的数据(例如Basket.Id
)。域实体必须定义用于对其数据进行操作的方法。
您所正确的是,您拥有将其子级括起来的合计根(例如,篮中包含项的私人列表)。应当将聚合视为原子,因此,每次将篮子篮加载或持久存储到数据库中时,您都将篮子篮和物品视为一个整体。这甚至会让您变得更轻松。
这是我的非常相似域的模型:
public class Cart : AggregateRoot
{
private const int maxQuantityPerProduct = 10;
private const decimal minCartAmountForCheckout = 50m;
private readonly List<CartItem> items = new List<CartItem>();
public Cart(EntityId customerId) : base(customerId)
{
CustomerId = customerId;
IsClosed = false;
}
public EntityId CustomerId { get; }
public bool IsClosed { get; private set; }
public IReadOnlyList<CartItem> Items => items;
public decimal TotalAmount => items.Sum(item => item.TotalAmount);
public Result CanAdd(Product product, Quantity quantity)
{
var newQuantity = quantity;
var existing = items.SingleOrDefault(item => item.Product == product);
if (existing != null)
newQuantity += existing.Quantity;
if (newQuantity > maxQuantityPerProduct)
return Result.Fail("Cannot add more than 10 units of each product.");
return Result.Ok();
}
public void Add(Product product, Quantity quantity)
{
CanAdd(product, quantity)
.OnFailure(error => throw new Exception(error));
for (int i = 0; i < items.Count; i++)
{
if (items[i].Product == product)
{
items[i] = items[i].Add(quantity);
return;
}
}
items.Add(new CartItem(product, quantity));
}
public void Remove(Product product)
{
var existing = items.SingleOrDefault(item => item.Product == product);
if (existing != null)
items.Remove(existing);
}
public void Remove(Product product, Quantity quantity)
{
var existing = items.SingleOrDefault(item => item.Product == product);
for (int i = 0; i < items.Count; i++)
{
if (items[i].Product == product)
{
items[i] = items[i].Remove(quantity);
return;
}
}
if (existing != null)
existing = existing.Remove(quantity);
}
public Result CanCloseForCheckout()
{
if (IsClosed)
return Result.Fail("The cart is already closed.");
if (TotalAmount < minCartAmountForCheckout)
return Result.Fail("The total amount should be at least 50 dollars in order to proceed to checkout.");
return Result.Ok();
}
public void CloseForCheckout()
{
CanCloseForCheckout()
.OnFailure(error => throw new Exception(error));
IsClosed = true;
AddDomainEvent(new CartClosedForCheckout(this));
}
public override string ToString()
{
return $"{CustomerId}, Items {items.Count}, Total {TotalAmount}";
}
}
以及商品的类别:
public class CartItem : ValueObject<CartItem>
{
internal CartItem(Product product, Quantity quantity)
{
Product = product;
Quantity = quantity;
}
public Product Product { get; }
public Quantity Quantity { get; }
public decimal TotalAmount => Product.UnitPrice * Quantity;
public CartItem Add(Quantity quantity)
{
return new CartItem(Product, Quantity + quantity);
}
public CartItem Remove(Quantity quantity)
{
return new CartItem(Product, Quantity - quantity);
}
public override string ToString()
{
return $"{Product}, Quantity {Quantity}";
}
protected override bool EqualsCore(CartItem other)
{
return Product == other.Product && Quantity == other.Quantity;
}
protected override int GetHashCodeCore()
{
return Product.GetHashCode() ^ Quantity.GetHashCode();
}
}
一些重要的事情要注意:
Cart
和CartItem
是一回事。它们作为一个整体从数据库中加载,然后在一个事务中照常保留下来; CanAdd
和Add
方法。此类的使用者应首先调用CanAdd
并将可能的错误传播给用户。如果在没有事先验证的情况下调用Add
,则Add
将与CanAdd
进行检查,如果违反了任何不变式,则抛出异常,并且在这里抛出异常是正确的做法,因为在没有先检查Add
的情况下进入CanAdd
表示软件中的错误,是程序员的错误; Cart
是一个实体,它具有一个ID,但是CartItem
是一个ValueObject而没有ID。客户可以重复购买具有相同项目的商品,但仍然是不同的购物车,但是具有相同属性(数量,价格,商品名称)的CartItem始终相同-它是其属性的组合构成其身份。因此,请考虑我的域的规则:
这些是由聚合根强制执行的,无法以任何允许破坏不变量的方式滥用这些类。
您可以在此处查看完整的模型:Shopping Cart Model
更新购物篮(也许某些物品的数量已更改)
在Basket
类中具有一个方法,该方法将负责对购物篮项目进行更改(添加,删除,更改数量)。
添加/设置新订单
似乎订单将驻留在另一个有界上下文中。在这种情况下,您将拥有一种类似Basket.ProceedToCheckout
的方法,该方法会将自己标记为已关闭并传播DomainEvent,该事件将依次在Order Bounded Context中获取,并添加/创建Order。 >
但是,如果您确定域中的Order与Basket属于同一BC,则可以使用DomainService一次处理两个聚合:它将调用Basket.ProceedToCheckout
,并且如果没有错误抛出后,它将根据它创建一个Order
聚合。请注意,此操作跨越两个聚合,因此已从聚合移至DomainService。
请注意,此处不需要数据库事务即可确保域状态的正确性。
您可以调用Basket.ProceedToCheckout
,它将通过将Closed
属性设置为true
来更改其内部状态。然后,创建订单可能会出错,并且您不需要需要回滚购物篮。
您可以修复软件中的错误,客户可以尝试再次结帐,而您的逻辑将仅检查购物篮是否已关闭并具有相应的订单。如果没有,它将仅执行必要的步骤,而跳过已经完成的步骤。这就是我们所说的幂等。
删除购物篮(或在数据库中标记为已删除)
您应该对此进行更多考虑。与网域专家联系,因为我们不会删除任何真实世界,并且您可能不应该删除网域中的篮子。因为这是最有可能对企业有价值的信息,例如知道哪些篮子被遗弃,然后知道营销部门。可以通过打折促销来吸引这些客户,以便他们购买。
我建议您阅读Udi Dahan的这篇文章:Don't Delete - Just Don't。他潜入主题深处。
使用特定的支付网关通过信用卡支付
付款网关是基础结构,您的域不应该对此有任何了解(即使接口应在另一层中声明)。在软件体系结构方面,更具体地说,在洋葱体系结构方面,我建议您定义以下类:
namespace Domain
{
public class PayOrderCommand : ICommand
{
public Guid OrderId { get; }
public PaymentInformation PaymentInformation { get; }
public PayOrderCommand(Guid orderId, PaymentInformation paymentInformation)
{
OrderId = orderId;
PaymentInformation = paymentInformation;
}
}
}
namespace Application
{
public class PayOrderCommandHandler : ICommandHandler<PayOrderCommand>
{
private readonly IPaymentGateway paymentGateway;
private readonly IOrderRepository orderRepository;
public PayOrderCommandHandler(IPaymentGateway paymentGateway, IOrderRepository orderRepository)
{
this.paymentGateway = paymentGateway;
this.orderRepository = orderRepository;
}
public Result Handle(PayOrderCommand command)
{
var order = orderRepository.Find(command.OrderId);
var items = GetPaymentItems(order);
var result = paymentGateway.Pay(command.PaymentInformation, items);
if (result.IsFailure)
return result;
order.MarkAsPaid();
orderRepository.Save(order);
return Result.Ok();
}
private List<PaymentItems> GetPaymentItems(Order order)
{
// TODO: convert order items to payment items.
}
}
public interface IPaymentGateway
{
Result Pay(PaymentInformation paymentInformation, IEnumerable<PaymentItems> paymentItems);
}
}
我希望这能给您一些见识。