c# - 一个事务中的多个聚合/存储库

标签 c# oop design-patterns domain-driven-design cqrs

我有一个支付系统,如下所示。可以通过多张礼券进行支付。礼券随购买一起发放。客户可以使用此礼券以供日后购买。

当通过礼券付款时,GiftCoupon 表中的 UsedForPaymentID 列需要更新为该 PaymentID(用于礼券 ID)。

GiftCouponID 已在数据库中可用。客户出示礼券时,上面印有GiftCouponID。运营商需要将此CouponID输入系统进行支付。

对于 MakePayment() 操作,它需要两个存储库。

  1. 礼券库
  2. 支付存储库

代码

//Use GiftCouponRepository to retrieve the corresponding GiftCoupon object.

这涉及为一项交易使用两个存储库。这是一个好习惯吗?如果不是,我们如何改变设计来克服这个问题?

Reference: In DDD the Aggregate should represent the transactional boundary. A transaction that requires the involvement of more than one aggregate is often a sign that either the model should be refined, or the transactional requirements should be reviewed, or both. Is CQRS correct for my domain?

enter image description here

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();
}

最佳答案

我认为您真正想问的是关于“一次交易中的多个聚合”。我认为使用多个存储库在事务中获取数据 没有任何问题。通常在事务期间,聚合需要来自其他聚合的信息,以便决定是否或如何更改状态。没关系。然而,在一个交易中修改多个聚合的状态被认为是不可取的,我认为这就是你引用的引述试图暗示的。

这是不可取的原因是并发性。除了保护其边界内的不变性外,还应保护每个聚合免受并发事务的影响。例如两个用户同时对聚合进行更改。

这种保护通常是通过在聚合的数据库表上添加版本/时间戳来实现的。保存聚合时,将对正在保存的版本和当前存储在数据库中的版本进行比较(现在可能与事务开始时不同)。如果它们不匹配,则会引发异常。

基本上可以归结为:在协作系统中(很多用户做很多事务),单次事务中修改的聚合越多,并发异常就会增加。

如果您的聚合太大并且提供了许多状态更改方法,则情况完全相同;多个用户一次只能修改一个聚合。通过设计在事务中单独修改的小聚合减少了并发冲突。

Vaughn Vernon has done an excellent job explaining this in his 3 part article.

但是,这只是一个指导原则,在需要修改多个聚合的情况下会有异常(exception)。事实上,您正在考虑是否可以重构事务/用例以仅修改一个聚合,这是一件好事。

考虑过您的示例后,我想不出一种方法来将其设计为满足交易/用例要求的单个聚合。需要创建付款,并且需要更新优惠券以表明它不再有效。

但是当真正分析这个事务的潜在并发问题时,我认为礼券聚合实际上不会发生冲突。它们只会被创建(发行)然后用于支付。中间没有其他状态改变操作。因此,在这种情况下,我们无需担心我们正在修改付款/订单和礼券聚合。

下面是我很快想到的一种可能的建模方式

  • 如果没有付款所属的订单聚合,我看不出付款有什么意义,所以我引入了一个。
  • 订单由付款组成。可以使用礼券付款。您可以创建其他类型的付款方式,例如 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;
    }
}

关于c# - 一个事务中的多个聚合/存储库,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/11445657/

相关文章:

php - 向急需重构的代码库添加功能

oop - UML 和算法

language-agnostic - 什么时候不应该使用单例模式? (除了显而易见的)

c# - 什么是提升运算符?

c# - 如何修复此错误以防止返回“测验”页面并更改他的答案?

java - 我们有 .NET "mustinherit"或 "notinheritable"的 Java 对应者吗?

oop - DDD : Where to put persistence logic, 以及何时使用 ORM 映射

c++ - 这是什么设计模式?

c# - 使用 Entity Framework 3.5 和 MVVM 的存储库模式 - 我应该在任何地方共享相同的上下文吗?

c# - 如何在 C# 中将 .proto 文件解析为 FileDescriptor?