Last active
March 17, 2023 03:42
-
-
Save FarrahStark/5437bfd71c045454eeb287fc30357cab to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Paul Miller | |
| // Please take a look at the code below. | |
| // Even though the program runs and generates correct result, we consider the code to be bad. | |
| // For example, if we want to expand the Christmas discount to January or if we want to introduce a first responder discount, the code can get really messy. | |
| // We ask you to refactor the code so that it's easier to apply new changes to it. | |
| // Once done, please send me a link to your gists. | |
| using System; | |
| using System.Collections.Generic; | |
| using static CodingChallenge.Shopping.Program; | |
| using System.Linq.Expressions; | |
| using System.Reflection; | |
| namespace CodingChallenge.Shopping | |
| { | |
| /// <summary> | |
| /// I used the strategy pattern to declaritively define discounts, and apply them based on the configured criteria. | |
| /// This solution allows easy extension of the types of discounts we can handle by adding new stategy types. | |
| /// Only one discount will be applied per item. Removes nested logic branching and allows new discounts to be added | |
| /// without breaking the criteria of the other discounts since the logic is isolated. | |
| /// </summary> | |
| class Program | |
| { | |
| static void Main(string[] args) | |
| { | |
| var program = new Program(); | |
| program.ChristmasShoppingAtTheGroceryStore(); | |
| program.BuyingFood(); | |
| } | |
| void ChristmasShoppingAtTheGroceryStore() | |
| { | |
| var carts = new List<CartItem> | |
| { | |
| new CartItem {ProductName = "Lights", Category = "Christmas", Price = 5.99m, Quantity = 10}, | |
| new CartItem {ProductName = "Tree", Category = "Christmas", Price = 169m, Quantity = 1}, | |
| new CartItem {ProductName = "Ornaments", Category = "Christmas", Price = 8m, Quantity = 15}, | |
| }; | |
| var calculator = new GroceryStoreCheckoutCalculator(); | |
| var total = calculator.Calculate(carts, new DateTimeOffset(2020, 11, 30, 0, 0, 0, TimeSpan.Zero)); | |
| Console.WriteLine(total); | |
| var totalAfterChristmas = calculator.Calculate(carts, new DateTimeOffset(2020, 12, 30, 0, 0, 0, TimeSpan.Zero)); | |
| Console.WriteLine(totalAfterChristmas); | |
| } | |
| void BuyingFood() | |
| { | |
| var carts = new List<CartItem> | |
| { | |
| new CartItem {ProductName = "Apple", Category = "Food", Price = 3.27m, Weight = 0.79m}, | |
| new CartItem {ProductName = "Scallop", Category = "Food", Price = 18m, Weight = 1.5m}, | |
| new CartItem {ProductName = "Salad", Category = "Food", Price = 6.99m, Quantity = 1}, | |
| new CartItem {ProductName = "Ground Beef", Category = "Food", Price = 7.99m, Weight = 1.5m}, | |
| new CartItem {ProductName = "Red Wine", Category = "Food", Price = 25.99m, Quantity = 1} | |
| }; | |
| var calculator = new GroceryStoreCheckoutCalculator(); | |
| var total = calculator.Calculate(carts, new DateTime(2020, 11, 30)); | |
| Console.WriteLine(total); | |
| var seniorHourTotal = calculator.Calculate(carts, new DateTime(2020, 11, 30, 7, 11, 0)); | |
| Console.WriteLine(seniorHourTotal); | |
| } | |
| } | |
| public class CartItem | |
| { | |
| public string ProductName { get; set; } | |
| public decimal Price { get; set; } | |
| public int Quantity { get; set; } | |
| public string Category { get; set; } | |
| public decimal Weight { get; set; } | |
| } | |
| public interface IDiscountStrategy<T> | |
| { | |
| decimal GetDiscountedPrice(T item); | |
| bool ShouldApplyDiscount(T item, DateTimeOffset checkoutTime); | |
| } | |
| public class DailyDiscountStrategy : IDiscountStrategy<CartItem> | |
| { | |
| private readonly int startHour; | |
| private readonly int endHour; | |
| private readonly int percentDiscount; | |
| private readonly string? category; | |
| public DailyDiscountStrategy(int startHour, int endHour, int percentDiscount, string? category = null) | |
| { | |
| this.startHour = Math.Clamp(startHour, 0, 23); | |
| this.endHour = Math.Clamp(endHour, 0, 23); | |
| this.percentDiscount = Math.Clamp(percentDiscount, 0, 100); | |
| this.category = category; | |
| } | |
| public decimal GetDiscountedPrice(CartItem item) | |
| { | |
| var quantifier = item.Weight == 0m ? item.Quantity : item.Weight; | |
| quantifier = quantifier <= 0 ? 0 : quantifier; | |
| return quantifier * item.Price * percentDiscount / 100m; | |
| } | |
| public bool ShouldApplyDiscount(CartItem item, DateTimeOffset checkoutTime) => | |
| (string.IsNullOrWhiteSpace(category) || item.Category == category) && | |
| checkoutTime.TimeOfDay.Hours > startHour && | |
| checkoutTime.TimeOfDay.Hours <= endHour; | |
| } | |
| public class PercentDiscountStrategy : IDiscountStrategy<CartItem> | |
| { | |
| private readonly DateTimeOffset startTime; | |
| private readonly DateTimeOffset endTime; | |
| private readonly string category; | |
| private readonly uint percentDiscount; | |
| public PercentDiscountStrategy( | |
| DateTimeOffset startTime, | |
| DateTimeOffset endTime, | |
| uint percentDiscount, | |
| string category = null, | |
| Expression<Func<CartItem, decimal>> quantifierProperty = null) | |
| { | |
| this.category = category; | |
| Expression<Func<CartItem, decimal>> defaultQuantifierProperty = x => x.Quantity; | |
| quantifierProperty ??= defaultQuantifierProperty; | |
| this.startTime = startTime; | |
| this.endTime = endTime; | |
| this.percentDiscount = Math.Max(percentDiscount, 100); // new feature. Can't discount more than 100% | |
| } | |
| public decimal GetDiscountedPrice(CartItem item) | |
| { | |
| return item.Quantity * (item.Price - item.Price * (percentDiscount / 100m)); | |
| } | |
| public bool ShouldApplyDiscount(CartItem item, DateTimeOffset checkoutTime) => | |
| (string.IsNullOrWhiteSpace(category) || item.Category == category) && | |
| checkoutTime >= startTime && | |
| checkoutTime <= endTime; | |
| } | |
| public class GroceryStoreCheckoutCalculator | |
| { | |
| // use strategy pattern to make discounts declarative instead of using logic branching | |
| // we could load this configuration from a datastore instead of hard coding it here | |
| private static readonly IDiscountStrategy<CartItem>[] CurrentDiscounts = | |
| { | |
| new PercentDiscountStrategy( | |
| new DateTimeOffset(2020,12,1,0,0,0, TimeSpan.Zero), | |
| new DateTimeOffset(2020,12,14,23,59,59, 999, TimeSpan.Zero), | |
| 20, | |
| "Christmas"), | |
| new PercentDiscountStrategy( | |
| new DateTimeOffset(2020,12,15,0,0,0, TimeSpan.Zero), | |
| new DateTimeOffset(2020,12,25,23,59,59, 999, TimeSpan.Zero), | |
| 60, | |
| "Christmas"), | |
| new PercentDiscountStrategy( | |
| new DateTimeOffset(2020,12,26,0,0,0, TimeSpan.Zero), | |
| new DateTimeOffset(2020,12,31,23,59,59, 999, TimeSpan.Zero), | |
| 90, | |
| "Christmas"), | |
| new DailyDiscountStrategy(6,8,10,"Food"), | |
| }; | |
| public decimal Calculate(List<CartItem> carts, DateTimeOffset checkOutDate) | |
| { | |
| decimal itemTotal = 0m; | |
| decimal getDiscountedPrice(CartItem item) | |
| { | |
| var discountStrategy = CurrentDiscounts.FirstOrDefault(strategy => strategy.ShouldApplyDiscount(item, checkOutDate)); | |
| if (discountStrategy == null) | |
| { | |
| return item.Quantity * item.Price; | |
| } | |
| return discountStrategy.GetDiscountedPrice(item); | |
| } | |
| foreach (var item in carts) | |
| { | |
| itemTotal += getDiscountedPrice(item); | |
| } | |
| return itemTotal; | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment