DEV Community

Victor Gallet
Victor Gallet

Posted on

Builder Pattern, a first step to DSL

When you are looking for an explanation of Builder pattern, you will probably find some articles all showing a class dedicated to creating an object. It's quite simple and we can go deeper with this pattern.

Let's start from the beginning with the class Person below:

public class Person {

   private String firstname;

   private String lastname;

   private String nickname;

   private String email;

   private String gender;

   private String country;

   private String city;

   public Person(String firstname, String lastname, String nickname, String email, String gender, String country, String city) {
       this.firstname = firstname;
       this.lastname = lastname;
       this.nickname = nickname;
       this.email = email;
       this.gender = gender;
       this.country = country;
       this.city = city;
   }
}

As we can see from this piece of code, there will be problems when creating a new Person object:

  • constructor that's too big,
  • constructor parameters have all the same type. We have to remember the right order of each parameter when using it. In fact, we are façing a common code smell: primitive obsession. You can take a look at this article which explains how to recognize and deal with them.
  • How can we set up constraints? It can be difficult to check nullity or to ensure consistency between fields.

So to deal with some of these points, we can add several constructors. For example:

public Person(String firstname, String lastname) {
   this(firstname, lastname, null, null, null, null, null);
}

public Person(String firstname, String lastname, String nickname) {
   this(firstname, lastname, nickname, null, null, null, null);
}

public Person(String firstname, String lastname, String nickname, String email) {
   this(firstname, lastname, nickname, email, null, null, null);
}

By adding different constructors to our class Person, the result is that building responsibility is delegated to the client. The developer who wants to create an object Person has to know which constructor to use and why. Moreover, the resulting code is difficult to read and to understand :

Person john = new Person("john", "smith", "john");

In this simple piece of code, it's difficult to know where the first name, last name, and nickname parameters are. We have to go through the source code to check on which fields will be initialized. An handy solution is to create a builder class to address this readability problem.

public class PersonBuilder {

   private final Person person;

   private PersonBuilder() {
       person = new Person();
   }

   public static PersonBuilder builder() {
       return new PersonBuilder();
   }

   public PersonBuilder withFirstname(String firstname) {
       if (firstname == null) {
           throw new IllegalArgumentException("firstname must be not null");
       }
       person.setFirstname(firstname);
       return this;
   }

   public PersonBuilder withLastname(String lastname) {
       if (lastname == null) {
           throw new IllegalArgumentException("lastname must be not null");
       }
       person.setLastname(lastname);
       return this;
   }

  ...

   public Person build() {
       return person;
   }
}

Then, we can use it like this:

Person john = PersonBuilder.builder()
   .withFirstname("john")
   .withLastname("smith")
   .build();

Great! This solution has the benefit of explicit arguments. We can easily understand what person's first name is. In addition, it was easy to add a not-null constraint for each building method. Furthermore, as each method returns the instance of PersonBuilder, it provides us a pseudo DomainSpecificLanguage.

However, unlike a constructor, this builder is not self-explanatory and can be used incorrectly. For example:

Person john = PersonBuilder.builder()
    .build();

This simplest case reveals a lack of guidance. In fact, it's possible to guide the developer during the creation phase.
For example, let's say we want to divide the creation process into four steps:

  • the developer has to set the first name first,
  • then he can set the last name,
  • then he can set the email
  • and finally he can build a Person object.

To do this, we have to create four interfaces :

public interface StepFirstnameBuilder {

   StepLastnamePersonBuilder withFirstname(String firstname);
}

This first step enables the developer to set the first name field and then to use the second step interface StepLastnamePersonBuilder.

 public interface StepLastnamePersonBuilder {

   StepEmailPersonBuilder withLastname(String lastname);
}

public interface StepEmailPersonBuilder {

   FinalStepPersonBuilder withEmail(String email);
}


public interface FinalStepPersonBuilder {

   Person build();
}

Finally, after setting all mandatory fields we can access the build method.
That's it! Now we can modify PersonBuilder class to implement our four steps.

public class PersonBuilder implements StepFirstnameBuilder, 
StepLastnamePersonBuilder,
StepEmailPersonBuilder,
FinalStepPersonBuilder {


   private final Person person;

   private PersonBuilder() {
       person = new Person();
   }

   public static StepFirstnameBuilder builder() {
       return new PersonBuilder();
   }

   @Override
   public StepLastnamePersonBuilder withFirstname(String firstname) {
       if (firstname == null) {
           throw new IllegalArgumentException("firstname must be not null");
       }
       person.setFirstname(firstname);
       return this;
   }

   @Override
   public StepEmailPersonBuilder withLastname(String lastname) {
       if (lastname == null) {
           throw new IllegalArgumentException("lastname must be not null");
       }
       person.setLastname(lastname);
       return this;
   }

   @Override
   public FinalStepPersonBuilder withEmail(String email) {
       person.setEmail(email);
       return this;
   }

   @Override
   public Person build() {
       return person;
   }
}

Now, let's use it:

Person john = PersonBuilder.builder() // -> returrns an instance of StepFirstnameBuilder
   .withFirstname("john") // -> returns an instance of StepLastnamePersonBuilder
   .withLastname("smith") // -> returns an instance of StepEmailPersonBuilder
   .withEmail("john@smith.com") // -> returns an instance of FinalStepPersonBuilder
   .build();

By implementing all these steps, we have totally controlled the way a Person object is built. Actually, we have just created a DomainSpecificLanguage. This Builder pattern is a particular case of FluentInterface dedicated to building object and it's an easy way to express the way an object is built.

A big thanks to Sonyth and Mickael for their time and proofreading.

Top comments (0)