0
0
様々な条件があるクーポンのドメインモデリング
注文情報・顧客情報・期間限定セールなどに応じて
・注文全体への割引
・特定の商品への割引
・特定のカテゴリ(ブランド)への割引
・割引ではなく、固定金額になる。(クーポンを使うと200円など)
・送料無料
・回数制限
など
ドメインを跨いで影響する様々な仕様を持つクーポンと注文クラスについてのドメインモデリングについて
他の方がどのようにクラスを実装するか、お聞きしたいです。
状況
特に既存のシステムがあるわけではなく、注文と注文に対して使用できるクーポンがある場合にどのようにモデリングしたらいいだろうという意見交換です。
概要
・OrderクラスはCouponの配列を持ったImmutableなクラス。
・OrderDetailクラスは商品情報を持った注文詳細クラス(各注文商品の金額を持つ)
・CouponクラスはCouponInterfaceを使用することで、様々な仕様のクーポン機能が可能
Orderクラス
php
1<?php 2namespace App\Beer; 3 4use App\Beer\Interface\CouponInterface; 5 6class Order { 7 8 private ?int $id; 9 10 private int $price; 11 12 private Customer $customer; 13 14 private OrderDetails $orderDetails; 15 16 private Coupons $coupons; 17 18 private DeliveryFee $deliveryFee; 19 20 public function __construct( 21 ?int $id, 22 int $price, 23 Customer $customer, 24 OrderDetails $orderDetails, 25 Coupons $coupons, 26 DeliveryFee $deliveryFee, 27 ) { 28 $this->id = $id; 29 $this->price = $price; 30 $this->customer = $customer; 31 $this->orderDetails = $orderDetails; 32 $this->coupons = $coupons; 33 $this->deliveryFee = $deliveryFee; 34 } 35 36 public function getId(): ?int { 37 return $this->id; 38 } 39 40 public function getPrice():int { 41 return $this->price; 42 } 43 44 public function getTotalPrice():int { 45 return $this->price + $this->getDeliveryFee()->getValue(); 46 } 47 48 public function getCustomer():Customer { 49 return $this->customer; 50 } 51 52 public function getOrderDetails(): OrderDetails { 53 return $this->orderDetails; 54 } 55 56 public function getCoupons():Coupons { 57 return $this->coupons; 58 } 59 60 public function getDeliveryFee():DeliveryFee { 61 return $this->deliveryFee; 62 } 63 64 public function addCoupon(CouponInterface $coupon) { 65 $this->coupons = $this->coupons->addCoupon($coupon); 66 } 67 68 public function addOrderDetail(OrderDetail $orderDetail) { 69 $this->orderDetails->addOrderDetail($orderDetail); 70 } 71 72 public function hasProduct(Product $product):bool { 73 return $this->orderDetails->hasProduct($product); 74 } 75}
OrderDomainService
php
1<?php 2namespace App\Beer\DomainService; 3 4use App\Beer\Coupons; 5use App\Beer\Order; 6use App\Beer\Interface\CouponInterface; 7 8class OrderDomainService { 9 10 public function __construct( 11 12 ) { 13 14 } 15 16 public function couponIsApplicable(Order $order, CouponInterface $coupon):bool { 17 // TODO:: すでに適用されているクーポンとの状況も確認する必要がある 18 19 return $coupon->isApplicable($order); 20 } 21 22 public function applyCoupon(Order $order, CouponInterface $coupon): Order { 23 $order->addCoupon($coupon); 24 25 $appliedOrder = $this->calculateCouponsOrder($order); 26 27 return $appliedOrder; 28 } 29 30 private function calculateCouponsOrder(Order $order):Order { 31 $orders = []; 32 $coupons = $order->getCoupons()->asArray(); 33 34 $combinations = $this->permutation($coupons); 35 // クーポンの適用順で最も最小金額になる組み合わせを探す 36 foreach ($combinations as $coupons) { 37 $couponCollection = new Coupons($coupons); 38 $appliedOrder = $couponCollection->apply($order); 39 $orders[] = $appliedOrder; 40 } 41 // 最小注文金額のOrderを返す 42 $minTotalPriceOrder = $this->getMinTotalPriceOrder($orders); 43 44 return $minTotalPriceOrder; 45 } 46 47 private function getMinTotalPriceOrder(Array $orders):Order { 48 $totalPrices = []; 49 foreach($orders as $order) { 50 $totalPrices[] = $order->getTotalPrice(); 51 } 52 53 $minTotalPriceIndex = array_keys($totalPrices, min($totalPrices))[0]; 54 55 return $orders[$minTotalPriceIndex]; 56 } 57 58 // 本来は渡された配列の順列を返す 59 private function permutation(array $arr): array { 60 return [ 61 [$arr[0],$arr[1]], 62 [$arr[1],$arr[0]], 63 ]; 64 } 65}
Couponsクラス
php
1<?php 2namespace App\Beer; 3 4use App\Beer\Interface\CouponInterface; 5 6class Coupons { 7 8 private Array $coupons; 9 10 public function __construct( 11 Array $coupons, 12 ) { 13 $this->coupons = $coupons; 14 } 15 16 public function asArray():Array { 17 return $this->coupons; 18 } 19 20 public function addCoupon(CouponInterface $coupon):self { 21 $this->coupons[] = $coupon; 22 23 return new Coupons($this->coupons); 24 } 25 26 public function apply(Order $order): Order { 27 foreach($this->coupons as $coupon) { 28 $order = $coupon->apply($order); 29 } 30 31 return $order; 32 } 33}
ProductCouponクラス
php
1<?php 2namespace App\Beer; 3 4use App\Beer\Enums\CouponTarget; 5use App\Beer\Enums\DiscountType; 6use App\Beer\Interface\CouponInterface; 7 8class ProductCoupon implements CouponInterface { 9 10 private String $couponCode; 11 12 private int $specificPrice; 13 14 private Product $product; 15 16 public function __construct( 17 String $couponCode, 18 int $specificPrice, 19 Product $product 20 ) { 21 $this->couponCode = $couponCode; 22 $this->specificPrice = $specificPrice; 23 $this->product = $product; 24 } 25 26 public function isApplicable(Order $order): bool { 27 return $order->hasProduct($this->product); 28 } 29 30 public function apply(Order $order):Order { 31 $appliedOrderDetails = []; 32 foreach($order->getOrderDetails()->asArray() as $orderDetail) { 33 if ($orderDetail->getProduct()->getProductCode() == $this->product->getProductCode()) { 34 $appliedOrderDetail = new OrderDetail( 35 $orderDetail->getId(), 36 $this->specificPrice, 37 $this->product, 38 ); 39 $appliedOrderDetails[] = $appliedOrderDetail; 40 } else { 41 $appliedOrderDetails[] = $orderDetail; 42 } 43 } 44 $orderDetails = new OrderDetails($appliedOrderDetails); 45 46 $appliedOrder = new Order( 47 $order->getId(), 48 $orderDetails->getTotalPrice(), 49 $order->getCustomer(), 50 $orderDetails, 51 $order->getCoupons(), 52 $order->getDeliveryFee(), 53 ); 54 55 return $appliedOrder; 56 } 57 58 public function getCouponTarget(CouponTarget $couponTarget): CouponTarget 59 { 60 return CouponTarget::PRODUCT_CODE; 61 } 62 63 public function getDiscountType(DiscountType $discountType): DiscountType 64 { 65 return DiscountType::SPECIFIC_PRICE; 66 } 67}
CouponInterface
php
1<?php 2namespace App\Beer\Interface; 3 4use App\Beer\Enums\CouponTarget; 5use App\Beer\Enums\DiscountType; 6use App\Beer\Order; 7 8interface CouponInterface { 9 10 public function isApplicable(Order $order): bool; 11 12 public function apply(Order $order):Order; 13 14 public function getCouponTarget(CouponTarget $couponTarget):CouponTarget; 15 16 public function getDiscountType(DiscountType $discountType):DiscountType; 17 18}
聴きたい意見
クーポンの割引の適用処理をOrderクラスで行うべきか、OrderDomainServiceで行うべきか
Orderクラスからクーポンの適用処理を行おうとすると、金額を変更したOrderDetailや DeliveryFeeを持ったOrderクラス(自分自身)を返す必要があり、不自然な処理になっている気がします。
一方、OrderDomainServiceにOrderとCouponクラスを渡し、割引が適用されたOrderクラスを受け取るようにすると
適用処理はDomainServiceに任せているのに、Orderはどのクーポンが適用されているか把握する必要があるためCoupon配列を持っているという違和感を感じます。
現状、OrderDomainServiceで実装する方が良いと考えていますが、他の方法も含めて意見を聴きたいです。
回答3件
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。