购物车和订单中的折扣策略

我正在尝试实施一个系统,可以处理应用于我的购物车/已完成订单的多个折扣。 我已应用策略类型模式来封装折扣中的折扣处理。

我提出了以下内容:一个抽象的折扣基类,其子类构成了具体的折扣。 然后将这些应用于订单/购物车对象,并在添加到购物车/订单时处理订单/购物车的内容。

会喜欢附加代码的一些评论。 各种受保护的构造函数和成员标记为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 { 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 { 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 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 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 _LineItems = new List(); private IList _Discounts = new List(); 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 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 _Discounts = new List(); 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 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 } 

}

正如我在你的问题的评论中提到的,我不认为策略在这种情况下是恰当的。

对我来说,所有这些折扣BuyXGetYFree,SpendMoreThanXGetYDiscount等都是可用于计算产品/购物车成本的所有规则(并且可能并非所有必须都是关于获得折扣)。 我会使用您概述的规则构建一个RulesEngine,当您要求购物车根据RulesEngine计算其成本过程时。 RulesEngine将处理构成购物车的产品线和整体订单,并对成本等应用相关调整。

RulesEngine甚至可以控制规则的应用顺序。

规则可以是基于产品的(例如买一送一)或基于订单(例如,购买X物品获得免费送货),您甚至可以内置到期日期。这些规则将持久保存到数据存储。

对我来说, Decorator模式似乎更适用于此。 它以您拥有的类似折扣级别层次结构开始,但折扣也会实现OrderBase 。 然后他们装饰订单而不是仅仅依附于它。 当查询时,装饰器从它装饰的订单实例(可能是普通的香草订单或其他装饰器)获取订单数据,并向其应用适当的折扣。 IMO这很容易实现,但也足够灵活; 简而言之,对我而言,这是最简单的解决方案

装饰链中的折扣顺序可能不是任意的; 首先猜测你应该首先应用价格改变折扣,然后改变数量。 但我想这不是一个非常强大的约束。