C# Design Patterns 101: Builder Pattern Introduction

Photo by Danist Soh on Unsplash

C# Design Patterns 101: Builder Pattern Introduction

Welcome to this series that discusses various common software design patterns in C#. As with the other posts in this series, I don't assume any level of knowledge beyond knowing C# as a language, so let's start by discussing what software design patterns are.

According to our old faithful Wikipedia, a design pattern is:

In software engineering, a software design pattern is a general, reusable solution to a commonly occurring problem within a given context in software design.

Although the above description is pretty good, I think we can further enhance the understanding of design patterns using a real-world analogy. I like to see design patterns as a set of recipes much akin to what you would find in a cookbook. Where a cook would use recipes for creating things like sauces, pastries, cakes etc. consistently, a software engineer can use design patterns to consistently solve common engineering challenges.

What are the common types of challenges we face as software engineers?

  • Creating objects efficiently

  • Structuring our code in a way that remains flexible and efficient as it grows

  • Managing the behaviour and interaction of objects within our system

These challenges have ultimately led to design patterns being split into creational, structural and behavioural.

Builder Pattern

When considering the different types of design patterns, the builder (as the name might suggest) belongs to the creational category.

This pattern allows us to control the various elements of the objects that we are "building" in a piece-by-piece manner. This means that by using a single builder pattern, you can create many different incarnations of objects from the same creational code. Plus, anyway, we can reduce duplicated code and make it easier to read and use the better...right?

This is a particularly useful pattern to use when there would otherwise be a lot of parameters that could make up a particular object, but this could also be a sign that the class is breaking the single responsibility principle and could be split into more classes...anyway we digress.

Real-world Analogy

Consider when you go to Subway (the sandwich store) and would like to order a sandwich. Many different variations of sandwiches span much further than the base menu you see on the wall.

For example:

Bob likes a foot-long BLT on hearty Italian with lettuce, tomatoes, olives and a splash of ranch sauce

or

James likes a six-inch Chicken Tikka on herbs and cheese, with jalapeños and a lot of honey mustard

Given the complexity of the various permutations of sandwiches, Subway was clever enough to identify that the best way of doing things was to have all the options made available to the customer, allowing them to specify which pieces they wanted as they effectively build their perfect sandwich.

With the above in mind, we can consider the builder pattern in a similar way to a Subway ordering system. We can customise the different elements of the object we are building to satisfy our needs, much akin to that perfectly customised sandwich

C# Example

A great use-case for the builder pattern is assisting your testing capabilities as a means to reduce the duplication of code and make test object creation easier.

Take this example whereby we have an Order class (see below) that is passed into various parts of a system, with the details of the order object being used to define some other logic.

public class Order
{
    public Customer Customer { get; set; }
    public DateOnly ExpectedDeliveryDate { get; set; }
    public IEnumerable<Product> Products { get; set; }
    public Address DeliveryAddress { get; set; }
    public PaymentInformation Payment { get; set; }
}

For example, one of our tests might check the behaviour of another part of our system based on a property of an order, such as the delivery address. In this scenario, we might just consider newing-up an instance of the order class and setting the values:

public class OrderSystemTests
{
    [Fact]
    public void GreaterManchesterOrderGetsNextDayShippingDate()
    {
        var systemUnderTest = new OrderSystem();

        var order = new Order()
        {
            Customer = new Customer()
            {
                CustomerId = "46c35ca0-0d23-4b12-8467-e5e7022e7734"
                FirstName = "Joe",
                LastName = "Bloggs",
            },
            ExpectedDeliveryDate = new DateOnly(2023, 9, 1),
            DeliveryAddress = new Address()
            {
                BuildingNumber = "123",
                Street = "High Street",
                County = "Greater Manchester",
                Country = "United Kingdom",
                PostCode = "M1 1AD"
            }
        };

        var result = systemUnderTest.GetProposedShippingDate(order);

        Assert.Equal(new DateOnly(2023, 9, 2), result);
    }
}

If you were to start adding more tests, this would become tedious having to duplicate large portions of the order each time (e.g. the standard customer details, a base address etc.). Depending on the test in question, you might just want a bog-standard order object with a single property modified to something that will impact the result of the business logic within your application. This is where the builder pattern can be useful for creating test objects. The builder variation of the above example would look something like this:

public class OrderSystemTests
{
    [Fact]
    public void GreaterManchesterOrderGetsNextDayShippingDate()
    {
        var systemUnderTest = new OrderSystem();

        var order = new OrderBuilder
                        .WithDeliveryAddress(
                            new Address()
                            {
                                BuildingNumber = "123",
                                Street = "High Street",
                                County = "Greater Manchester",
                                Country = "United Kingdom",
                                PostCode = "M1 1AD"
                            }).Build();       

        var result = systemUnderTest.GetProposedShippingDate(order);

        Assert.Equal(new DateOnly(2023, 9, 2), result);
    }
}

So what is happening under the hood?

Firstly we can define the template for our builder by creating an interface that outlines the behaviours we are expecting:

public interface IOrderBuilder
{
    IOrderBuilder WithDeliveryAddress(Address address);
    IOrderBuilder WithCustomer(Customer customer);
    IOrderBuilder WithExpectedDeliveryDate(DateOnly expectedDeliveryDate);
    IOrderBuilder WithProducts(IEnumerable<Product> products);
    IOrderBuilder WithPaymentInformation(PaymentInformation payment);
    Order Build();
}

Next, we can implement the interface into a concrete class with actual logic for building our order object:

public class OrderBuilder : IOrderBuilder
{
    private Order _order = new Order();

    public IOrderBuilder WithDeliveryAddress(Address address)
    {
        _order.DeliveryAddress = address;
        return this;
    }

    public IOrderBuilder WithCustomer(Customer customer)
    {
        _order.Customer = customer;
        return this;
    }

    public IOrderBuilder WithExpectedDeliveryDate(DateOnly expectedDeliveryDate)
    {
        _order.ExpectedDeliveryDate = expectedDeliveryDate;
        return this;
    }

    public IOrderBuilder WithProducts(IEnumerable<Product> products)
    {
        _order.Products = products;
        return this;
    }

    public IOrderBuilder WithPaymentInformation(PaymentInformation payment)
    {
        _order.Payment = payment;
        return this;
    }

    public Order Build()
    {
        return _order;
    }
}

You will notice that each builder method (besides the Build() method) returns this. In this context, this refers to the instance of the builder class itself. This is another pattern known as a fluent API that allows the chaining of method calls, allowing us to do things like orderBuilder.WithPaymentInformation().WithProducts().WithCustomer().Build();

The Build() method then completes the creation of the object and returns the instance with the relevant modifications as per the method chaining we mentioned above.

Summary

So in this post we have explored the Builder Pattern in C# and recognised it as a creational pattern.

We can remember this pattern using the analogy of building your perfect sandwich at a Subway store (or your preferred sandwich store), whereby you can pick from an array of configurable elements to make up your sandwich.

The builder patterns also uses the fluent API pattern to allow method chaining, culminating in a single Build() method (or whatever you name it) that is in charge of returning our built-up object.