Builder Pattern With Mandatory Calls

Builder Pattern

Builder pattern is one of the creational design patterns which is used in almost all code bases. In this post, I would like to share a different approach to this popular pattern which eliminates error prone usages with high readability.

First things first, let’s take a look at the intent of builder pattern

The intent of the Builder design pattern is to separate the construction of a complex object from its representation.

And this intention is to solve problems like:

  • How can a class (the same construction process) create different representations of a complex object?
  • How can a class that includes creating a complex object be simplified?

In short, we can use builder pattern when we want to create complex objects with ease. The properties of the complex object can also vary depending on usage of the builder class.

How do we use it?

In our current project, we are also using builder pattern. In many cases, we have to create the same object type in a flexible way with some optional values. Builder classes can achieve these constructions with ease and the final code is highly readable.

Challenge

To fulfill a recent requirement we needed to build an object with a number of mandatory and optional field values together. Optional fields were not a problem, but the mandatory fields had to be initialized when the resulting object is constructed. This is one of the weak points of builder pattern.

Disadvantages of the Builder pattern include:

  • Data members of class aren’t guaranteed to be initialized.

But in our case, anytime the Build() method is called from the builder, we should get back an object which has all the mandatory fields initialized. Handling mandatory fields and keeping the code readable can be a difficult task.

General approach to dictate mandatory fields is to require them in the constructor of the builder class. On the other hand, in the case of too many mandatory fields, readability of the code decreases significantly and it becomes more difficult track the mandatory parameters. Achieving this functionality and keeping the builder interface developer-friendly is kind of challenging. This post discusses over an approach to solve this challenge. To elaborate further on the topic, here are the options that are generally used when a mix of mandatory and optional fields are used through builder pattern:

  • The recommended way with builder pattern is to add all your mandatory parameters to the constructor of the class and keep optional parameters in separate methods. This option is viable if you do not have too many mandatory parameters. Otherwise it can decrease readability of the code.
  • Use a combination of factory class that would default mandatory parameters per use case. This option is viable when you don’t have more than 2-3 build cases for the resulting object or some parameters can be defaulted.
  • Use several builders and combine the results in the end in object constructor. Each builder will create a part of the final result and they will be combined with a final builder to rule them all. This option can decrease code readability.
  • Use builders with mandatory method calls and chain them (chain builders or step builders).

Decision was to use builders with mandatory method calls which is the main idea to write this blog post and it was fun to brainstorm on it. Basically the idea is to help the developer step by step while making sure that mandatory fields are initialized. I have never seen a similar implementation before we actually created it for our project. But before writing this blog post, I did some search on existing blogs and saw that there were some Java implementations for the same concept. They were called chain builders and step builders. You can see them from the references section.

Show Me The Code

If you kept reading so far, it is great. This section is where the fun begins. I will try to give an example using an entity we use almost everyday. We will create a Contact just like the ones on your phone. Let’s consider these requirements:

In order to create a valid contact, following fields will be mandatory:

  • Name
  • Surname
  • Phone number

and we will have a few optional fields:

  • E-mail address
  • Address
  • Notes

and when we call the Build() anytime, resulting Contact object should have the mandatory fields in place.

Here is our Contact object:

1
2
3
4
5
6
7
8
9
10
11
public class Contact
{
// Mandatory properties
public string Name { get; set; }
public string Surname { get; set; }
public string PhoneNumber { get; set; }
// Optional properties
public string Email { get; set; }
public string Address { get; set; }
public string Notes { get; set; }
}

Naive Implementation

Please consider this most basic builder implementation in order to understand why it is better to enforce mandatory fields. Note that for simplicity purposes, I skipped any input validation for now.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public interface IContactBuilder
{
IContactBuilder WithName(string name);
IContactBuilder WithSurname(string surname);
IContactBuilder WithPhone(string phoneNumber);
IContactBuilder WithEmail(string email);
IContactBuilder WithAddress(string address);
IContactBuilder WithNotes(string notes);
Contact Build();
}

public class ContactBuilder : IContactBuilder
{
private readonly Contact contact;
public ContactBuilder()
{
contact = new Contact();
}
public IContactBuilder WithName(string name)
{
contact.Name = name;
return this;
}
public IContactBuilder WithSurname(string surname)
{
contact.Surname = surname;
return this;
}
public IContactBuilder WithPhone(string phoneNumber)
{
contact.PhoneNumber = phoneNumber;
return this;
}
public IContactBuilder WithEmail(string email)
{
contact.Email = email;
return this;
}
public IContactBuilder WithAddress(string address)
{
contact.Address = address;
return this;
}
public IContactBuilder WithNotes(string notes)
{
contact.Notes = notes;
return this;
}
public Contact Build()
{
return this.contact;
}
}

And on another class we consume this builder class to create a new instance of Contact.

1
2
3
4
5
6
7
8
public class BuilderConsumerClass
{
public BuilderConsumerClass(IContactBuilder builder)
{
var contact = builder
.Build();
}
}

This is what intellisense shows about our builder class with its current state:

intellisense-naive-implementation

So developers using this builder class can call any method in any order right now. The following call is a possible usage by a developer who does not know the requirements that we knew while implementing the builder class.

1
2
3
4
5
6
var builder = new ContactBuilder();

var contact = builder
.WithEmail("[email protected]")
.WithNotes("My notes")
.Build();

It is obvious that resulting Contact object violates our requirements. Mandatory fields are not entered yet.

How Can We Eliminate Error Prone Usages?

Well, normally for an example like this, requiring mandatory fields in the builder constructor is a very good approach. But for the sake of discussion let’s imagine that we have too many mandatory fields and using constructor initialization would decrease our code readability.

We would like to enforce our fellow developers to enter the mandatory fields and only after that they can choose to add optional fields or build the contact object. How can we achieve that? Let’s make Name field mandatory. Name field is handled by WithName() method in the builder class. In order to make WithName() call mandatory, we will extract the method to a new interface and create a chain to IContactBuilder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface IContactWithMandatoryNameBuilder
{
IContactBuilder WithName(string name);
}

public interface IContactBuilder
{
IContactBuilder WithSurname(string surname);
IContactBuilder WithPhone(string phoneNumber);
IContactBuilder WithEmail(string email);
IContactBuilder WithAddress(string address);
IContactBuilder WithNotes(string notes);
Contact Build();
}

IContactWithMandatoryNameBuilder will now contain the WithName() method of IContactBuilder. Concrete implementation ContactBuilder will now implement both of these interfaces. Here is how it will look after the refactoring.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ContactBuilder : IContactBuilder, IContactWithMandatoryNameBuilder
{
private readonly Contact contact;

public ContactBuilder()
{
contact = new Contact();
}

public IContactBuilder WithName(string name)
{
contact.Name = name;
return this;
}

// ...
// Rest is the same

From now on we will inject IContactWithMandatoryNameBuilder to our consumer class

1
2
3
4
5
6
7
8
9
public class BuilderConsumerClass
{
public BuilderConsumerClass(IContactWithMandatoryNameBuilder builder )
{
var contact = builder
.WithName("Alex")
.Build();
}
}

The new interface will show only WithName() method in the intellisense, nothing else:

Intellisense before mandatory call

Once the WithName() call has been made, our fellow developers will be able to access the Build() method. Now we guaranteed that Name field will be filled anytime Build() method is called.

Intellisense after mandatory call

Add More Mandatory Calls

Now that we have Name field as mandatory on the Contact object, it is simple to add more mandatory method calls. We will add more links to the chain and optional parameters will stay in the final builder interface. Let’s quickly make the remaining two fields Surname and PhoneNumber also mandatory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface IContactWithMandatoryNameBuilder
{
IContactWithMandatorySurnameBuilder WithName(string name);
}

public interface IContactWithMandatorySurnameBuilder
{
IContactWithMandatoryPhoneBuilder WithSurname(string surname);
}

public interface IContactWithMandatoryPhoneBuilder
{
IContactBuilder WithPhone(string phoneNumber);
}

public interface IContactBuilder
{
IContactBuilder WithEmail(string email);
IContactBuilder WithAddress(string address);
IContactBuilder WithNotes(string notes);
Contact Build();
}

Please note how the interfaces refer to each other just like links in a chain. In order to access Build() method our fellow developers will need to call WithName(), WithSurname(), WithPhone() methods. Another advantage is that the code is now self-describing. It is obvious that these calls have to be made (which means the fields that handled by these methods need to be filled in). Let’s take a look to final usage:

Intellisense after mandatory surname call

Intellisense after mandatory phone call

As you can see, at this point all the mandatory fields have been handled and it is the only way to access the Build() method. Now the developer can choose whether to add optional fields or not.

Intellisense final call

Remarks

If you decide to use this approach, please consider the following points:

  • Parameters that are related to each other can be grouped in the interface methods so maybe it is possible to shorten the chain.
  • If you can only see a few types of Contact being created in the codebase, it might be better to implement factory methods/classes in combination with the naive builder class. With that approach you can hide builder class under the hood and only allow calls to factory methods to get back the Contact you need.

Summary

Now that we know how it is done, mandatory meme will follow.

Chain them all

Creational design patterns are frequently used in almost any code base. Having minor tweaks on them, as described in this post, can be handy when you spot a requirement similar to the example. As my final words in this post, I would like to share the pros and cons of using this approach.

Pros

  • Easy to implement
  • Easy to understand
  • Eliminates error prone usages
  • Code is directly reflecting requirements (Self-describing)
  • Code readability is high

Cons

  • Unit testing on the link chain will require more effort
  • Double check would be necessary on IoC container registrations

Please let me know what you think in the comments below. I hope you find this post helpful. An example project related to this topic is available in blog-code-examples. Special thanks to my team members who helped me brainstorming and implementation on this topic.

Happy coding!

References