Как реализовать оформление заказа в приложении на основе DDD? - PullRequest
0 голосов
/ 05 сентября 2018

Прежде всего, скажем, у меня есть два отдельных агрегата Корзина и Заказ на сайте электронной коммерции.

Корзина агрегат имеет две сущности Корзина (которая является корнем агрегата) и BaskItem , определенные следующим образом (для простоты я удалил фабрики и другие методы агрегирования) ):

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

}

Второй агрегат, который является Order , имеет Order в качестве корня агрегата и OrderItem в качестве сущности и Address и CatalogueItemOrdered в качестве определенных объектов значения следующим образом:

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. Удаление корзины (или пометка как удаленная в БД)

  4. Оплата с помощью кредитной карты с использованием определенного Платежного шлюза.

Как я вижу, необходимо выполнить несколько транзакций, поскольку в зависимости от DDD в каждой транзакции должен быть изменен только один агрегат.

Так, пожалуйста, не могли бы вы подсказать мне, как я могу реализовать это (возможно, используя возможную согласованность) таким образом, чтобы я не нарушал принципы DDD?

PS:

Я ценю любые ссылки или ресурсы

1 Ответ

0 голосов
/ 02 августа 2019

Самое важное, чего не хватает вашей модели - это поведение. Ваши классы содержат только данные, иногда с общедоступными установщиками, когда они не должны (например, 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, а принцип объектно-ориентированного программирования. Это то, что ОО все о;
  3. Каждая операция, которую кто-либо может сделать с моделью, выражается в виде метода в совокупном корне, и сводный корень заботится обо всем, когда речь идет о работе со своими внутренними объектами. Он контролирует все, каждая операция должна проходить через корень;
  4. Для каждой операции, которая потенциально может пойти не так, есть метод проверки. Например, у вас есть методы CanAdd и Add. Потребители этого класса должны сначала вызвать CanAdd и сообщить о возможных ошибках пользователю. Если Add вызывается без предварительной проверки, то Add проверит с помощью CanAdd и сгенерирует исключение, если какой-либо инвариант должен быть нарушен, и выбрасывание исключения - это правильная вещь, потому что получение Add без первая проверка с CanAdd представляет ошибку в программном обеспечении, ошибку, совершенную программистами;
  5. Cart - это объект, у него есть Id, но CartItem - это ValueObject, у которого нет Id. Покупатель может повторить покупку с теми же предметами, и это все равно будет другая корзина, но CartItem с одинаковыми свойствами (количество, цена, наименование товара) всегда одинаковы - это комбинация его свойств, которые составляют его идентичность .

Итак, рассмотрим правила моего домена:

  • Пользователь не может добавить более 10 единиц каждого продукта в корзину;
  • Пользователь может перейти к оформлению заказа только в том случае, если в его корзине было не менее 50 долларов США.

Они применяются агрегатным корнем, и нет никакого способа неправильно использовать классы любым способом, который позволил бы нарушить инварианты.

Вы можете увидеть полную модель здесь: Корзина Модель


Вернуться к вашему вопросу

Обновление корзины (возможно, количество некоторых предметов было изменено)

В классе Basket есть метод, который будет отвечать за оперативные изменения позиций корзины (добавление, удаление, изменение количества).

Добавление / настройка нового заказа

Похоже, Орден будет находиться в другом Ограниченном Контексте. В этом случае у вас будет такой метод, как Basket.ProceedToCheckout, который будет отмечать себя как закрытый и распространять DomainEvent, который, в свою очередь, будет выбран в контексте ограниченного порядка, и будет добавлен / создан порядок.

Но если вы решите, что Заказ в вашем домене является частью той же BC, что и Корзина, вы можете иметь DomainService, который будет работать с двумя агрегатами одновременно: он вызовет Basket.ProceedToCheckout и, если не будет выдано никакой ошибки было бы создать агрегат Order из него. Обратите внимание, что это операция, охватывающая два агрегата, и поэтому она была перемещена из агрегата в DomainService.

Обратите внимание, что транзакция с базой данных здесь не требуется для обеспечения правильности состояния домена.

Вы можете вызвать Basket.ProceedToCheckout, что изменит его внутреннее состояние, установив для свойства Closed значение true. Тогда создание Ордена может пойти не так, и вам не потребуется откатить Корзину.

Вы можете исправить ошибку в программном обеспечении, клиент может попытаться еще раз оформить заказ, и ваша логика просто проверит, закрыта ли корзина и имеется ли соответствующий ордер. Если нет, он будет выполнять только необходимые шаги, пропуская те, которые уже выполнены. Это то, что мы называем Идемпотентность .

Удаление корзины (или пометить как удаленную в БД)

Тебе стоит подумать об этом больше. Поговорите со специалистами по доменам, потому что мы ничего не удаляем из реального мира, и вам, вероятно, не следует удалять корзину в вашем домене. Потому что эта информация, скорее всего, имеет ценность для бизнеса, например, знание того, какие корзины были оставлены, а затем отдел маркетинга. может продвигать акции со скидками, чтобы вернуть этих клиентов, чтобы они могли покупать.

Я рекомендую вам прочитать эту статью: Не удалять - просто не , автор Udi Dahan. Он глубоко погружается в тему.

Оплата с помощью кредитной карты с использованием определенного Платежного шлюза

Платежный шлюз - это инфраструктура, ваш домен не должен ничего знать об этом (даже интерфейсы должны быть объявлены на другом уровне). С точки зрения архитектуры программного обеспечения, более конкретно в Onion Architecture, я рекомендую вам определить эти классы:

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

Надеюсь, это дало вам некоторое представление.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...