Press "Enter" to skip to content

How to implement the Builder Design Pattern in C#

What is the Builder Design Pattern?

The Builder Design Pattern is a creational design pattern. It’s used to create instances of objects whose constructors have many parameters, or when many properties need to be set before the object can be used.

The Builder pattern provides a simpler way to pass all those parameters or set all those values.

This example will be instantiating a Report object that would be used to create a report on orders at a company.

In the sample code, the constructor only needs five parameters. But you can see how it could potentially need many more.

Non-Pattern Version

The Report class below could be used to build a custom report.

The constructor takes in five parameters, based on user requirements of what orders to include in the report. The number of parameters could easily be much higher. The user may only want to include specific categories of products, regions, salespeople, etc.

A real version of this class could need 20 parameters (or properties that need to be set).

namespace DesignPatternsCSharpNet6.Builder.NonPatternVersion;
public class Report
{
    public enum SortingMethod
    {
        BySalesperson,
        ByTaxCategory
    }
    private DateTime _fromDate;
    private DateTime _toDate;
    private bool _includeReturnedOrders;
    private bool _includeUnshippedOrders;
    private SortingMethod _sortBy;
    public Report(DateTime from, DateTime to,
        bool includeReturnedOrders, bool includeUnshippedOrders, SortingMethod sortBy)
    {
        _fromDate = from;
        _toDate = to;
        _includeReturnedOrders = includeReturnedOrders;
        _includeUnshippedOrders = includeUnshippedOrders;
        _sortBy = sortBy;
    }
    public object CreatePDFReport()
    {
        // Pretend this object is a PDF report, 
        // built for the sales that match the passed-in constructor parameters.
        return new object();
    }
}

This unit test method shows how you might use the class. You need to pass in all the parameters, every time you instantiate a Report object.

Notice that the last three parameters of the Tax reports are the same values. In this example, when we create a tax report, we always want to use those values for those parameters.

The commission reports also use their own (different) values for the last three parameters. Those are the “standard” values for those three parameters – for commission reports.

using DesignPatternsCSharpNet6.Builder.NonPatternVersion;
namespace Test.DesignPatternsCSharpNet6.BuilderPattern.NonPatternVersion;
public class TestNonPatternReport
{
    [Fact]
    public void Test_BuildReports()
    {
        DateTime now = DateTime.UtcNow;
        Report currentMonthTaxReport =
            new Report(new DateTime(now.Year, now.Month, 1),
                new DateTime(now.Year, now.Month, 1).AddMonths(1).AddSeconds(-1),
                false, true, Report.SortingMethod.ByTaxCategory);
        Report currentYearTaxReport =
            new Report(new DateTime(now.Year, 1, 1),
                new DateTime(now.Year, 12, 31),
                false, true, Report.SortingMethod.ByTaxCategory);
        Report currentMonthCommissionReport =
            new Report(new DateTime(now.Year, now.Month, 1),
                new DateTime(now.Year, now.Month, 1).AddMonths(1).AddSeconds(-1),
                false, false, Report.SortingMethod.BySalesperson);
        Report currentYearCommissionReport =
            new Report(new DateTime(now.Year, 1, 1),
                new DateTime(now.Year, 12, 31),
                false, false, Report.SortingMethod.BySalesperson);
    }
}

Pattern Version

In this example, which uses the Builder Design Pattern, we have the same code for the Report class.

namespace DesignPatternsCSharpNet6.Builder.PatternVersion;
public class Report
{
    public enum SortingMethod
    {
        BySalesperson,
        ByTaxCategory
    }
    private DateTime _fromDate;
    private DateTime _toDate;
    private bool _includeReturnedOrders;
    private bool _includeUnshippedOrders;
    private SortingMethod _sortBy;
    public Report(DateTime from, DateTime to,
        bool includeReturnedOrders, bool includeUnshippedOrders, SortingMethod sortBy)
    {
        _fromDate = from;
        _toDate = to;
        _includeReturnedOrders = includeReturnedOrders;
        _includeUnshippedOrders = includeUnshippedOrders;
        _sortBy = sortBy;
    }
    public object CreatePDFReport()
    {
        // Pretend this object is a PDF report, 
        // built for the sales that match the passed-in constructor parameters.
        return new object();
    }
}

We’ll implement the Builder pattern with the ReportBuilder class.

This class has methods that reduce the number of parameters needed to construct a Report object.

It also ensures the tax reports all use the same set of parameters for the third through fifth paramers by having CreateMonthTaxReport and CreateYearTaxReport call the private CreateTaxReport function.

namespace DesignPatternsCSharpNet6.Builder.PatternVersion;
public class ReportBuilder
{
    public static Report CreateMonthTaxReport(int month, int year)
    {
        return CreateTaxReport(new DateTime(year, month, 1),
            new DateTime(year, month, 1).AddMonths(1).AddSeconds(-1));
    }
    public static Report CreateYearTaxReport(int year)
    {
        return CreateTaxReport(new DateTime(year, 1, 1),
            new DateTime(year, 12, 31));
    }
    private static Report CreateCommissionReport(DateTime from, DateTime to)
    {
        return new Report(from, to, false, false, Report.SortingMethod.BySalesperson);
    }
    public static Report CreateMonthCommissionReport(int month, int year)
    {
        return CreateCommissionReport(new DateTime(year, month, 1),
            new DateTime(year, month, 1).AddMonths(1).AddSeconds(-1));
    }
    public static Report CreateYearCommissionReport(int year)
    {
        return CreateCommissionReport(new DateTime(year, 1, 1),
            new DateTime(year, 12, 31));
    }
    private static Report CreateTaxReport(DateTime from, DateTime to)
    {
        return new Report(from, to, false, true, Report.SortingMethod.ByTaxCategory);
    }
}

Now, to instantiate a Report object, we can call the methods in the ReportBuilder class, as shown in this unit test code.

using DesignPatternsCSharpNet6.Builder.PatternVersion;
namespace Test.DesignPatternsCSharpNet6.BuilderPattern.PatternVersion;
public class TestPatternReport
{
    [Fact]
    public void Test_BuildReports()
    {
        Report currentMonthTaxReport =
            ReportBuilder.CreateMonthTaxReport(7, 2022);
        Report currentYearTaxReport =
            ReportBuilder.CreateYearTaxReport(2022);
        Report currentMonthCommissionReport =
            ReportBuilder.CreateMonthCommissionReport(7, 2022);
        Report currentYearCommissionReport =
            ReportBuilder.CreateYearCommissionReport(2022);
    }
}

Overview

There are many ways to implement the Builder Design Pattern.

You don’t need to use a static method in the ReportBuilder class. That is just the way I chose to do it in this example.

You could also use a ReportBuilder class that is instantiated. You could pass values into its constructor or set values on its public properties. Then, the builder class would use those values to “build” the class it is instantiating.

If you are simplifying object instantiation, by reducing the parameters (or properties you need to set in your calling code), you are using the Builder Design Pattern.

    Leave a Reply

    Your email address will not be published.

    This site uses Akismet to reduce spam. Learn how your comment data is processed.