Working with List<T> with builder pattern - c#

Viewed 1064

Apologies for the title as I could not quite word my issue, so here we go.

I have a DiscountBuilder class that has two methods WithBuyOneGetOneFree and WithDiscount. They both take a List<ShoppingItem> and work well in isolation like this:

var items = BasketStore.ShoppingItems;

var updatedItems = new DiscountBuilder()
    .WithBuyOneGetOneFree(items)
    .Create();
    
var updatedItems2 = new DiscountBuilder()
    .WithDiscount(75, items)
    .Create();

But when I attempt to chain the both methods to apply WithBuyOneGetOneFree and WithDiscount I only get the list of shopping items returned with WithBuyOneGetOneFree which is completely logical and understandable:

var updatedItems3 = new DiscountBuilder()
    .WithDiscount(75, items)
    .WithBuyOneGetOneFree(items)
    .Create();

What is the best way/design to achieve this so all three examples above all work in complete harmony, so I have the option to use the methods individual like now, and chain the methods together to get both discounts?

Here is the rest of the code.

DiscountBuilder:

#region Private Fields
private List<ShoppingItem> internalShoppingItems;
#endregion

#region Public Methods
public List<ShoppingItem> Create()
{
    return internalShoppingItems;
}

public IDiscountBuilder WithBuyOneGetOneFree(List<ShoppingItem> shoppingItems)
{
    internalShoppingItems = new List<ShoppingItem>();

    foreach(var item in shoppingItems)
    {
        var updateItem = new ShoppingItem
        {
            Id = item.Id,
            Price = item.Price,
            Name = item.Name,
            Quantity = item.Quantity + 1
        };

        internalShoppingItems.Add(updateItem);
    }
    return this;
}

public IDiscountBuilder WithDiscount(int percentage, List<ShoppingItem> shoppingItems)
{
    internalShoppingItems = new List<ShoppingItem>();

    foreach (var item in shoppingItems)
    {
        var updateItem = new ShoppingItem
        {
            Id = item.Id,
            Price = GetDiscountedPrice(percentage, item.Price),
            Name = item.Name,
            Quantity = item.Quantity
        };

        internalShoppingItems.Add(updateItem);
    }
    return this;
}
#endregion

ShoppingItem:

public class ShoppingItem
{
    public int Id { get; set; }
    public double Price { get; set; }
    public string Name { get; set; }
    public int Quantity { get; set; }
}

BasketStore (just where the original dummy data is coming from):

public static class BasketStore
{
    #region Properties
    public static List<ShoppingItem> ShoppingItems { get; private set; }
    #endregion

    #region Constructor
    static BasketStore()
    {
        ShoppingItems = new List<ShoppingItem>()
        {
            new ShoppingItem {
                Id = 1,
                Price = 10.0,
                Name = "IWatch",
                Quantity = 1
            },
            new ShoppingItem {
                Id = 5,
                Price = 9.99,
                Name = "Ladies Watch",
                Quantity = 3
            },
            new ShoppingItem {
                Id = 7,
                Price = 1.75,
                Name = "Ladies Replacement Strap",
                Quantity = 1
            }
        };
    }
    #endregion
}
1 Answers

This might be a classic XY problem. The problem is that each fluent action is overriding the internal list, so last applied discount wins and changes the list.

In terms of best way/design, there are too many possible approaches to this problem.

Here is one simple example.

Change the approach of storing the internal list and overriding it.

public interface IDiscountBuilder {
    IDiscountBuilder WithBuyOneGetOneFree();
    IDiscountBuilder WithDiscount(int percentage);
    List<ShoppingItem> Create(List<ShoppingItem> shoppingItems);
}

Instead store the actions to be applied to the items in the list and then create the list based on the provided input.

For example

public class DiscountBuilder : IDiscountBuilder {
    #region Private Fields        
    private Action<ShoppingItem> buyOneGetOneFree;
    private Action<ShoppingItem> withDiscount;
    #endregion

    #region Public Methods
    public List<ShoppingItem> Create(List<ShoppingItem> shoppingItems) {
        List<ShoppingItem> result = new List<ShoppingItem>();
        foreach (ShoppingItem item in shoppingItems) {
            //copy item details
            ShoppingItem updateItem = new ShoppingItem() {
                Id = item.Id,
                Price = item.Price,
                Name = item.Name,
                Quantity = item.Quantity
            };
            //apply actions
            buyOneGetOneFree?.Invoke(updateItem);
            withDiscount?.Invoke(updateItem);
            result.Add(updateItem);
        }
        return result;
    }

    public IDiscountBuilder WithBuyOneGetOneFree() {
        buyOneGetOneFree = item => {
            item.Quantity = item.Quantity + 1;
        };
        return this;
    }

    public IDiscountBuilder WithDiscount(int percentage) {
        withDiscount = item => {
            item.Price = GetDiscountedPrice(percentage, item.Price);
        };
        return this;
    }

    private double GetDiscountedPrice(int percentage, double price) {
        return price - (price * (percentage / 100D));
    }
    #endregion
}

All 3 examples in the original question can be applied independent of each other

var items = BasketStore.ShoppingItems;

var updatedItems = new DiscountBuilder()
    .WithBuyOneGetOneFree()
    .Create(items);

var updatedItems2 = new DiscountBuilder()
    .WithDiscount(75)
    .Create(items);

var updatedItems3 = new DiscountBuilder()
    .WithDiscount(75)
    .WithBuyOneGetOneFree()
    .Create(items);

This approach can be expanded even further and multiple ways but that would outside of the scope of the original question.

Related