代码之家  ›  专栏  ›  技术社区  ›  Simple Code

如何在基于DDD的应用程序中实现签出?

  •  3
  • Simple Code  · 技术社区  · 6 年前

    首先,假设我有两个分离的聚合 篮子 在一个电子商务网站上。

    篮子 聚合有两个实体 (这是聚合根)和 巴斯基山脉

    public class Basket : BaseEntity, IAggregateRoot
    {
        public int Id { get; set; }
    
        public string BuyerId { get; private set; }
    
        private readonly List<BasketItem> items = new List<BasketItem>();
    
        public  IReadOnlyCollection<BasketItem> Items
        {
                get
                {
                    return items.AsReadOnly();
                }
         }
    
    }
    
    public class BasketItem : BaseEntity
    {
        public int Id { get; set; }
    
        public decimal UnitPrice { get; private set; }
    
        public int Quantity { get; private set; }
    
        public string CatalogItemId { get; private set; }
    
    }
    

    第二个集合是 以聚合根和 订单项 地址 作为值对象定义如下:

    public class Order : BaseEntity, IAggregateRoot
        {
            public int Id { get; set; }
    
            public string BuyerId { get; private set; }
    
            public readonly List<OrderItem> orderItems = new List<OrderItem>();
    
            public IReadOnlyCollection<OrderItem> OrderItems
            {
                get
                {
                    return orderItems.AsReadOnly();
                }
            }
    
            public DateTimeOffset OrderDate { get; private set; } = DateTimeOffset.Now;
    
            public Address DeliverToAddress { get; private set; }
    
            public string Notes { get; private set; }
    
        }
    
        public class OrderItem : BaseEntity
        {
            public int Id { get; set; }
            public CatalogItemOrdered ItemOrdered { get; private set; }
            public decimal Price { get; private set; }
            public int Quantity { get; private set; }
        }
    
        public class CatalogItemOrdered
        {
            public int CatalogItemId { get; private set; }
            public string CatalogItemName { get; private set; }
            public string PictureUri { get; private set; }
        }
    
        public class Address
        {
            public string Street { get; private set; }
    
            public string City { get; private set; }
    
            public string State { get; private set; }
    
            public string Country { get; private set; }
    
            public string ZipCode { get; private set; }
        }
    

    1. 更新购物篮(可能某些商品的数量已更改)

    2. 新增/设置新订单

    3. 使用特定的支付网关通过信用卡支付。

    所以,你能指导我如何在不违反DDD原则的情况下实现这一点吗(也许是通过使用最终的一致性)?

    我很感激你的推荐信和资料

    0 回复  |  直到 6 年前
        1
  •  3
  •   Phillippe Santana    5 年前

    你的模型缺少的最重要的东西是行为。您的类只保存数据,有时在不应该保存数据的时候使用公共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();
            }
        }
    

    1. Cart CartItem 是一回事。它们作为单个单元从数据库中加载,然后在一个事务中按原样持久化;
    2. 数据和操作(行为)紧密结合在一起。这实际上不是DDD规则或准则,而是面向对象的编程原则。这就是OO的意义所在;
    3. 有人可以对模型执行的每个操作都在聚合根中表示为一个方法,而aggreate根在处理其内部对象时会处理所有操作。它控制一切,每一个操作都必须经过根;
    4. CanAdd 以及 Add 方法。这个类的消费者应该首先调用 加拿大 并将可能的错误传播给用户。如果 添加 在未经事先验证的情况下调用 将检查 加拿大 如果要违反任何不变量,则抛出异常,在这里抛出异常是正确的,因为 未经事先检查 加拿大
    5. 手推车 是一个实体,它有一个Id,但是 手推车

    所以,考虑一下我的领域规则:

    • 用户在购物车中添加的商品不能超过10件;

    这些都是由聚合根强制执行的,没有任何方法可以以任何方式误用类,从而破坏不变量。

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

    推荐文章