The Object Instantiation Nightmare: The Builder Pattern

In a previous post, we talked about how to build objects like entities and value objects, using setters and constructors, and factory methods. We saw those strategies have advantages and disadvantages.

In this post, we are going to move forward and check how builders can help us to mitigate those disadvantages.

NOTE: Do not forget why we don’t use Dependency Injection to solve this. You can see here how Dependency Injection works and here why this objects should be handled different.

TRY IT YOURSELF: You can find the source code of this post here.

The Object Instantiation Nightmare Series

Creating an Object using the Builder Pattern

The builder pattern is a creational pattern. Its intent is to separate the construction of a complex object from its representation:

Builder pattern

As we can see, we delegate to the Builder class the creation of the Product. Builder class exposes the necessary behavior to build the Product. After we call the necessary BuildPart methods, we can call GetRestult method to create the final Product object.

Now, let’s see an example of this using our Person class.

Builder pattern example

In this case, we only define one PersonBuilder. That class is concrete and we do not define any interfaces for simplicity.

NOTE: The full pattern uses interfaces to define a builder structure. You can do it if you want, but, for this use case that usually is too much.

PersonBuilder is mutable, it changes by each time you build a new part. Now, after you call build method, it creates an immutable Person.

Let’s See Some Code

Well, let’s use the builder pattern to create the Person class.

class Person {
  private final String name;
  private final List<String> addresses;
  private final LocalDate birthDate;

  private Person(String name, List<String> addresses,
                 LocalDate birthDate) {
    this.name = name;
    this.addresses = Collections
            .unmodifiableList(addresses);
    this.birthDate = birthDate;
  }

  public Integer calculateAgeFrom(
          LocalDate currentDate) {
    return Period.between(birthDate, currentDate)
            .getYears();
  }
  
  ............

  public static class PersonBuilder {
    private String name;
    private List<String> addresses;
    private LocalDate birthDate;

    public PersonBuilder(String name) {
      this.name = name;
      this.addresses = new ArrayList<String>();
    }

    public PersonBuilder birthDate(
            LocalDate birthDate) {
      this.birthDate = birthDate;
      return this;
    }

    public PersonBuilder address(String address) {
      this.addresses.add(address);
      return this;
    }

    ..............

    public Person build() {
      return new Person(name, addresses, birthDate);
    }
  }
}

Now, we created a full private constructor in line 6. This will allow us to have control on how this class should be created. Moreover, our Personal class is immutable.

After, we created the builder class. That is mutable, and it will hold the temporal values previous to the final creation of the Person class. We defined methods by Person attribute, change the state of the builder, and return itself to chain calls.

Finally, we have the build method. It creates the Person object with the state the builder currently has.

TIP: See the builder constructor on line 33. That constructor defines the minimum required attributes to create a Person object, in this case, the name.

Now, let’s create an unit test for the method calculateAgeFrom:

  @Test
  public void calculateAgeFrom_from1988_ageIs31() {
    Person.PersonBuilder personBuilder = new Person.PersonBuilder(
            "name");
    
    Person person = personBuilder
                      .birthDate(LocalDate.of(1988, 5, 5))
                      .build();

    Person personCopy = personBuilder.build();
    
    Integer age = person.calculateAgeFrom(
            LocalDate.of(2019, 5, 5));
    
    assertThat(age).isEqualTo(31);
    assertThat(person).isEqualTo(personCopy);
  }

There, we created a Person using our new builder object, adding the required attribute name, and as Person class is immutable, we added statements to validate that feature.

NOTE: See the line 10, we reused the builder object to create a perfect immutable copy of the Person. Looks cleaner and useful.

TIP: You can read more about testing immutability here.

Let’s create an external logic to validate some Person properties:

class PersonValidation {

  public boolean validate(Person person) {
    return person.getBirthDate() != null;
  }

}

Again, we created an unit test for this method:

  @Test
  public void validate_birthDayNotNull_true() {
    Person.PersonBuilder personBuilder = new Person.PersonBuilder(
            "name");

    Person person = personBuilder
                        .birthDate(LocalDate.of(1988, 5, 5))
                        .build();

    boolean valid = personValidation
            .validate(person);

    assertThat(valid).isTrue();
  }

Good, our unit test only creates the enough Person data to work. Moreover, see how the method chain works on the builder object.

TRY IT YOURSELF: You can find the source code of these unit tests here.

Project Lombok: @Builder

Project Lombok is a pretty good tool to save some code in our developments. It has a Builder feature that generates a builder class using only the annotation @Builder.

NOTE: To install Lombok, you can see here.

So, let’s change our Person class to use @Builder annotation:

@Builder
class Person {
  private final String name;
  @Singular
  private final List<String> addresses;
  private final LocalDate birthDate;

  private Person(String name, List<String> addresses,
                 LocalDate birthDate) {
    this.name = name;
    this.addresses = Collections
            .unmodifiableList(addresses);
    this.birthDate = birthDate;
  }

  public Integer calculateAgeFrom(
          LocalDate currentDate) {
    return Period.between(birthDate, currentDate)
            .getYears();
  }

  ........
}

There, we add two annotations, @Builder and @Singular.

@Builder tells lombok to create a full builder of Person class, using the attributes it finds in the Person constructor.

@Singular tells lombok to create buildPart methods for a collection. It means, you will be able to add addresses one by one.

NOTE: For more information about @Builder and @Singular, you can see here.

Now, let’s create an unit test for the method calculateAgeFrom:

  @Test
  public void calculateAgeFrom_from1988_ageIs31() {
    Person.PersonBuilder personBuilder = Person.builder();

    Person person = personBuilder
            .birthDate(
                    LocalDate
                            .of(1988, 5, 5))
            .build();

    Person personCopy = personBuilder
            .build();

    Integer age = person.calculateAgeFrom(
            LocalDate.of(2019, 5, 5));

    assertThat(age).isEqualTo(31);
    assertThat(person).isEqualTo(personCopy);
  }

As we can see, the code is pretty similar to our handwritten PersonBuilder.

NOTE: Did you pay attention to the line 3? Lombok doesn’t support required builder fields for now.

TRY IT YOURSELF: You can find the source code of that unit test here.

About Builder Pattern

Well, we can conclude some advantages:

  • Person class is immutable.
  • Person class doesn’t have setters methods.
  • You can add new builder classes based on your user cases to Person class.
  • You can add new attributes to Person class, and the clients (like the unit tests) won’t break.
  • You can name with intent your builder class and method.
  • You can change the internal implementation of a builder class. For instance, adding logic or returning a sub type of the Person.
  • You can have two builder classes with the same parameters types but different intents.
  • You can define required attributes through the builder class constructor.
  • Lombok @Builder specifics:
    • Generate the builder class for you
    • Less code to maintain.
    • Some good customization we can use

And some disadvantages:

  • If you need to add a new required parameter to an existing builder class, you will break any client that uses that builder.
  • There, it is a new class by each business object that needs to be tested and maintain.
  • Lombok @Builder specifics:
    • You are coupling in compile time Lombok and your entities.
    • Your IDE and build tool (gradle, maven) needs to be aware about Lombok.
    • You cannot have required builder attributes.
    • Some good customization but not enough for some use cases.
    • You cannot add specific behavior to the generated builder class.
    • Troubles handling inheritance.

Final Thought

Well, as discussed setters and constructors and factory methods before, and how to use them to decouple Business Object in our application.

As we can see, builder pattern is the best between them, however, it has disadvantages too.

There are some tools that helps us to create builders easily, like Lombok Project, however, it doesn’t have the option to define required attributes, and it has troubles handling inheritance.

I encourage you to review Lombok and use it carefully for your use cases. At the end, doing at hand your builders, give you the whole power.

If you liked this post and are interested in hearing more about my journey as a Software Engineer, you can follow me on Twitter and travel together.

Advertisement

2 comments

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s