まず最初に、e-commereceWebサイトに2つの別々のアグリゲートBasketとOrderがあるとしましょう。
バスケットアグリゲートには、次のように定義された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があり、値オブジェクトとして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; }
}
ここで、ユーザーがバスケットにいくつかのアイテムを追加した後にチェックアウトしたい場合は、いくつかのアクションを適用する必要があります。
バスケットの更新(一部のアイテムの数量が変更された可能性があります)
新しい注文の追加/設定
バスケットの削除(またはDBで削除済みのフラグ)
特定の支払いゲートウェイを使用してクレジットカードで支払う。
ご覧のとおり、すべてのトランザクションの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();
}
}
注意すべきいくつかの重要なこと:
Cart
そしてCartItem
一つのことです。それらはデータベースから単一のユニットとしてロードされ、1つのトランザクションでそのまま保持されます。CanAdd
とAdd
メソッドがあります。このクラスのコンシューマーは、最初に呼び出してCanAdd
、発生する可能性のあるエラーをユーザーに伝達する必要があります。場合は、Add
より、事前検証なしで呼ばれているAdd
に確認しますCanAdd
いかなる不変に違反するようにしている場合、例外をスローし、になっているので、例外をスローすることはここで行うには正しいことであるAdd
と最初にチェックせずにはCanAdd
ソフトウェアのバグを表し、エラープログラマーをコミットすることによって;Cart
はエンティティであり、IDはありますCartItem
が、ValueObjectであり、IDはありません。顧客は同じアイテムで購入を繰り返すことができ、それでも別のカートになりますが、同じプロパティ(数量、価格、アイテム名)を持つCartItemは常に同じです-アイデンティティを構成するのはプロパティの組み合わせです。だから、私のドメインのルールを考えてみましょう:
これらは集約ルートによって強制され、不変条件を破ることができるような方法でクラスを誤用する方法はありません。
あなたはここで完全なモデルを見ることができます:ショッピングカートモデル
バスケットの更新(一部のアイテムの数量が変更された可能性があります)
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]
コメントを追加