2010-02-12 8 views
8

카트/완료 주문에 적용되는 여러 할인을 처리 할 수있는 시스템을 구현하려고합니다. 나는 할인 내에서 할인 처리를 캡슐화하기 위해 전략 유형 패턴을 적용했다.장바구니 및 주문의 할인 전략

나는 구체적인 할인을 구성하는 하위 클래스가있는 추상 할인 기본 클래스를 제안했다. 그런 다음 주문/장바구니 개체에 적용되며 장바구니/주문에 추가 할 때 주문/장바구니의 내용을 처리합니다.

첨부 된 코드에 대한 일부 의견을 환영합니다. nhibernate에 필요한 "가상"으로 표시된 다양한 보호 된 생성자 및 멤버.

체브 나에게

using System; 
using System.Collections.Generic; 
using System.Linq; 
using NUnit.Framework; 

namespace CodeCollective.RaceFace.DiscountEngine 
{ 
[TestFixture] 
public class TestAll 
{ 
    #region Tests 

    [Test] 
    public void Can_Add_Items_To_Cart() 
    { 
     Cart cart = LoadCart(); 

     // display the cart contents 
     foreach (LineItem lineItem in cart.LineItems) 
     { 
      Console.WriteLine("Product: {0}\t Price: {1:c}\t Quantity: {2} \t Subtotal: {4:c} \t Discount: {3:c} \t| Discounts Applied: {5}", lineItem.Product.Name, lineItem.Product.Price, lineItem.Quantity, lineItem.DiscountAmount, lineItem.Subtotal, lineItem.Discounts.Count); 
     } 
    } 

    [Test] 
    public void Can_Add_Items_To_An_Order() 
    { 
     // create the cart 
     Order order = new Order(new Member("Chev")); 

     // add items to the cart 
     GenericProduct hat = new GenericProduct("Cap", 110m); 
     order.AddLineItem(hat, 5); 

     EventItem race = new EventItem("Ticket", 90m); 
     order.AddLineItem(race, 1); 

     // add discounts 
     Discount percentageOff = new PercentageOffDiscount("10% off all items", 0.10m); 
     percentageOff.CanBeUsedInJuntionWithOtherDiscounts = false; 
     order.AddDiscount(percentageOff); 

     Discount spendXgetY = new SpendMoreThanXGetYDiscount("Spend more than R100 get 10% off", 100m, 0.1m); 
     spendXgetY.SupercedesOtherDiscounts = true; 
     order.AddDiscount(spendXgetY); 

     Discount buyXGetY = new BuyXGetYFree("Buy 4 hats get 2 hat free", new List<Product> { hat }, 4, 2); 
     buyXGetY.CanBeUsedInJuntionWithOtherDiscounts = false; 
     buyXGetY.SupercedesOtherDiscounts = true; 
     order.AddDiscount(buyXGetY); 

     // display the cart contents 
     foreach (LineItem lineItem in order.LineItems) 
     { 
      Console.WriteLine("Product: {0}\t Price: {1:c}\t Quantity: {2} \t Subtotal: {4:c} \t Discount: {3:c} \t| Discounts Applied: {5}", lineItem.Product.Name, lineItem.Product.Price, lineItem.Quantity, lineItem.DiscountAmount, lineItem.Subtotal, lineItem.Discounts.Count); 
     } 
    } 

    [Test] 
    public void Can_Process_A_Cart_Into_An_Order() 
    { 
     Cart cart = LoadCart(); 

     Order order = ProcessCartToOrder(cart); 

     // display the cart contents 
     foreach (LineItem lineItem in order.LineItems) 
     { 
      Console.WriteLine("Product: {0}\t Price: {1:c}\t Quantity: {2} \t Subtotal: {4:c} \t Discount: {3:c} \t| Discounts Applied: {5}", lineItem.Product.Name, lineItem.Product.Price, lineItem.Quantity, lineItem.DiscountAmount, lineItem.Subtotal, lineItem.Discounts.Count); 
     } 
    } 

    private static Cart LoadCart() 
    { 
     // create the cart 
     Cart cart = new Cart(new Member("Chev")); 

     // add items to the cart 
     GenericProduct hat = new GenericProduct("Cap", 110m); 
     cart.AddLineItem(hat, 5); 

     EventItem race = new EventItem("Ticket", 90m); 
     cart.AddLineItem(race, 1); 

     // add discounts 
     Discount percentageOff = new PercentageOffDiscount("10% off all items", 0.10m); 
     percentageOff.CanBeUsedInJuntionWithOtherDiscounts = false; 
     cart.AddDiscount(percentageOff); 

     Discount spendXgetY = new SpendMoreThanXGetYDiscount("Spend more than R100 get 10% off", 100m, 0.1m); 
     spendXgetY.SupercedesOtherDiscounts = true; 
     cart.AddDiscount(spendXgetY); 

     Discount buyXGetY = new BuyXGetYFree("Buy 4 hats get 2 hat free", new List<Product> { hat }, 4, 2); 
     buyXGetY.CanBeUsedInJuntionWithOtherDiscounts = false; 
     buyXGetY.SupercedesOtherDiscounts = true; 
     cart.AddDiscount(buyXGetY); 

     return cart; 
    } 

    private static Order ProcessCartToOrder(Cart cart) 
    { 
     Order order = new Order(cart.Member); 
     foreach(LineItem lineItem in cart.LineItems) 
     { 
      order.AddLineItem(lineItem.Product, lineItem.Quantity); 
      foreach(Discount discount in lineItem.Discounts) 
      { 
       order.AddDiscount(discount);  
      } 
     } 
     return order; 
    } 

    #endregion 
} 

#region Discounts 

[Serializable] 
public abstract class Discount : EntityBase 
{ 
    protected internal Discount() 
    { 
    } 

    public Discount(string name) 
    { 
     Name = name; 
    } 

    public virtual bool CanBeUsedInJuntionWithOtherDiscounts { get; set; } 
    public virtual bool SupercedesOtherDiscounts { get; set; } 
    public abstract OrderBase ApplyDiscount(); 
    public virtual OrderBase OrderBase { get; set; } 
    public virtual string Name { get; private set; } 
} 

[Serializable] 
public class PercentageOffDiscount : Discount 
{ 
    protected internal PercentageOffDiscount() 
    { 
    } 

    public PercentageOffDiscount(string name, decimal discountPercentage) 
     : base(name) 
    { 
     DiscountPercentage = discountPercentage; 
    } 

    public override OrderBase ApplyDiscount() 
    { 
     // custom processing 
     foreach (LineItem lineItem in OrderBase.LineItems) 
     { 
      lineItem.DiscountAmount = lineItem.Product.Price * DiscountPercentage; 
      lineItem.AddDiscount(this); 
     } 
     return OrderBase; 
    } 

    public virtual decimal DiscountPercentage { get; set; } 
} 

[Serializable] 
public class BuyXGetYFree : Discount 
{ 
    protected internal BuyXGetYFree() 
    { 
    } 

    public BuyXGetYFree(string name, IList<Product> applicableProducts, int x, int y) 
     : base(name) 
    { 
     ApplicableProducts = applicableProducts; 
     X = x; 
     Y = y; 
    } 

    public override OrderBase ApplyDiscount() 
    { 
     // custom processing 
     foreach (LineItem lineItem in OrderBase.LineItems) 
     { 
      if(ApplicableProducts.Contains(lineItem.Product) && lineItem.Quantity > X) 
      { 
       lineItem.DiscountAmount += ((lineItem.Quantity/X) * Y) * lineItem.Product.Price; 
       lineItem.AddDiscount(this);  
      } 
     } 
     return OrderBase; 
    } 

    public virtual IList<Product> ApplicableProducts { get; set; } 
    public virtual int X { get; set; } 
    public virtual int Y { get; set; } 
} 

[Serializable] 
public class SpendMoreThanXGetYDiscount : Discount 
{ 
    protected internal SpendMoreThanXGetYDiscount() 
    { 
    } 

    public SpendMoreThanXGetYDiscount(string name, decimal threshold, decimal discountPercentage) 
     : base(name) 
    { 
     Threshold = threshold; 
     DiscountPercentage = discountPercentage; 
    } 

    public override OrderBase ApplyDiscount() 
    { 
     // if the total for the cart/order is more than x apply discount 
     if(OrderBase.GrossTotal > Threshold) 
     { 
      // custom processing 
      foreach (LineItem lineItem in OrderBase.LineItems) 
      { 
       lineItem.DiscountAmount += lineItem.Product.Price * DiscountPercentage; 
       lineItem.AddDiscount(this); 
      } 
     } 
     return OrderBase; 
    } 

    public virtual decimal Threshold { get; set; } 
    public virtual decimal DiscountPercentage { get; set; } 
} 

#endregion 

#region Order 

[Serializable] 
public abstract class OrderBase : EntityBase 
{ 
    private IList<LineItem> _LineItems = new List<LineItem>(); 
    private IList<Discount> _Discounts = new List<Discount>(); 

    protected internal OrderBase() { } 

    protected OrderBase(Member member) 
    { 
     Member = member; 
     DateCreated = DateTime.Now; 
    } 

    public virtual Member Member { get; set; } 

    public LineItem AddLineItem(Product product, int quantity) 
    { 
     LineItem lineItem = new LineItem(this, product, quantity); 
     _LineItems.Add(lineItem); 
     return lineItem; 
    } 

    public void AddDiscount(Discount discount) 
    { 
     discount.OrderBase = this; 
     discount.ApplyDiscount(); 
     _Discounts.Add(discount); 
    } 

    public virtual decimal GrossTotal 
    { 
     get 
     { 
      return LineItems 
       .Sum(x => x.Product.Price * x.Quantity); 
     } 
    } 
    public virtual DateTime DateCreated { get; private set; } 
    public IList<LineItem> LineItems 
    { 
     get 
     { 
      return _LineItems; 
     } 
    } 
} 

[Serializable] 
public class Order : OrderBase 
{ 
    protected internal Order() { } 

    public Order(Member member) 
     : base(member) 
    { 
    } 
} 

#endregion 

#region LineItems 

[Serializable] 
public class LineItem : EntityBase 
{ 
    private IList<Discount> _Discounts = new List<Discount>(); 

    protected internal LineItem() { } 

    public LineItem(OrderBase order, Product product, int quantity) 
    { 
     Order = order; 
     Product = product; 
     Quantity = quantity; 
    } 

    public virtual void AddDiscount(Discount discount) 
    { 
     _Discounts.Add(discount); 
    } 

    public virtual OrderBase Order { get; private set; } 
    public virtual Product Product { get; private set; } 
    public virtual int Quantity { get; private set; } 
    public virtual decimal DiscountAmount { get; set; } 
    public virtual decimal Subtotal 
    { 
     get { return (Product.Price*Quantity) - DiscountAmount; } 
    } 
    public virtual IList<Discount> Discounts 
    { 
     get { return _Discounts.ToList().AsReadOnly(); } 
    } 
} 
#endregion 

#region Member 

[Serializable] 
public class Member : EntityBase 
{ 
    protected internal Member() { } 

    public Member(string name) 
    { 
     Name = name; 
    } 

    public virtual string Name { get; set; } 
} 

#endregion 

#region Cart 

[Serializable] 
public class Cart : OrderBase 
{ 
    protected internal Cart() 
    { 
    } 

    public Cart(Member member) 
     : base(member) 
    { 
    } 
} 

#endregion 

#region Products 

[Serializable] 
public abstract class Product : EntityBase 
{ 
    protected internal Product() 
    { 
    } 

    public Product(string name, decimal price) 
    { 
     Name = name; 
     Price = price; 
    } 

    public virtual string Name { get; set; } 
    public virtual decimal Price { get; set; } 
} 

// generic product used in most situations for simple products 
[Serializable] 
public class GenericProduct : Product 
{ 
    protected internal GenericProduct() 
    { 
    } 

    public GenericProduct(String name, Decimal price) : base(name, price) 
    { 
    } 
} 

// custom product with additional properties and methods 
[Serializable] 
public class EventItem : Product 
{ 
    protected internal EventItem() 
    { 
    } 

    public EventItem(string name, decimal price) : base(name, price) 
    { 
    } 
} 

#endregion 

#region EntityBase 

[Serializable] 
public abstract class EntityBase 
{ 
    private readonly Guid _id; 

    protected EntityBase() : this(GenerateGuidComb()) 
    { 
    } 

    protected EntityBase(Guid id) 
    { 
     _id = id; 
    } 

    public virtual Guid Id 
    { 
     get { return _id; } 
    } 

    private static Guid GenerateGuidComb() 
    { 
     var destinationArray = Guid.NewGuid().ToByteArray(); 
     var time = new DateTime(0x76c, 1, 1); 
     var now = DateTime.Now; 
     var span = new TimeSpan(now.Ticks - time.Ticks); 
     var timeOfDay = now.TimeOfDay; 
     var bytes = BitConverter.GetBytes(span.Days); 
     var array = BitConverter.GetBytes((long)(timeOfDay.TotalMilliseconds/3.333333)); 
     Array.Reverse(bytes); 
     Array.Reverse(array); 
     Array.Copy(bytes, bytes.Length - 2, destinationArray, destinationArray.Length - 6, 2); 
     Array.Copy(array, array.Length - 4, destinationArray, destinationArray.Length - 4, 4); 
     return new Guid(destinationArray); 
    } 

    public virtual int Version { get; protected set; } 

    #region Equality Tests 

    public override bool Equals(object entity) 
    { 
     return entity != null 
      && entity is EntityBase 
      && this == (EntityBase)entity; 
    } 

    public static bool operator ==(EntityBase base1, 
     EntityBase base2) 
    { 
     // check for both null (cast to object or recursive loop) 
     if ((object)base1 == null && (object)base2 == null) 
     { 
      return true; 
     } 

     // check for either of them == to null 
     if ((object)base1 == null || (object)base2 == null) 
     { 
      return false; 
     } 

     if (base1.Id != base2.Id) 
     { 
      return false; 
     } 

     return true; 
    } 

    public static bool operator !=(EntityBase base1, EntityBase base2) 
    { 
     return (!(base1 == base2)); 
    } 

    public override int GetHashCode() 
    { 
     { 
      return Id.GetHashCode(); 
     } 
    } 

    #endregion 

#endregion 
} 

}

+0

아마도 입력하려는 특정 코드 조각을 걸러 낼 수 있습니까? –

+0

답장을 보내 주셔서 감사합니다. 솔직히 말해 코드에 대한 특정 문제보다는 아키텍처 문제에 더 가깝습니다. – Chev

+0

전략 패턴이 나에게 맞는 것처럼 보이지 않습니다. 특히 카트에 여러 할인을 적용 할 수있는 경우가 있습니다. 내게 당신의 규칙 엔진의 일종을 구현 찾고. – David

답변

2

Decorator pattern 여기에 더 적용 할 것으로 보인다. 그것은 당신과 비슷한 Discount 클래스 계층 구조로 시작하지만 할인은 OrderBase을 구현합니다. 그런 다음 그들은 단지 그것에 붙이기보다는 주문을 장식합니다. 질의를 받으면 데코레이터는 데코 레이팅하는 주문 인스턴스 (보통 바닐라 주문 또는 다른 데코레이터 일 수 있음)에서 주문 데이터를 가져 와서 적절한 할인을 적용합니다. IMO 구현은 매우 쉽지만 충분히 유연합니다. 간단히 말해서 이것은 일 수있는 가장 간단한 해결책 인 입니다.

데코레이터 체인의 할인 순서는 아마 임의적이지는 않습니다. 처음에는 가격 변경 할인을 먼저 적용한 다음 수량 변경 할인을 적용해야합니다. 그러나 이것은 매우 강한 제약이 아니라고 생각합니다.

+0

피터 답장을 보내 주셔서 감사합니다. 그것이 맞는지보기 위해 꾸미기를 보면서 ... – Chev

4

질문에 대한 언급에서 언급했듯이 나는이 경우 전략이 적절하지 않다고 생각합니다.

나에게이 모든 할인 BuyXGetYFree, SpendMoreThanXGetYDiscount 등은 모두 상품/장바구니 비용 계산에 적용 할 수있는 모든 규칙 (모든 할인에 대해 반드시 필요한 것은 아님)입니다. 내가 규정 한 규칙과 RulesEngine에 대한 원가 계산 과정을 장바구니에 요청하면 RulesEngine을 구축 할 것입니다. RulesEngine은 카트 및 전체 주문을 구성하는 제품 라인을 처리하고 비용에 대한 관련 조정을 적용합니다.

룰 엔진은 룰이 적용되는 순서를 제어 할 수도 있습니다.

규칙은 제품 기반 (예 : 하나 구입 무료) 또는 주문 기반 (예 : X 항목 구매는 무료 배송) 일 수 있으며 만료 날짜가있을 수도 있습니다. 이러한 규칙은 데이터 저장소에 보관됩니다.