DDDベースのアプリケーションでチェックアウトを実装する方法は?

シンプルなコード

まず最初に、e-commereceWebサイトに2つの別々のアグリゲートBasketOrderがあるとしましょう

バスケットアグリゲートには、次のように定義された2つのエンティティBasket(アグリゲートルート)と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である2番目の集計には、集計ルートとしてOrderがあり、エンティティとしてOrderItemがあり、値オブジェクトとしてAddressCatalogueItemOrderedが次のように定義されています。

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. バスケットの削除(またはDBで削除済みのフラグ)

  4. 特定の支払いゲートウェイを使用してクレジットカードで支払う。

ご覧のとおり、すべてのトランザクションのDDDに応じて、1つのアグリゲートのみを変更する必要があるため、複数のトランザクションを実行する必要があります。

では、DDDの原則に違反しない方法で(おそらく結果整合性を使用して)それを実装する方法を教えていただけますか?

PS:

参考資料やリソースに感謝します

フィリップサンタナ

モデルに欠けている最も重要なことは動作です。あなたのクラスはデータのみを保持しており、公開セッターが保持すべきでない場合もあります(のようにBasket.Id)。ドメインエンティティは、データを操作するためのメソッドを定義する必要があります。

あなたが正しかったのは、その子を囲む集約ルートがあるということです(たとえば、アイテムのプライベートリストを含むバスケット)。集合体はアトムのように扱われることになっているため、バスケットをデータベースにロードまたは永続化するたびに、バスケットとアイテムを1つの全体として扱うことになります。これにより、物事がはるかに簡単になります。

これは、非常によく似たドメインの私のモデルです。

    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一つのことです。それらはデータベースから単一のユニットとしてロードされ、1つのトランザクションでそのまま保持されます。
  2. データと操作(動作)は密接に関連しています。これは実際にはDDDのルールやガイドラインではなく、オブジェクト指向プログラミングの原則です。これがOOのすべてです。
  3. 誰かがモデルで実行できるすべての操作は、集約ルートのメソッドとして表現され、内部オブジェクトの処理に関しては、集約ルートがすべてを処理します。それはすべてを制御し、すべての操作はルートを経由する必要があります。
  4. 失敗する可能性のあるすべての操作には、検証方法があります。たとえば、CanAddAddメソッドがあります。このクラスのコンシューマーは、最初に呼び出してCanAdd、発生する可能性のあるエラーをユーザーに伝達する必要があります。場合は、Addより、事前検証なしで呼ばれているAddに確認しますCanAddいかなる不変に違反するようにしている場合、例外をスローし、になっているので、例外をスローすることはここで行うには正しいことであるAddと最初にチェックせずにはCanAddソフトウェアのバグを表し、エラープログラマーをコミットすることによって;
  5. Cartはエンティティであり、IDはありますCartItemが、ValueObjectであり、IDはありません。顧客は同じアイテムで購入を繰り返すことができ、それでも別のカートになりますが、同じプロパティ(数量、価格、アイテム名)を持つCartItemは常に同じです-アイデンティティを構成するのはプロパティの組み合わせです。

だから、私のドメインのルールを考えてみましょう:

  • ユーザーは、カートに各製品を10ユニット以上追加することはできません。
  • ユーザーは、カートに少なくとも50米ドルの商品がある場合にのみ、チェックアウトに進むことができます。

これらは集約ルートによって強制され、不変条件を破ることができるような方法でクラスを誤用する方法はありません。

あなたはここで完全なモデルを見ることができます:ショッピングカートモデル


質問に戻る

バスケットの更新(一部のアイテムの数量が変更された可能性があります)

Basketバスケットアイテムの変更(数量の追加、削除、変更)の操作を担当するメソッドをクラスに用意します。

新しい注文の追加/設定

注文は別の境界コンテキストに存在するようです。その場合、Basket.ProceedToCheckoutそれ自体を閉じたものとしてマークし、DomainEventを伝播するようなメソッドがあります。これは、Order Boundedコンテキストで取得され、Orderが追加/作成されます。

ただし、ドメイン内の注文がバスケットと同じBCの一部であると判断した場合は、2つのアグリゲートを同時に処理するDomainServiceを使用できます。呼び出しBasket.ProceedToCheckoutが行われ、エラーがスローされない場合は、Orderそれから集約します。これは2つのアグリゲートにまたがる操作であるため、アグリゲートからDomainServiceに移動されていることに注意してください。

ドメインの状態の正確さを保証するために、ここではデータベーストランザクションは必要ないことに注意してください。

を呼び出すことができますBasket.ProceedToCheckout。これは、Closedプロパティをに設定することで内部状態を変更trueます。その場合、注文の作成が失敗する可能性があり、バスケットをロールバックする必要ありません

ソフトウェアのエラーを修正すると、顧客はもう一度チェックアウトを試みることができ、ロジックはバスケットがすでに閉じられており、対応する注文があるかどうかを確認するだけです。そうでない場合は、必要な手順のみを実行し、すでに完了している手順はスキップします。これは、私たちが呼んでいるものであるべき等

バスケットの削除(またはDBで削除済みのフラグ)

あなたは本当にそれについてもっと考える必要があります。現実の世界は何も削除しないので、ドメインの専門家に相談してください。ドメイン内のバスケットを削除するべきではないでしょう。これは、どのバスケットが放棄されたかを知り、次にマーケティング部門など、ビジネスにとって価値がある可能性が最も高い情報だからです。割引付きのアクションを促進して、これらの顧客を呼び戻し、購入できるようにすることができます。

この記事を読むことをお勧めします:UdiDahanによる「削除しないください-ただしないください」彼は主題を深く掘り下げます。

特定の支払いゲートウェイを使用したクレジットカードによる支払い

Payment Gatewayはインフラストラクチャであり、ドメインはそれについて何も知らないはずです(インターフェースでさえ別のレイヤーで宣言する必要があります)。ソフトウェアアーキテクチャに関して、より具体的にはOnionアーキテクチャでは、次のクラスを定義することをお勧めします。

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

これがあなたにいくつかの洞察を与えたことを願っています。

この記事はインターネットから収集されたものであり、転載の際にはソースを示してください。

侵害の場合は、連絡してください[email protected]

編集
0

コメントを追加

0

関連記事

分類Dev

Javaアプリケーションとシェルスクリプト間の同期を実装する方法は?

分類Dev

クラウドベースのSQLServerに対してデスクトップアプリケーションを実行することは可能ですか?

分類Dev

アノテーションベースのSpring Bootアプリケーションにプラグインアーキテクチャを実装する

分類Dev

React&Reduxアプリケーションのイベントハンドラーでアクションをディスパッチする方法

分類Dev

チャットアプリケーション-Androidにチャットアプリを実装するのに適した技術はどれですか

分類Dev

GUIベースのアプリケーションはバックグラウンドでシェルコマンドを実行しますか?

分類Dev

GUIベースのアプリケーションはバックグラウンドでシェルコマンドを実行しますか?

分類Dev

Androidアプリケーションにクライアント<->サーバー<->データベースアーキテクチャを実装する最良の方法は?

分類Dev

openId Java Webベースのアプリケーションを実装する方法は?

分類Dev

djangoアプリケーションでチェックボックスの値を取得する方法

分類Dev

クリックベースのCLIアプリケーションの単体テスト内でコンポーネントをモックする方法は?

分類Dev

VSCodeでSpringBootMavenプロジェクトを実行する方法とSpringBootWebアプリケーションのベースURLを構成する方法

分類Dev

APIベースのマルチページクライアント側アプリケーションでルーティングを整理する方法は?

分類Dev

WindowsマウスフックAPI関数を使用してC ++ Builderアプリケーションでマウスイベントをフックする方法は?

分類Dev

アノテーションを使用してメソッドレベルのアクセスチェックを実装することは可能ですか?

分類Dev

vueアプリケーションにPaypalサブスクリプションを実装する方法は?

分類Dev

Javaコンソールアプリケーションを実行するバッチ/シェルスクリプトを自動的に作成する方法

分類Dev

Linuxアプリケーション内で偽のウェイクアップをトリガーする方法は?

分類Dev

ページの読み込み間でnodejsチャットアプリケーションウィジェットを保持する方法は?

分類Dev

YesodアプリケーションのGHCiでデータベースクエリを実行する方法

分類Dev

私のAndroidクイズアプリケーションで各質問のタイムスロットを実装する方法

分類Dev

Javaアプリケーションのシリアル番号とアクティベーション保護を実装するための一般的なアプローチは何ですか?

分類Dev

チェックアウトされたgitリポジトリのスナップショット対応のバックアップ/レプリケーション-.gitディレクトリをrsyncする必要はありません

分類Dev

MeanJSアプリケーションでファイルのダウンロードを実装する方法

分類Dev

大規模なAngularアプリケーションでクリックイベントをキャプチャするためのアプリケーション全体のイベント検出メカニズムをどのように設計および実装しますか?

分類Dev

マルチスレッドアプリケーションAndroidでデータベースにアクセスする最良の方法は?

分類Dev

アプリ内タイムアウトセッションをフラッターで実装する方法

分類Dev

アプリ内タイムアウトセッションをフラッターで実装する方法

分類Dev

J2MEアプリケーションをバックグラウンドで実行する方法は?

Related 関連記事

  1. 1

    Javaアプリケーションとシェルスクリプト間の同期を実装する方法は?

  2. 2

    クラウドベースのSQLServerに対してデスクトップアプリケーションを実行することは可能ですか?

  3. 3

    アノテーションベースのSpring Bootアプリケーションにプラグインアーキテクチャを実装する

  4. 4

    React&Reduxアプリケーションのイベントハンドラーでアクションをディスパッチする方法

  5. 5

    チャットアプリケーション-Androidにチャットアプリを実装するのに適した技術はどれですか

  6. 6

    GUIベースのアプリケーションはバックグラウンドでシェルコマンドを実行しますか?

  7. 7

    GUIベースのアプリケーションはバックグラウンドでシェルコマンドを実行しますか?

  8. 8

    Androidアプリケーションにクライアント<->サーバー<->データベースアーキテクチャを実装する最良の方法は?

  9. 9

    openId Java Webベースのアプリケーションを実装する方法は?

  10. 10

    djangoアプリケーションでチェックボックスの値を取得する方法

  11. 11

    クリックベースのCLIアプリケーションの単体テスト内でコンポーネントをモックする方法は?

  12. 12

    VSCodeでSpringBootMavenプロジェクトを実行する方法とSpringBootWebアプリケーションのベースURLを構成する方法

  13. 13

    APIベースのマルチページクライアント側アプリケーションでルーティングを整理する方法は?

  14. 14

    WindowsマウスフックAPI関数を使用してC ++ Builderアプリケーションでマウスイベントをフックする方法は?

  15. 15

    アノテーションを使用してメソッドレベルのアクセスチェックを実装することは可能ですか?

  16. 16

    vueアプリケーションにPaypalサブスクリプションを実装する方法は?

  17. 17

    Javaコンソールアプリケーションを実行するバッチ/シェルスクリプトを自動的に作成する方法

  18. 18

    Linuxアプリケーション内で偽のウェイクアップをトリガーする方法は?

  19. 19

    ページの読み込み間でnodejsチャットアプリケーションウィジェットを保持する方法は?

  20. 20

    YesodアプリケーションのGHCiでデータベースクエリを実行する方法

  21. 21

    私のAndroidクイズアプリケーションで各質問のタイムスロットを実装する方法

  22. 22

    Javaアプリケーションのシリアル番号とアクティベーション保護を実装するための一般的なアプローチは何ですか?

  23. 23

    チェックアウトされたgitリポジトリのスナップショット対応のバックアップ/レプリケーション-.gitディレクトリをrsyncする必要はありません

  24. 24

    MeanJSアプリケーションでファイルのダウンロードを実装する方法

  25. 25

    大規模なAngularアプリケーションでクリックイベントをキャプチャするためのアプリケーション全体のイベント検出メカニズムをどのように設計および実装しますか?

  26. 26

    マルチスレッドアプリケーションAndroidでデータベースにアクセスする最良の方法は?

  27. 27

    アプリ内タイムアウトセッションをフラッターで実装する方法

  28. 28

    アプリ内タイムアウトセッションをフラッターで実装する方法

  29. 29

    J2MEアプリケーションをバックグラウンドで実行する方法は?

ホットタグ

アーカイブ