あなたが本当に尋ねるつもりだったのは、「1つのトランザクションでの複数の集計」に関することだったと思います。トランザクションでデータをフェッチするために複数のリポジトリを使用することに何も問題はないと思います。多くの場合、トランザクション中に、状態を変更するかどうか、または変更する方法を決定するために、アグリゲートは他のアグリゲートからの情報を必要とします。それはいいです。ただし、望ましくないと見なされるのは、1つのトランザクション内の複数のアグリゲートの状態の変更であり、これは、参照された見積もりが意味しようとしていたことだと思います。
これが望ましくない理由は、並行性のためです。境界内のインバリアントを保護するだけでなく、各アグリゲートを同時トランザクションから保護する必要があります。たとえば、2人のユーザーが同時に集計に変更を加えます。
この保護は通常、アグリゲートのDBテーブルにバージョン/タイムスタンプを設定することで実現されます。アグリゲートが保存されると、保存されているバージョンと現在データベースに保存されているバージョンが比較されます(トランザクションが開始されたときとは異なる場合があります)。それらが一致しない場合、例外が発生します。
基本的には次のように要約されます。コラボレーションシステム(多くのユーザーが多くのトランザクションを実行する)では、単一のトランザクションで変更される集計が増えると、同時実行の例外が増加します。
集計が大きすぎて、多くの状態変更メソッドを提供している場合も、まったく同じことが言えます。複数のユーザーが変更できるのは、一度に1つだけです。トランザクションで個別に変更される小さなアグリゲートを設計することにより、同時実行の衝突を減らします。
Vaughn Vernonは、彼の3部構成の記事でこれを説明する素晴らしい仕事をしました。
ただし、これは単なる指針であり、複数の集計を変更する必要がある場合は例外があります。トランザクション/ユースケースをリファクタリングして1つのアグリゲートのみを変更できるかどうかを検討しているという事実は良いことです。
あなたの例を考えたので、トランザクション/ユースケースの要件を満たす単一の集合体にそれを設計する方法を考えることはできません。支払いを作成する必要があり、クーポンが無効になったことを示すためにクーポンを更新する必要があります。
しかし、このトランザクションで発生する可能性のある同時実行の問題を実際に分析する場合、ギフトクーポンの集計に実際に衝突が発生することはないと思います。それらは、作成(発行)されてから支払いに使用されるだけです。間に他の状態変更操作はありません。したがって、この場合、支払い/注文とギフトクーポンの合計の両方を変更しているという事実を心配する必要はありません。
以下は、それをモデル化するための可能な方法として私がすぐに思いついたものです
- 支払いが属する注文集計がないと、支払いがどのように意味をなすのかわからなかったので、1つ紹介しました。
- 注文は支払いで構成されます。ギフトクーポンでお支払いいただけます。たとえば、CashPaymentやCreditCardPaymentなどの他のタイプの支払いを作成できます。
- ギフトクーポンの支払いを行うには、クーポンの集計を注文の集計に渡す必要があります。これにより、クーポンが使用済みとしてマークされます。
- トランザクションの終了時に、注文の集計が新しい支払いとともに保存され、使用されたギフトクーポンも保存されます。
コード:
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;
}
}