Constructing Immutable Objects with a Builder

June 12th 2020 Design Patterns C#

Immutable objects can't change after they've been created. Because of this, all data needed for their initialization must be passed into them through the constructor. This can result in constructors with (too) many parameters. With the builder design pattern, this can be avoided.

In C#, read-only properties without setters are often used to hold data in immutable classes:

public class ImmutablePerson
{
  public string FirstName { get; }
  public string LastName { get; }
  public string? MiddleName { get; }
  public IEnumerable<string> ChildrenNames { get; }
}

These can only be set with an initializer or inside the constructor:

public ImmutablePerson(
  string firstName,
  string lastName,
  string? middleName = null,
  IEnumerable<string>? childrenNames = null)
{
  FirstName = firstName;
  LastName = lastName;
  MiddleName = middleName;
  ChildrenNames = childrenNames ?? new string[0];
}

The constructor for any reasonably sized class will end up having many parameters so that all the properties can be initialized. This quickly makes the calling code difficult to understand:

var immutable = new ImmutablePerson("John", "Doe", "Don", new[] { "Jane" });

Named parameters make this somewhat better but require developer discipline to consistently use them:

var immutable = new ImmutablePerson(
  firstName: "John",
  lastName: "Doe",
  middleName: "Don",
  childrenNames: new[] { "Jane" });

Of course, optional parameters for values with valid defaults can be omitted to make code shorter:

var immutable = new ImmutablePerson("John", "Doe");

The builder design pattern can be used to make this problem more manageable as the number of parameters grows:

Builder design pattern

To give the builder access to private members of the target class, it should be created as its inner class:

public class ImmutablePerson
{
  public class Builder
  {
  }
}

Now, the target class only needs a private constructor with the builder as its parameter. It can initialize its values by reading them from the builder:

private ImmutablePerson(Builder builder)
{
  FirstName = builder.FirstName;
  LastName = builder.LastName;
  MiddleName = builder.MiddleName;
  ChildrenNames = builder.ChildrenNames;
}

This means that the builder will have to expose these properties. They won't be immutable so they can have private setters:

public class Builder
{
  internal string FirstName { get; private set; }
  internal string LastName { get; private set; }
  internal string? MiddleName { get; private set; }
  internal IEnumerable<string> ChildrenNames { get; private set; } = new string[0];
}

The builder constructor still needs to initialize the properties without valid default values so that it's always in a valid state:

public Builder(string firstName, string lastName)
{
  FirstName = firstName;
  LastName = lastName;
}

For initializing the other properties, separate methods can be created. They return the builder instance to allow the more convenient fluent interface:

public Builder SetMiddleName(string middleName)
{
  MiddleName = middleName;
  return this;
}

public Builder SetChildrenNames(IEnumerable<string> childrenNames)
{
  ChildrenNames = childrenNames;
  return this;
}

The actual target class instance is created by calling the Build method which simply calls the previously implemented private constructor:

public ImmutablePerson Build()
{
  return new ImmutablePerson(this);
}

The calling code is more verbose but also much easier to follow:

var immutable = new ImmutablePerson.Builder("John", "Doe")
  .SetMiddleName("Don")
  .SetChildrenNames(new[] { "Jane" })
  .Build();

You can check the full code for the ImmutablePerson class and its Builder class on GitHub.

The builder pattern hides the complexities of creating a class. In the case of an immutable class, it can be used to avoid constructors with too many parameters. Since the builder is not immutable, the values can be set through multiple calls. The builder itself is then used as a source of values for the immutable class. Adding a fluent interface for the builder will allow the immutable class to still be created with only a single chain of calls.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License