Cidenet+Blog+Clean Dev Practices+Builder

Builder

What is it? What problem does it solve?

Suppose you have to initialize a complex object, made up of many fields and nested objects. Usually, such initialization is through constructors with a large number of parameters or even worse, multiple initializations in different places in the client code.

The Builder pattern separates the construction of a complex object from its representation, so those different representations can be created under the same construction process. The construction of these objects are done step by step.

Diagram

  • Builder: specifies an abstract interface to create an object.
  • Concrete Builder: Build and assemble parts of a product by implementing the Builder interface. Provides an interface to get the constructed object.
  • Director: build an object using the Builder interface.
  • Product: represents the complex object build.

How to apply Builder:

 

We will apply the Builder pattern in a class used to filter database records dynamically. In the Spring Framework, there is an interface called Specification, which allows making dynamic queries at runtime. For this purpose, we will create a class with all the fields through which the specification can filter and we will seek to create different combinations of these fields, to be able to filter for some fields and not for others. In this example, it is assumed that there is a view through which the user can search for x object and that the view has optional search filters.

We assume that an HTTP request is sent with the filters as the request parameters. These parameters are received in the back-end service and the query is made according to them.

First, an interface is created that indicates which parts of the product each ConcreteBuilder should build.

With the build method, you get the built product.

public interface BookBuilder {
   BookBuilder setIsbn(String isbn);
   BookBuilder setName(String name);
   BookBuilder setAuthor(String author);
   BookBuilder setReleaseDate(Date date);
   Book build();
}

The Builder must build an object that has the following structure.

To create a ConcreteBuilder you must create a class that implements the interface (The Builder).

public class FilterBookBuilder implements BookBuilder {

   private String isbn;
   private String name;
   private String author;
   private Date releaseDate;

   @Override
   public BookBuilder setIsbn(String isbn) {
       this.isbn = isbn;
       return this;
   }

   @Override
   public BookBuilder setName(String name) {
       this.name = name;
       return this;
   }

   @Override
   public BookBuilder setAuthor(String author) {
       this.author = author;
       return this;
   }

   @Override
   public BookBuilder setReleaseDate(Date date) {
       this.releaseDate = date;
       return this;
   }

Note how each implemented method returns to the same inflated instance with the new information.  This in order to build the object in a chained way, step by step and regardless of the order.

The build method simply returns an instance of the built object.

@Override
public Book build() {
   return new Book(this);
}

The constructor in the Book class must be private or packaged to avoid creating instances outside of the Builders.

Book(FilterBookBuilder filterBookBuilder) {
   this.isbn = filterBookBuilder.getIsbn();
   this.name = filterBookBuilder.getName();
   this.author = filterBookBuilder.getAuthor();
   this.releaseDate = filterBookBuilder.getReleaseDate();
}

Constructor of the Book class

Note how in this constructor the information from the Builder is simply copied to the object that’s being created. It should be noted that validations can be performed in this constructor, just as they do in the Message class that is used to create Firebase Messaging push notifications.

private Message(Message.Builder builder) {
this.data = builder.data.isEmpty() ? null : ImmutableMap.copyOf(builder.data);
   this.notification = builder.notification;
   this.androidConfig = builder.androidConfig;
   this.webpushConfig = builder.webpushConfig;
   this.apnsConfig = builder.apnsConfig;
int count = Booleans.countTrue(new boolean[]{!Strings.isNullOrEmpty(builder.token), !Strings.isNullOrEmpty(builder.topic), !Strings.isNullOrEmpty(builder.condition)});
   Preconditions.checkArgument(count == 1, "Exactly one of token, topic or condition must be specified");
   this.token = builder.token;
   this.topic = stripPrefix(builder.topic);
   this.condition = builder.condition;
   this.fcmOptions = builder.fcmOptions;
}

Private constructor of the Message class that has validations.

With this model, you can create dynamic objects. For example, a filter that searches for books by name and author only.

@Test
  public void shouldBuildABookWithNameAndAuthor() {
      Book book = new FilterBookBuilder()
              .setName("Book 1")
              .setAuthor("Author 1")
              .build();
assertEquals("Book{isbn='null', name='Book 1', author='Author 1', releaseDate=null}", book.toString());
  }

Or a search only by registration date and ISBN.

@Test
public void shouldBuildABookWithIsbnAndReleaseDate() {
   Calendar calendar = Calendar.getInstance();
   calendar.set(2020, Calendar.FEBRUARY, 1, 1, 1, 1);
   Book book = new FilterBookBuilder()
           .setIsbn("aert6789")
           .setReleaseDate(calendar.getTime())
           .build();
   assertEquals(
           "Book{isbn='aert6789', name='null', author='null', releaseDate=Sat Feb 01 01:01:01 COT 2020}",
           book.toString()
   );
}

This allows having only one constructor creating different combinations of books according to the variable parameters. If this strategy is not applied, a constructor must be created for each possible combination as shown in figure 10.

public Book(String name, String author) {
   this.name = name;
   this.author = author;
}

public Book(String isbn, Date releaseDate) {
   this.isbn = isbn;
   this.releaseDate = releaseDate;
}

Book class with different constructor implementations

In this case, it is not possible to create a constructor for each possible combination, since there are attributes that have the same data type, for example, name – author, name – ISBN.

Another advantage of applying the Builder pattern is that if a new field must be added to the filters, it is no longer filtered, for example, by name – author, but must also be searched by ISBN. In the case that you do not apply Builder, the already created constructor must be modified or a new one must be created, while applying the Builder pattern it is only necessary to create the field in the object, and fill it through the Builder, thus leaving the logic already created intact.

Conclusions:

  • With this pattern, the internal representation of the product can be varied.
  • It hides the internal structure of the product, as well as the construction of the product.
  • As the product is built through an interface, what must be done to change the internal representation of the product is to define a new type of Builder.
  • You get complete control over the construction process, as the product is created step by step under the control of the Concrete Builder.
  • With this pattern, modularity is gained since the way a complex object is created is encapsulated. The customer does not need to know anything about the classes that define the internal structure of the product.

Sample code can be found at https://bit.ly/2R57Ysv

Sources:

https://sourcemaking.com/design_patterns/Builder
https://refactoring.guru/design-patterns/Builder
https://medium.com/xebia-engineering/fluent-Builder-pattern-with-a-real-world-example-7b61be375a40
Design Patterns : Elements of Reusable Object-Oriented Software

Contact us

Allow us to contact you

    Medellín - Colombia

  • Calle 47D #72-29
  • (+57) 4 3222567
  • comunicaciones@cidenet.com.co

    United States

  • 1200 Colorado Blvd, Denver Colorado 80220
  • (+1) 7723619239
  • jceballos@cidenet.net
WhatsApp
>