Дисконтная стратегия в корзине и заказах

Я пытаюсь реализовать систему, которая может обработать несколько скидок, к которым относятся мой везла/завершаемая заказы. Я применил шаблон типа стратегии для инкапсуляции обработки скидок в скидках.

Я придумал следующее: абстрактный дисконтный базовый класс с подклассами, составляющими конкретные скидки. Они затем применяются к любому объект порядка/корзины и обработают содержание порядка/корзины при добавлении к корзине/порядку.

Любил бы некоторые комментарии к присоединенному коду. Различные защищенные конструкторы и участники, отмеченные "виртуальный" необходимый для nhibernate.

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
}

}

8
задан Samuel G 12 February 2010 в 16:47
поделиться

2 ответа

На мой взгляд, здесь более применим паттерн Decorator. Он начинается с аналогичной иерархии класса Discount, которая есть у вас, но скидки также реализуют OrderBase. Затем они украшают заказ, а не просто прикрепляются к нему. При запросе декоратор получает данные заказа из украшенного им экземпляра заказа (который может быть обычным ванильным заказом или другим декоратором) и применяет к нему соответствующую скидку. IMO это довольно просто реализовать, но при этом достаточно гибко; короче говоря, для меня это самое простое решение, которое может работать.

Порядок скидок в цепочке декораторов, вероятно, не произволен; на первый взгляд, сначала следует применять скидки, изменяющие цену, затем скидки, изменяющие количество. Но я полагаю, что это не очень сильное ограничение.

2
ответ дан 5 December 2019 в 22:18
поделиться

Как я уже упоминал в комментариях к вашему вопросу Я не думаю, что в данном случае стратегия уместна.

Для меня все эти скидки BuyXGetYFree, SpendMoreThanXGetYDiscount и т. Д. Являются правилами (и не обязательно все они связаны с получением скидки), которые можно применить при расчете стоимости товара / корзины. Я бы построил RulesEngine, используя описанные вами правила, и когда вы попросите корзину рассчитать свою стоимость, обработайте ее в соответствии с RulesEngine. RulesEngine будет обрабатывать продуктовые линейки, составляющие корзину и общий порядок, и применять соответствующие корректировки к затратам и т. Д.

RulesEngine может даже контролировать порядок, в котором применяются правила.

Правила могут быть основаны на продукте (например, «Купи один - получишь один бесплатно») или на основе заказа (например, «Купи X товаров - получишь бесплатную доставку»), и вы даже можете иметь встроенные даты истечения срока действия. Эти правила будут сохранены в хранилище данных.

4
ответ дан 5 December 2019 в 22:18
поделиться
Другие вопросы по тегам:

Похожие вопросы: