by Zoran Horvat
Builder design pattern is useful in situations when we have to construct an object, but construction process is rather complicated. Typically it would be a process that takes multiple steps. Possibly other, smaller objects would have to be constructed before stepping to the construction of the target object.
In this article we will walk through several examples in order to see where the complications are coming from when using the Builder design pattern. We will gradually progress from basic to more advanced examples. And, as we progress, you will see that the builder interface becomes more and more complex. At some point it will be so complicated that it will not be worth using. Even worse, overly complicated builder will become an obstacle and source of defects, because callers will often forget to make certain calls or will make calls in unexpected order.
When we hit that limit, we will have to reconsider the design method applied to construct the builder class. By the end of this article you will see a completely different approach to builders, approach which significantly reduces chances for defects caused by not understanding the intended use. This approach also increases the overall code readability.
We can consider the problem of constructing a Person object. This object will have to contain a number of smaller objects, such as strings holding name and surname, or a primary contact, etc. As these are mandatory parameters, we will add them as constructor arguments.
Further on, Person object will have to contain more contact points, such as business phone, email address and similar. We will want to add these to the Person object when created. Code is telling more than words, so here is the Person class implementation:
class Person
{
private string Name { get; }
private string Surname { get; }
private IContact PrimaryContact { get; set; }
private IList<IContact> AllContacts { get; }
public Person(string name, string surname, IContact primaryContact)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException(nameof(name));
if (string.IsNullOrEmpty(surname))
throw new ArgumentException(nameof(surname));
this.Name = name;
this.Surname = surname;
this.AllContacts = new List<IContact>();
this.SetPrimaryContact(primaryContact);
}
public void SetPrimaryContact(IContact contact)
{
this.AddContact(contact);
this.PrimaryContact = contact;
}
public void AddContact(IContact contact)
{
if (contact == null)
throw new ArgumentNullException(nameof(contact));
this.AllContacts.Add(contact);
}
}
We could come up with even more elements, but this will be enough to prove the point. Name and surname strings will be easy to provide. Only the IContact objects would be more complicated. And the protocol of adding contacts is even more complicated because those are separated into a single primary contact and a multitude of other contacts.
Suppose that in our system we have two kinds of contacts: phone numbers and email addresses:
interface IContact
{
}
class PhoneNumber : IContact
{
private string AreaCode { get; }
private string Number { get; }
public PhoneNumber(string areaCode, string number)
{
if (string.IsNullOrEmpty(areaCode))
throw new ArgumentException(nameof(areaCode));
if (string.IsNullOrEmpty(number))
throw new ArgumentException(nameof(number));
// ... more validation, e.g. regular expression
this.AreaCode = areaCode;
this.Number = number;
}
}
class EmailAddress : IContact
{
private string Address { get; }
public EmailAddress(string address)
{
if (string.IsNullOrEmpty(address))
throw new ArgumentException(nameof(address));
// ... more validation, e.g. regular expression
this.Address = address;
}
}
Once again we only have a basic implementation here, but that will be enough to start the demonstration.
And the demonstration begins by recognizing the need to use the Builder design pattern. Basically, we have three possible levels of engagement when constructing objects:
In this article we will only deal with the last and at the same time most demanding approach.
We could construct an abstract builder which builds an abstract Person class directly from the Person’s public interface:
interface IPerson
{
void SetPrimaryContact(IContact primaryContact);
void AddContact(IContact contact);
}
interface IPersonBuilder
{
void SetName(string name);
void SetSurname(string surname);
void SetPrimaryContact(IContact primaryContact);
void AddContact(IContact contact);
IPerson Build();
}
This would be the most typical Builder interface we could make. It exposes a set of methods that let us prepare the contents of the target object. Once we are done with that, there will be the Build method waiting for us. This method is then supposed to construct concrete Person object and return it.
Of course, the Person class is now supposed to implement the IPerson interface:
class Person: IPerson
{
...
}
Not much fun. These interfaces were added just to separate the implementation from abstract types we are dealing with. We could as well live without them, though the code would be somewhat awkward to read.
Anyway, the time has come to provide the first concrete builder implementation. Here is the first attempt:
class PersonBuilder : IPersonBuilder
{
private string Name { get; set; }
private string Surname { get; set; }
private IContact PrimaryContact { get; set; }
private IList<IContact> OtherContacts { get; } = new List<IContact>();
public void SetName(string name)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException(nameof(name));
this.Name = name;
}
public void SetSurname(string surname)
{
if (string.IsNullOrEmpty(surname))
throw new ArgumentException(nameof(surname));
this.Surname = surname;
}
public void SetPrimaryContact(IContact primaryContact)
{
if (primaryContact == null)
throw new ArgumentNullException(nameof(primaryContact));
this.PrimaryContact = primaryContact;
}
public void AddContact(IContact contact)
{
if (contact == null)
throw new ArgumentNullException(nameof(contact));
this.OtherContacts.Add(contact);
}
public IPerson Build()
{
// The next line may throw exception if data were not correct
IPerson person = new Person(this.Name, this.Surname, this.PrimaryContact);
foreach (IContact contact in this.OtherContacts)
person.AddContact(contact);
return person;
}
}
The last method, Build(), is the essence of the whole process. It takes all the data collected so far and pushes them into the Person object prior to returning it.
But this Build() method implementation causes concern. Calling the Person constructor could fail if any of the arguments passed is null. And that is not because we have allowed null references to sneak into the PersonBuilder class. That is not possible, because all Set methods are protected against null arguments. The problem is in the state of the builder object itself: If we forget to call any of the Set methods, the corresponding private property will remain null.
It is easy to break the builder like this by simply calling its Build() method without setting any parameter:
IPersonBuilder builder = new PersonBuilder();
IPerson person = builder.Build(); // fails
We could try to mitigate the issue by forcing the mandatory parameters to be set through the builder's constructor. But then the client would have to initialize the builder the same way as it had to initialize the Person object, rendering the whole builder idea useless.
We will start solving the problem by first accepting it as the limitation of this Builder implementation. Then we will move on to better solutions.
The first idea we can apply is to simply admit that the PersonBuilder object might not be ready to construct the Person object. This happens if some of the mandatory constructor parameters of the Person class have not been set in the builder. Calling the Person constructor will then fail for sure.
This is the implementation:
class PersonBuilder : IPersonBuilder
{
private string Name { get; set; }
private bool IsNameSet => !string.IsNullOrEmpty(this.Name);
private string Surname { get; set; }
private bool IsSurnameSet => !string.IsNullOrEmpty(this.Surname);
private IContact PrimaryContact { get; set; }
private bool IsPrimaryContactSet => this.PrimaryContact != null;
private bool CanBuildPerson =>
this.IsNameSet && this.IsSurnameSet && this.IsPrimaryContactSet;
private IList<IContact> OtherContacts { get; } = new List<IContact>();
public void SetName(string name)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException(nameof(name));
this.Name = name;
}
public void SetSurname(string surname)
{
if (string.IsNullOrEmpty(surname))
throw new ArgumentException(nameof(surname));
this.Surname = surname;
}
public void SetPrimaryContact(IContact primaryContact)
{
if (primaryContact == null)
throw new ArgumentNullException(nameof(primaryContact));
this.PrimaryContact = primaryContact;
}
public void AddContact(IContact contact)
{
if (contact == null)
throw new ArgumentNullException(nameof(contact));
this.OtherContacts.Add(contact);
}
public IPerson Build()
{
if (!this.CanBuildPerson)
throw new InvalidOperationException("Some data are missing.");
IPerson person = new Person(this.Name, this.Surname, this.PrimaryContact);
foreach (IContact contact in this.OtherContacts)
person.AddContact(contact);
return person;
}
}
In this implementation we are keeping track of mandatory data that have been set. If any of the three mandatory objects - name, surname, primary contact - is missing when the Build() method is invoked, the method will give up and throw InvalidOperationException.
It's not a mistake to say that the Build() method has given up. Throwing the InvalidOperationException is the defeat of the design. This exception indicates that the object is in an inconsistent state. Was the design better, this situation would never happen - it would be impossible to hold a reference to an inconsistent builder object.
On a deeper level, this design is a failure because the client must know which methods are mandatory and which are not or otherwise the whole operation will fail. What we need to make it better is to disallow the caller to access methods that must not be invokable in the given state of the builder object. That is the idea we will investigate in the next section.
When looking at the Person object we want to construct, we note that some of the data are mandatory, while others are optional. This gives rise to an idea to try to navigate the client through the process of setting up the mandatory data on the future Person object, so that nothing can possibly be missing.
Consider this basic implementation of the PersonBuilder class:
class PersonBuilder
{
private PersonBuilder() { }
public static IExpectSurnamePersonBuilder WithName(string name)
{
...
}
}
The IExpectSurnamePersonBuilder is a magical interface, which we will define in a minute. What catches our attention in this design is that the PersonBuilder class cannot be instantiated anymore. It has only one constructor and even that one is private and therefore inaccessible to the outsiders. The only way to construct any object using this PersonBuilder class is to use the WithName() public method.
On the other hand, this public method does not return the PersonBuilder, but the object which expects surname. This already exposes the basic idea behind this approach: The role of the PersonBuilder class is to navigate the caller through mandatory operations, one at the time, and ask for the data in order which is guaranteed to be correct - first name first, surname next. Primary contact will follow, and then optional alternate contacts will be asked for.
To see the idea better, we can provide the IExpectSurnamePersonBuilder interface:
interface IExpectSurnamePersonBuilder
{
IExpectPrimaryContact WithSurname(string surname);
}
This should not be a surprise. The interface which accepts surname returns back the next interface in order - the one which defines the request for the primary contact, yet another mandatory piece of data.
Anyway, where would the implementation of this interface reside then? One very interesting place is the PersonBuilder itself. The trick is that the PersonBuilder can implement IExpectSurnamePersonBuilder without affecting its users. Remember, users would not be able to obtain a PersonBuilder object, because it doesn't have a public constructor.
To cut the long story short, we can let the PersonBuilder become the IExpectSurname object as well, in addition to the responsibility of holding the person's name:
class PersonBuilder : IExpectSurnamePersonBuilder
{
private string Name { get; }
private PersonBuilder(string name)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException(nameof(name));
this.Name = name;
}
public static IExpectSurnamePersonBuilder WithName(string name)
{
return new PersonBuilder(name);
}
public IExpectPrimaryContact WithSurname(string surname)
{
...
}
}
This is the proper implementation of the WithName method. It constructs a new PersonBuilder object with specified name and simply returns it. The caller might never know that this object implementing the IExpectSurnamePersonBuilder interface is actually the PersonBuilder object. And there is no reason for him to bother with that detail. Working with these small interfaces is a convenient way to navigate the client through the sequence of mandatory steps it must make.
Once holding the IExpectSurname interface, the caller will be able to make a call to the WithSurname method and provide the valid surname. This will lead it even further through the process, where primary contact will be expected.
We can follow this pattern all to the end. The process of implementing builders in this way is to split the process of building the target object into stages. Each stage will be represented by a separate interface, small and focused. And each interface will provide means of constructing the next interface in the sequence, the interface which represents the subsequent stage in the process of building the object.
With Person class this means that the process of building the new object is split into these phases:
The first step has already been covered by the static WithName() method on the PersonBuilder class. This method serves the purpose of the entry point. Other stages will be represented by separate interfaces:
interface IExpectSurnamePersonBuilder
{
IExpectPrimaryContactPersonBuilder WithSurname(string surname);
}
interface IExpectPrimaryContactPersonBuilder
{
IExpectOtherContactsPersonBuilder WithPrimaryContact(IContact contact);
}
interface IExpectOtherContactsPersonBuilder
{
IExpectOtherContactsPersonBuilder WithOtherContact(IContact contact);
IPersonBuilder WithNoMoreContacts();
}
interface IPersonBuilder
{
IPerson Build();
}
Even without much explanation, the grand plan behind this set of interfaces is quite clear. Making the call on one interface leads to having the next one in your hands. The sequence goes on until the last interface is obtained, the IPersonBuilder interface, which only exposes one method: Build(). This method will finally construct the desired Person object and the whole process will be over.
Once again, we can ask the question who will implement all these interfaces? We can stick to the idea of putting them all in the PersonBuilder class, because that will keep related data and methods that operate on them together. On the other hand, since PersonBuilder class has no public constructors, it will not be visible for the callers that they are actually working with only one builder object all the time.
We are ready to provide the complete implementation of the PersonBuilder class. Here it is:
class PersonBuilder :
IExpectSurnamePersonBuilder,
IExpectPrimaryContactPersonBuilder,
IExpectOtherContactsPersonBuilder,
IPersonBuilder
{
private string Name { get; }
private string Surname { get; set; }
private IContact PrimaryContact { get; set; }
private Person Person { get; set; }
private PersonBuilder(string name)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException(nameof(name));
this.Name = name;
}
public static IExpectSurnamePersonBuilder WithName(string name)
{
return new PersonBuilder(name);
}
public IExpectPrimaryContactPersonBuilder WithSurname(string surname)
{
if (string.IsNullOrEmpty(surname))
throw new ArgumentException(nameof(surname));
this.Surname = surname;
return this;
}
public IExpectOtherContactsPersonBuilder WithPrimaryContact(IContact contact)
{
if (contact == null)
throw new ArgumentNullException(nameof(contact));
this.Person = new Person(this.Name, this.Surname, contact);
return this;
}
public IExpectOtherContactsPersonBuilder WithOtherContact(IContact contact)
{
if (contact == null)
throw new ArgumentNullException(nameof(contact));
this.Person.AddContact(contact);
return this;
}
public IPersonBuilder WithNoMoreContacts()
{
return this;
}
public IPerson Build()
{
return this.Person;
}
}
This implementation makes a clear separation of stages in building the Person object. In order to move on to the next stage, the caller must provide all mandatory data for the current stage. In each stage, all data provided are validated. It is impossible to progress further if any of the data collected so far violates the rules of the Person class.
The whole effort pays off when we look at the client. The consuming code now becomes very easy to write, and also very easy to read and understand later:
IPerson person =
PersonBuilder
.WithName("John")
.WithSurname("Doe")
.WithPrimaryContact(new EmailAddress("john@doe.com"))
.WithOtherContact(new EmailAddress("admin@work.com"))
.WithOtherContact(new EmailAddress("bounce@home.com"))
.WithNoMoreContacts()
.Build();
It is clear that the client cannot cause InvalidOperationException anymore. It is also clear that the client cannot obtain reference to IPerson object without walking through the whole process of supplying all contained data and letting the builder validate all the data supplied. Once a step is done, there will be no subsequent surprises. This design is from the point of view of the consumer much better. It promotes certainty, which is one of the highest values in computing.
On the other hand, we could object that implementation of the PersonBuilder class is somewhat shaky. If you are accustomed to the Command-Query Separation Principle (CQS), then this implementation of the PersonBuilder class will not satisfy your standards.
In the following section we will investigate the ways to improve the PersonBuilder implementation so that it conforms to the Command-Query Separation Principle, while still keeping the good parts of this solution - segregation of interfaces that are defining the object building stages.
Command-Query Separation (CQS) in its shortest form says that any method defined on an object should either be a command, or a query, but never both. The distinction is made over the question what the method is doing. Commands are telling the object to actually do something, which implies that the object itself, or some other object it uses, will be changed by the operation. Queries are telling the object to construct a result as the answer.
CQS then says that methods that return a result should only be queries and should be side-effect-free. It should be possible for us to call the query method as many times as we wish and always to receive the same result. Even more, all other observable properties of the object should remain unchanged by the query method.
On the other hand, a method which changes the object should only be considered a command. It should not return a result, because that would confuse its callers - is the method changing the object or querying it?
It is generally known that methods that do both - change the object and return a result - are generators of confusion and defects. Keeping command methods separate from query methods has become a matter of coding culture. When done right, this method leads to design which is easier to understand: Methods that return void are commands; those returning a type are queries.
And with convention come the benefits. Methods returning void are known to cause changes to the system. Methods returning non-void results are known to leave the object and everything it references intact.
Speaking of CQS principle, we can say that the previous implementation of the PersonBuilder class has been violating it all the way through. Each of the With methods has changed the builder and then returned it for further use. Take WithSurname() method as an example:
class PersonBuilder :
IExpectSurnamePersonBuilder,
IExpectPrimaryContactPersonBuilder,
IExpectOtherContactsPersonBuilder,
IPersonBuilder
{
...
public IExpectPrimaryContactPersonBuilder WithSurname(string surname)
{
if (string.IsNullOrEmpty(surname))
throw new ArgumentException(nameof(surname));
this.Surname = surname;
return this;
}
...
}
This method is obviously changing the underlying object and then returning the reference to it for further use. This makes the caller wander whether the object has been changed and then returned for further calls, or it is some other, new object, and the original object was left intact.
It would be better if we could stick to a design method which would guarantee one or the other. In that respect, CQS is the only industry standard which comes to mind. It says that any concrete implementation of the method of the IPersonBuilder interface which returns IPersonBuilder object would have to create a new object and return a reference to it, rather than to change the current object.
Some might ask at this point if creating new object with every call to a method is a reasonable strategy from performance point of view. In that respect, we should keep a few points on mind.
First of all, modern computers are perfectly capable of instantiating millions of objects every second, which perfectly suits even the most demanding business applications. It is not the cost of creating objects which burns CPUs worldwide. Much more often it is a less than optimal algorithm design, repeated processing and similar issues. The first measure in fighting performance bottlenecks is not to optimize object creation, but rather to pay attention to operations that are executed and see if there is some better approach from the algorithmic point of view.
The next issue that may hurt performance is the way we are creating new objects. They should be the shallow copies, not deep copies. If we have to construct a new object which works on the same data as the current object, then there is no reason to make copies of the contained data. If contained data are immutable, or just not mutated by the object, then you can freely refer them in the copied object without fear that the object will ever observe a change on those data.
This already gives us a guide where to search for good designs that obey CQS principle, but let's not hurry. Further on, we could ponder over performance vs. reliability in software. CQS is imagined to clarify the design of types so that the callers can tell with more certainty what will be the effects (and, more importantly: side effects) of calls they make. When CQS principle is applied to a design, we may pay certain performance price in some cases, but the result will probably be a more reliable implementation, which will fail less often in production.
Among the most difficult defects are aliasing bugs. When two objects are referencing the same target object, then those two references are considered aliases for the same object. The problem may occur if one object makes a change - then the other object will immediately be able to observe that change. But will it expect the change to occur? If it doesn't expect the change, and doesn't guard against it, but instead continues executing with previous data on mind, then it might fail because that object is not the same anymore.
This effect is known as aliasing bug and it can be avoided quite effectively by not sharing references to mutable objects. If you plan to disseminate references all over the application, then it pays to invest into design and make sure that all referenced objects are immutable whenever possible.
Now that we have analyzed the consequences of CQS principle from the theoretical point of view, we can try to apply it to the PersonBuilder implementation.
In the previous section we have already applied the Interface Segregation Principle (ISP). Following this design method has led us to defining a few interfaces which can walk the consumer through the process of constructing a Person object:
interface IExpectSurnamePersonBuilder
{
IExpectPrimaryContactPersonBuilder WithSurname(string surname);
}
interface IExpectPrimaryContactPersonBuilder
{
IExpectOtherContactsPersonBuilder WithPrimaryContact(IContact contact);
}
interface IExpectOtherContactsPersonBuilder
{
IExpectOtherContactsPersonBuilder WithOtherContact(IContact contact);
IPersonBuilder WithNoMoreContacts();
}
interface IPersonBuilder
{
IPerson Build();
}
These are the interfaces that lead one to another until finally the IPersonBuilder interface is reached, with only the Build method there to conclude the process. The first interface, IExpectSurnamePersonBuilder, is produced by the static method of the PersonBuilder class.
We can design the whole set of classes by following the natural flow in building a Person object. As said, it all starts from the PersonBuilder class and a static factory method WithName(), which just returns an object implementing the IExpectSurnamePersonBuilder interface:
class PersonBuilder
{
public static IExpectSurnamePersonBuilder WithName(string name)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException(nameof(name));
return new ExpectSurname(name);
}
}
This method returns an ExpectSurname object. It is not a mistake - this is really a class, rather than the IExpectSurnamePersonBuilder interface. Objects of the ExpectSurname class expect a valid name in their constructor.
Note that there we are validating the name in this method. This is because the ExpectSurname is not going to validate it. As will soon become obvious, in this solution the caller would have to validate data or otherwise all validations would be repeated. Those are the small things we have to keep on mind when designing immutable classes. It wouldn’t be the mistake to validate data in constructors either. Slight performance hit would be traded for the peace of mind.
Anyway, we are ready to define this next class:
class ExpectSurname : IExpectSurnamePersonBuilder
{
private string Name { get; }
public ExpectSurname(string name)
{
this.Name = name;
}
public IExpectPrimaryContactPersonBuilder WithSurname(string surname)
{
if (string.IsNullOrEmpty(surname))
throw new ArgumentException(nameof(surname));
return new ExpectPrimaryContact(this.Name, surname);
}
}
This is the complete ExpectSurname class implementation. As you can see, it generates immutable objects, each containing only a name. When the surname arrives, we create a new object, this time of class ExpectPrimaryContact, and return it from the method. There are no changes to the ExpectSurname object after it has been created.
The next class we have to define is obviously the one implementing the IExpectPrimaryContactPersonBuilder interface:
class ExpectPrimaryContact : IExpectPrimaryContactPersonBuilder
{
private string Name { get; }
private string Surname { get; }
public ExpectPrimaryContact(string name, string surname)
{
this.Name = name;
this.Surname = surname;
}
public IExpectOtherContactsPersonBuilder WithPrimaryContact(IContact contact)
{
if (contact == null)
throw new ArgumentNullException(nameof(contact));
return new ExpectFirstContact(this.Name, this.Surname, contact);
}
}
Implementation is quite similar to previous two. Only this time we have a twist: WithPrimaryContact() method is returning an ExpectFirstContact object. This class name already suggests that no contacts have been received yet. And the importance of this fact will become obvious soon.
To give out the secret up front, there will be one additional implementation of the IExpectOtherContactsPersonBuilder interface - the one which actually contains some contacts. Both variants of the interface implementation will be equally important when it comes to filling the contacts into the newly created Person object. Basically, there will be one (polymorphic) feature which connects these two classes - the process of adding the contacts to the Person class. ExpectFirstContact variant will do nothing in that case, because it contains no contacts. ExpectMoreContacts, which is the name of the second class, will really add contacts to the Person object.
The concept which unites these two variants will be the abstract ExpectContacts class:
abstract class ExpectContacts : IExpectOtherContactsPersonBuilder
{
public abstract IExpectOtherContactsPersonBuilder WithOtherContact(IContact contact);
public abstract Tuple<string, string, IContact> GetMandatoryData();
public abstract void ForEachContact(Action<IContact> action);
public IPersonBuilder WithNoMoreContacts()
{
Tuple<string, string, IContact> basicData = this.GetMandatoryData();
string name = basicData.Item1;
string surname = basicData.Item2;
IContact primaryContact = basicData.Item3;
IList<IContact> otherContacts = new List<IContact>();
this.ForEachContact((contact) => otherContacts.Add(contact));
return new FinalPersonBuilder(name, surname, primaryContact, otherContacts);
}
}
The whole story about this abstract class is in its WithNoMoreContacts() method. This method relies on two abstract methods - one named GetMandatoryData(), which returns a tuple containing name, surname and primary contact; method named ForEachContact() receives an action and is supposed to invoke the action for each of the contacts that were previously stored through calls to the WithOtherContact() method. These two methods are abstract because base class cannot figure any of the information they are meant to produce. That is what the two derived classes are there for.
So, the first one to come is ExpectFirstContact. That is the class which still doesn't contain any optional contacts, but does contain name, surname and primary contact. Therefore, its duty will be to supply a meaningful implementation of the GetMandatoryData() method:
class ExpectFirstContact : ExpectContacts
{
private string Name { get; }
private string Surname { get; }
private IContact PrimaryContact { get; }
public ExpectFirstContact(string name, string surname, IContact primaryContact)
{
this.Name = name;
this.Surname = surname;
this.PrimaryContact = primaryContact;
}
public override IExpectOtherContactsPersonBuilder WithOtherContact(IContact contact)
{
if (contact == null)
throw new ArgumentNullException(nameof(contact));
return new ExpectMoreContacts(this, contact);
}
public override Tuple<string, string, IContact> GetMandatoryData()
{
return Tuple.Create(this.Name, this.Surname, this.PrimaryContact);
}
public override void ForEachContact(Action<IContact> action)
{
}
}
This implementation was very easy to produce. Objects of this class contain mandatory data, but no optional contacts. Therefore, GetMandatoryData() method contains concrete implementation, while ForEachContact() only provides an empty body.
The second derived class will be more engaged in iterating through optional contacts, while its ability to produce mandatory data will only be limited to delegating the call to the object’s predecessor:
class ExpectMoreContacts : ExpectContacts
{
private ExpectContacts Predecessor { get; }
private IContact Contact { get; }
public ExpectMoreContacts(ExpectContacts predecessor, IContact contact)
{
this.Predecessor = predecessor;
this.Contact = contact;
}
public override IExpectOtherContactsPersonBuilder WithOtherContact(IContact contact)
{
if (contact == null)
throw new ArgumentNullException(nameof(contact));
return new ExpectMoreContacts(this, contact);
}
public override Tuple<string, string, IContact> GetMandatoryData()
{
return this.Predecessor.GetMandatoryData();
}
public override void ForEachContact(Action<IContact> action)
{
this.Predecessor.ForEachContact(action);
action(this.Contact);
}
}
This is all it takes to collect the data required to construct the Person object. Observe that the ExpectMoreContacts object is first letting its predecessors to report their contained contacts and only after that it adds one contact more. In that way we can guarantee that contacts will be reported in order of them being added to the builder interface implementations. We don't know whether the order of contacts is important or not, so we are not taking any chances: Builders are passing them further in order in which the client has sent them in.
Finally, we are ready to write the FinalPersonBuilder class, which completes the process of instantiating the Person class and it is actually going to be very simple:
class FinalPersonBuilder : IPersonBuilder
{
private string Name { get; }
private string Surname { get; }
private IContact PrimaryContact { get; }
private IEnumerable<IContact> OtherContacts { get; }
public FinalPersonBuilder(string name, string surname, IContact primaryContact,
IEnumerable<IContact> otherContacts)
{
this.Name = name;
this.Surname = surname;
this.PrimaryContact = primaryContact;
this.OtherContacts = otherContacts;
}
public IPerson Build()
{
IPerson person = new Person(this.Name, this.Surname, this.PrimaryContact);
foreach (IContact contact in this.OtherContacts)
person.AddContact(contact);
return person;
}
}
In the last section we have complete implementation of all concrete classes that are working together on the task of building a Person object. You may feel that solution like that one is overkill for such a small problem as instantiating Persons. But think again.
Builder classes will be designed and implemented only once, while Person objects will be constructed in many places in code. The old approach, in which Builder class was entitled to throw InvalidOperationException whenever the client fails to populate all the mandatory fields, was a time bomb. Sooner or later, the client will fail. And at that moment entire application may fail as well. What would you prefer now? A shorter implementation, which is bound to fail every once in a while; or a longer implementation which takes care of the process and also comes with the promise of obeying the CQS principle? In any serious application I vote for the latter.
Just for the end, we can demonstrate how this flock of Builder classes can be used to construct a Person object:
IPerson person =
PersonBuilder
.WithName("Max")
.WithSurname("Planck")
.WithPrimaryContact(new EmailAddress("max@planck.institute"))
.WithOtherContact(new EmailAddress("max@houseof.planck"))
.WithOtherContact(new PhoneNumber("011", "12345"))
.WithNoMoreContacts()
.Build();
If you know that no method in this code segment has side-effects, and if you know that none of the calls made will ever throw an InvalidOperationException, then I believe you will agree that this approach to writing builders is more convenient for business applications than the naive one which was demonstrated first.
If you wish to learn more, please watch my latest video courses
In this course, you will learn the basic principles of object-oriented programming, and then learn how to apply those principles to construct an operational and correct code using the C# programming language and .NET.
As the course progresses, you will learn such programming concepts as objects, method resolution, polymorphism, object composition, class inheritance, object substitution, etc., but also the basic principles of object-oriented design and even project management, such as abstraction, dependency injection, open-closed principle, tell don't ask principle, the principles of agile software development and many more.
More...
In this course, you will learn how design patterns can be applied to make code better: flexible, short, readable.
You will learn how to decide when and which pattern to apply by formally analyzing the need to flex around specific axis.
More...
This course begins with examination of a realistic application, which is poorly factored and doesn't incorporate design patterns. It is nearly impossible to maintain and develop this application further, due to its poor structure and design.
As demonstration after demonstration will unfold, we will refactor this entire application, fitting many design patterns into place almost without effort. By the end of the course, you will know how code refactoring and design patterns can operate together, and help each other create great design.
More...
In four and a half hours of this course, you will learn how to control design of classes, design of complex algorithms, and how to recognize and implement data structures.
After completing this course, you will know how to develop a large and complex domain model, which you will be able to maintain and extend further. And, not to forget, the model you develop in this way will be correct and free of bugs.
More...
Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.