2010-02-12 8 views
8

Sto cercando di implementare un sistema in grado di gestire più sconti applicati al mio carrello/ordini completati. Ho applicato un modello di tipo strategico per incapsulare l'elaborazione degli sconti all'interno degli sconti.Strategia di sconto nel carrello acquisti e ordini

Ho trovato il seguente: una classe di base di sconto astratto con sottoclassi che compongono gli sconti concreti. Questi vengono quindi applicati a un oggetto ordine/carrello e elaboreranno il contenuto dell'ordine/carrello quando aggiunti al carrello/ordine.

Mi piacerebbe qualche commento sul codice allegato. Vari costruttori e membri protetti hanno contrassegnato "virtuale" necessario per il divieto.

Chev

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

Forse potresti filtrare alcuni pezzi di codice specifici che vuoi inserire? –

+0

Grazie per la risposta. Per essere onesti, è più una questione di architettura che un problema specifico con il codice. – Chev

+0

Il modello di strategia non sembra essere giusto per me, specialmente se puoi avere più sconti applicati al carrello. Per me stai cercando di implementare una sorta di motore di regole. – David

risposta

2

Per me il Decorator pattern sembra più applicabile qui. Inizia con una simile gerarchia di classi di sconto che hai, ma gli sconti implementerebbero anche OrderBase. Quindi decorano l'ordine invece di essere semplicemente attaccati ad esso. Quando richiesto, il decoratore riceve i dati dell'ordine dall'istanza dell'ordine che decora (che può essere un semplice ordine di vaniglia o un altro decoratore) e applica ad esso lo sconto appropriato. IMO questo è abbastanza facile da implementare ma anche abbastanza flessibile; in breve, per me questa è la soluzione più semplice che potrebbe funzionare.

L'ordine degli sconti nella catena di decoratori non è probabilmente arbitrario; all'inizio suppongo che dovresti applicare prima gli sconti per la modifica del prezzo, poi quelli per la modifica della quantità. Ma immagino che questo non sia un vincolo molto forte.

+0

grazie per la risposta Peter. Avere un aspetto attraverso decoratore per vedere se si adatta ... – Chev

4

Come ho menzionato nei commenti alla tua domanda, non penso che la strategia sia adatta in questo caso.

Per me tutti questi sconti BuyXGetYFree, SpendMoreThanXGetYDiscount ecc. Sono tutte regole (e potrebbe non essere tutto sommato necessario per ottenere sconti) che possono essere applicate nel calcolo del costo del prodotto/carrello. Vorrei creare un RulesEngine utilizzando le regole che hai delineato e quando chiedi al carrello di calcolarne il costo, procedilo contro il RulesEngine. Il RulesEngine elaborerebbe le linee di prodotto che costituiscono il carrello e l'ordine generale e applicherà le relative rettifiche ai costi, ecc.

Il RulesEngine potrebbe persino controllare l'ordine in cui vengono applicate le regole.

regole potrebbe essere prodotto a base (per esempio Paghi uno prendi due) o un ordine basato (elementi ad es. Acquista X ottenere la spedizione gratuita) e si potrebbe anche avere date di scadenza costruiti in. Queste regole sarebbero persistito in un archivio dati.

Problemi correlati