Java

Builder

Builder é um design pattern criacional que foca no desacoplamento entre a complexa lógica de construção e a representação de um objeto.

Introdução

As vezes a construção de um objeto pode ser complexa, devido a validações de parâmetros, busca de informações em arquivos ou bases de dados, ou até mesmo pela quantidade de parâmetros que um construtor espera receber. Nestes casos, se você “misturar” a lógica de criação do objeto com a lógica que define seu comportamento, você poderá acabar por ter uma classe pouco coesa e difícil de reaproveitar.

O design pattern Builder vem para resolver este problema, colocando tanto a lógica criacional quanto a lógica comportamental eu seus devidos lugares, por proporcionar o encapsulamento da construção de um objeto, de um modo tão claro que você poderá até desenvolver diferentes implementações para a criação de um mesmo objeto ou de um mesmo builder.

Estrutura do Builder

Iremos criar uma classe Customer para representar um cliente, adicionar algumas validações de parâmetros e fazer com que um deles seja obrigatório para demonstrar o pattern.

Abaixo está o diagrama UML com sua representação:

Algumas considerações importantes a respeito do diagrama:

  • Client pode ser qualquer classe que necessite instanciar um objeto da classe Customer. Em nosso exemplo, essa classe irá conter diferentes formas de instanciar a classe Customer para podermos discutir a utilização do pattern;
  • Todos os métodos exceto o build da interface CustomerBuilder são declarados utilizando fluent interface (interface fluente), um tipo de implementação que provê um código mais legível, utilizando um “linguajar” que um humano utilizaria para se comunicar de modo geral. Métodos desenvolvidos com interface fluente sempre retornam a instância do objeto onde eles foram invocados. A adoção da interface fluente é opcional, veja os trechos de código para exemplos;
  • O método build retorna a instância para o qual o builder foi designado.

Implementação

Abaixo estão as classes Java que implementam o pattern. Alguns javadocs, comentários, getters, setters, construtores e sobrescritas de toString foram omitidos para fins de legibilidade. Por favor acesse o repositório no GitHub para obter a versão completa deste código.

Customer: a classe que precisamos instanciar para utilizarmos em nossas classes clientes:

package com.bgasparotto.designpatterns.builder;

import java.util.HashSet;
import java.util.Set;

public class Customer {
 private long id;
 private String name;
 private String city;
 private Set<Phone> phones;

 public Customer(long id, String name, String city, Set<Phone> phones) {
  this.id = id;
  this.name = name;
  this.city = city;
  this.phones = phones;
 }

 public Customer(String name, String city, Set<Phone> phones) {
  this(0, name, city, phones);
 }

 public Customer(String name) {
  this(0, name, null, new HashSet<Phone>());
 }

 // Getters and setters.
}

Phone: somente uma dependência de Customer, algo do tipo com o qual nos deparamos em nosso dia a dia:

package com.bgasparotto.designpatterns.builder;

public class Phone {
 private long id;
 private String number;

 public Phone(long id, String number) {
  this.id = id;
  this.number = number;
 }

 public Phone(String number) {
  this(0, number);
 }

 // Getters and setters.
}

CustomerBuilder CustomerBuilderImpl: o contrato e a implementação do pattern, respectivamente:

package com.bgasparotto.designpatterns.builder;

public interface CustomerBuilder {

 CustomerBuilder fromCity(String cityName);

 CustomerBuilder hasPhone(String phoneNumber);

 Customer build();
}
package com.bgasparotto.designpatterns.builder;

import java.util.Set;

public class CustomerBuilderImpl implements CustomerBuilder {
 private Customer customer;
 
 public CustomerBuilderImpl(String name) {
  this.customer = new Customer(name);
 }

 @Override
 public CustomerBuilder fromCity(String cityName) {
  customer.setCity(cityName);
  return this;
 }

 @Override
 public CustomerBuilder hasPhone(String phoneNumber) {
  Phone phone = new Phone(phoneNumber);
  
  Set<Phone> phones = customer.getPhones();
  phones.add(phone);
  return this;
 }

 @Override
 public Customer build() {
  return customer;
 }
}

Discussão

Vamos dizer que precisamos instanciar um objeto de Customer  e tudo que temos em mão é o valor para seu atributo name, que por sinal, é o único parâmetro obrigatório. Neste caso, independente de usarmos ou não o pattern o código fica limpo, porém, um pouco menor se não aplicarmos o pattern:

// Instancia o Customer passando o nome diretamente
customer = new Customer("Bruno");

// Instancia o Customer passando o nome para o Builder
CustomerBuilder builder = new CustomerBuilderImpl("Bruno");
customer = builder.build();

Agora vamos dizer que além do nome, precisaremos também passar o nome da cidade cityName. As duas soluções continuam limpas e simples, porém, sem o pattern, estamos acoplando nosso código aos detalhes de implementação do Customer, manualmente invocando seu método setCity(String).

// Instancia o Customer e define sua cidade.
customer = new Customer("Bruno");
customer.setCity("Sao Paulo");

// Instancia o Customer passando a cidade para a interface fluente
CustomerBuilder builder = new CustomerBuilderImpl("Bruno");
customer = builder.fromCity("Sao Paulo").build();

No próximo passo, além de passar os argumentos anteriores, precisamos passar também um número de telefone para criar nossa instância, possibilitando já observar que a complexidade do código aumenta conforme a lista de argumentos também cresce se não utilizarmos o pattern:

// Passando a cidade e o telefone sem o pattern.
customer = new Customer("Bruno");
customer.setCity("Sao Paulo");

Phone mobile = new Phone("999999999");
Set<Phone> phones = customer.getPhones();
phones.add(mobile);

// Passando a cidade e o telefone com o pattern.
CustomerBuilder builder = new CustomerBuilderImpl("Bruno");
customer = builder.fromCity("Sao Paulo").hasPhone("999999999").build();

Conclusão

Além das linhas de código adicionais quando precisamos passar mais argumentos, é importante notar que na versão do nosso código sem o pattern, foi preciso acoplar nosso código cliente aos setters da classe Customer para configurar seus atributos. Isto pode se tornar um problema no futuro caso precisemos por exemplo, remover os setters da classe Customer e tratar sua configuração somente no construtor para torná-la imutável, ou até mesmo modificar seu nível hierárquico em relação a classe Phone. Neste ponto, sem o pattern teríamos que modificar todas as classes do sistema que de algum modo criam uma instância de customer, enquanto que com o pattern, focaríamos nossa manutenção apenas nas classes do Builder.

bgasparotto

Recent Posts

Python function decorator

This guide will show you how to create a Python function decorator with a few…

2 years ago

Got permission denied while trying to connect to the Docker daemon socket

This guide will show you how to fix the error Got permission denied while trying…

2 years ago

Python virtual environment on Intellij IDEA

This guide will show you how to create a Python virtual environment on Intellij IDEA…

2 years ago

Find and kill processes on Linux and Mac by port number

This tutorial will quickly show you how to to find and kill processes on Linux,…

2 years ago

Python: Relocation R_X86_64_PC32 against symbol can not be used when making a shared object Error

This guide shows a possible solution for Python error Relocation R_X86_64_PC32 against symbol can not…

2 years ago

Kubernetes useful commands

I condensed below a cheat sheet of Kubernetes useful commands. I will keep updating this…

2 years ago