我认为你真正想问的是关于'一笔交易中的多个聚合'。我不认为使用多个存储库有什么问题获取事务中的数据。通常在事务期间,聚合需要来自其他聚合的信息,以便决定是否或如何更改状态。没关系。然而,在一个事务中修改多个聚合的状态被认为是不可取的,我认为这就是您引用的引用试图暗示的内容。
这是不受欢迎的原因是并发性。除了保护其边界内的不变量之外,还应该保护每个聚合免受并发事务的影响。例如两个用户同时对聚合进行更改。
这种保护通常是通过在聚合的数据库表上拥有版本/时间戳来实现的。保存聚合时,将对正在保存的版本和当前存储在数据库中的版本进行比较(现在可能与事务开始时不同)。如果它们不匹配,则会引发异常。
基本上可以归结为:在协作系统(许多用户进行许多事务)中,单个事务中修改的聚合越多,将导致并发异常的增加。
如果您的聚合太大并且提供了许多状态更改方法,则情况完全相同;多个用户一次只能修改一个聚合。通过设计在事务中单独修改的小型聚合,可以减少并发冲突。
沃恩·弗农 (Vaughn Vernon) 在他的三部分文章中出色地解释了这一点。 http://dddcommunity.org/library/vernon_2011
然而,这只是一个指导原则,存在需要修改多个聚合的例外情况。事实上,您正在考虑是否可以重构事务/用例以仅修改一个聚合,这是一件好事。
考虑了您的示例后,我想不出一种将其设计为满足事务/用例要求的单个聚合的方法。需要创建付款,并且需要更新优惠券以表明其不再有效。
但是当真正分析潜在的并发问题时this交易,我认为礼券总量实际上不会发生冲突。它们仅被创建(发行)然后用于付款。其间没有其他状态改变操作。因此,在这种情况下,我们不需要担心我们正在修改付款/订单和礼品券聚合的事实。
下面是我很快想出的一种可能的建模方法
- 如果没有付款所属的订单聚合,我无法理解付款如何有意义,因此我引入了一个。
- 订单由付款组成。可以使用礼券付款。您可以创建其他类型的付款方式,例如现金付款或信用卡付款。
- 要进行礼品券支付,必须将优惠券聚合传递到订单聚合。然后,这会将优惠券标记为已使用。
- 交易结束时,订单总额将与其新付款一起保存,并且所有使用的礼券也会保存。
Code:
public class PaymentApplicationService
{
public void PayForOrderWithGiftCoupons(PayForOrderWithGiftCouponsCommand command)
{
using (IUnitOfWork unitOfWork = UnitOfWorkFactory.Create())
{
Order order = _orderRepository.GetById(command.OrderId);
List<GiftCoupon> coupons = new List<GiftCoupon>();
foreach(Guid couponId in command.CouponIds)
coupons.Add(_giftCouponRepository.GetById(couponId));
order.MakePaymentWithGiftCoupons(coupons);
_orderRepository.Save(order);
foreach(GiftCoupon coupon in coupons)
_giftCouponRepository.Save(coupon);
}
}
}
public class Order : IAggregateRoot
{
private readonly Guid _orderId;
private readonly List<Payment> _payments = new List<Payment>();
public Guid OrderId
{
get { return _orderId;}
}
public void MakePaymentWithGiftCoupons(List<GiftCoupon> coupons)
{
foreach(GiftCoupon coupon in coupons)
{
if (!coupon.IsValid)
throw new Exception("Coupon is no longer valid");
coupon.UseForPaymentOnOrder(this);
_payments.Add(new GiftCouponPayment(Guid.NewGuid(), DateTime.Now, coupon));
}
}
}
public abstract class Payment : IEntity
{
private readonly Guid _paymentId;
private readonly DateTime _paymentDate;
public Guid PaymentId { get { return _paymentId; } }
public DateTime PaymentDate { get { return _paymentDate; } }
public abstract decimal Amount { get; }
public Payment(Guid paymentId, DateTime paymentDate)
{
_paymentId = paymentId;
_paymentDate = paymentDate;
}
}
public class GiftCouponPayment : Payment
{
private readonly Guid _couponId;
private readonly decimal _amount;
public override decimal Amount
{
get { return _amount; }
}
public GiftCouponPayment(Guid paymentId, DateTime paymentDate, GiftCoupon coupon)
: base(paymentId, paymentDate)
{
if (!coupon.IsValid)
throw new Exception("Coupon is no longer valid");
_couponId = coupon.GiftCouponId;
_amount = coupon.Value;
}
}
public class GiftCoupon : IAggregateRoot
{
private Guid _giftCouponId;
private decimal _value;
private DateTime _issuedDate;
private Guid _orderIdUsedFor;
private DateTime _usedDate;
public Guid GiftCouponId
{
get { return _giftCouponId; }
}
public decimal Value
{
get { return _value; }
}
public DateTime IssuedDate
{
get { return _issuedDate; }
}
public bool IsValid
{
get { return (_usedDate == default(DateTime)); }
}
public void UseForPaymentOnOrder(Order order)
{
_usedDate = DateTime.Now;
_orderIdUsedFor = order.OrderId;
}
}