你的模型缺少的最重要的东西是行为。您的类只保存数据,有时在不应该保存数据的时候使用公共setter(比如
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
是一回事。它们作为单个单元从数据库中加载,然后在一个事务中按原样持久化;
-
数据和操作(行为)紧密结合在一起。这实际上不是DDD规则或准则,而是面向对象的编程原则。这就是OO的意义所在;
-
有人可以对模型执行的每个操作都在聚合根中表示为一个方法,而aggreate根在处理其内部对象时会处理所有操作。它控制一切,每一个操作都必须经过根;
-
CanAdd
以及
Add
方法。这个类的消费者应该首先调用
加拿大
并将可能的错误传播给用户。如果
添加
在未经事先验证的情况下调用
将检查
加拿大
如果要违反任何不变量,则抛出异常,在这里抛出异常是正确的,因为
未经事先检查
加拿大
-
手推车
是一个实体,它有一个Id,但是
手推车
所以,考虑一下我的领域规则:
这些都是由聚合根强制执行的,没有任何方法可以以任何方式误用类,从而破坏不变量。
Shopping Cart Model
回到你的问题上来
更新购物篮(可能某些商品的数量已更改)
有一个方法
Basket
新增/设置新订单
似乎一个顺序将驻留在另一个有界上下文中。在这种情况下,你会有一种方法
Basket.ProceedToCheckout
这会将自身标记为关闭,并传播一个DomainEvent,然后在Order-Bounded上下文中拾取它,并添加/创建一个Order。
篮式进料器
如果没有抛出错误,它将创建
Order
注意,这里不需要数据库事务来确保域状态的正确性。
你可以打电话
,它将通过设置
Closed
true
. 那么秩序的建立就会出错
不
您可以修复软件中的错误,客户可以再次尝试结帐,而您的逻辑只是检查购物篮是否已关闭并有相应的订单。如果没有,它将只执行必要的步骤,跳过那些已经完成的步骤。这就是我们所说的
幂等性
删除购物篮(或在数据库中标记为已删除)
你真的应该多想想。与领域专家交谈,因为我们不会删除现实世界中的任何内容,而且您可能不应该删除域中的篮子。因为这是最有可能对企业有价值的信息,比如知道哪些篮子被丢弃了,然后市场营销部可以通过打折促销来吸引这些客户,以便他们能够购买。
我建议你读这篇文章:
Don't Delete - Just Don't
,作者Udi Dahan。他深入研究这个问题。
支付网关是基础设施,你的域不应该知道它的任何东西(甚至接口应该在另一层声明)。在软件架构方面,尤其是在洋葱架构中,我建议您定义以下类:
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);
}
}