15

以下のような支払いシステムがあります。お支払いは、複数のギフト クーポンを使用して行うことができます。ギフトクーポンは購入と同時に発行されます。お客様は、このギフト クーポンを今後の購入に使用できます。

支払いがギフト クーポンを通じて行われる場合、GiftCoupon テーブルの UsedForPaymentID 列をその PaymentID (ギフトクーポン ID 用) で更新する必要があります。

GiftCouponID は、データベースで既に使用可能です。顧客がギフト クーポンを作成すると、GiftCouponID が印刷されます。オペレーターは、この CouponID をシステムに入力して支払いを行う必要があります。

MakePayment() 操作には、2 つのリポジトリが必要です。

  1. ギフト クーポン リポジトリ
  2. 支払いリポジトリ

コード

//GiftCouponRepository を使用して、対応する GiftCoupon オブジェクトを取得します。

これには、1 つのトランザクションで 2 つのリポジトリを使用することが含まれます。それは良い習慣ですか?そうでない場合、これを克服するためにどのように設計を変更できますか?

参照: DDD では、Aggregate はトランザクション境界を表す必要があります。複数の集計の関与を必要とするトランザクションは、多くの場合、モデルを改良する必要があるか、トランザクション要件を確認する必要があるか、またはその両方を示す兆候です。CQRS は私のドメインに適していますか?

ここに画像の説明を入力

C# コード

public RepositoryLayer.ILijosPaymentRepository repository { get; set; }

public void MakePayment(int giftCouponID)
{
    DBML_Project.Payment paymentEntity = new DBML_Project.Payment();
    paymentEntity.PaymentID = 1;

    DBML_Project.GiftCoupon giftCouponObj;

    //Use GiftCouponRepository to retrieve the corresponding GiftCoupon object.     

    paymentEntity.GiftCouponPayments = new System.Data.Linq.EntitySet<DBML_Project.GiftCoupon>();
    paymentEntity.GiftCouponPayments.Add(giftCouponObj);

    repository.InsertEntity(paymentEntity);
    repository.SubmitChanges();
}
4

4 に答える 4

32

あなたが本当に尋ねるつもりだったのは、「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;
    }
}
于 2012-07-12T12:59:05.060 に答える
2

1 つのトランザクションで 2 つのリポジトリを使用しても問題はありません。JB Nizet が指摘するように、それがサービス層の目的です。

接続の共有を維持するのに問題がある場合は、Unit of Work 1パターンを使用してサービス レイヤーから接続を制御し、リポジトリにデータ コンテキストを提供するファクトリに OoW インスタンスを提供させることができます。

1 EF/L2S DataContext はそれ自体が UoW 実装ですが、このような状況では、サービス レイヤー用に抽象化されていると便利です。

于 2012-07-12T10:28:41.310 に答える
0

2 つのアプローチ:

  • 2 つの別個のトランザクション。トランザクション 2 が失敗した場合は、トランザクション 1 をロールバックする必要があります。
  • カードは口座です。そのアカウントに対してトランザクションを記録します。計算された残高 (すべてのトランザクションの合計) がゼロ (またはそれ以下、発生しないはず) になった場合、カードは「使用済み」ですが、DB に「使用済み」を記録しないでください。バランスから導き出すだけです。
于 2015-06-22T23:33:25.620 に答える