Padrão Builder: Como construir objetos complexos em Java (Effective Java’s Builder pattern)

Classes com muitos atributos opcionais em seus métodos construtores podem trazer bastante sujeira para o código, além de aumentar a probabilidade de erros quanto mais atributos forem necessários para instanciar um objeto da classe em questão.

Considere o exemplo a seguir da classe Produto, que possui 8 atributos.

import java.math.BigDecimal;

public class Produto {
	
	private Long id;
	private String nome;
	private String descricao;
	private BigDecimal valor;
	private String cor;
	private String altura;
	private String largura;
	private String comprimento;
	
	public Long getId() {
		return id;
	}
	
	public void setId(Long id) {
		this.id = id;
	}
	
	public String getNome() {
		return nome;
	}
	
	public void setNome(String nome) {
		this.nome = nome;
	}
	
	public String getDescricao() {
		return descricao;
	}
	
	public void setDescricao(String descricao) {
		this.descricao = descricao;
	}
	
	public BigDecimal getValor() {
		return valor;
	}
	
	public void setValor(BigDecimal valor) {
		this.valor = valor;
	}
	
	public String getCor() {
		return cor;
	}
	
	public void setCor(String cor) {
		this.cor = cor;
	}
	
	public String getAltura() {
		return altura;
	}
	
	public void setAltura(String altura) {
		this.altura = altura;
	}
	
	public String getLargura() {
		return largura;
	}
	
	public void setLargura(String largura) {
		this.largura = largura;
	}
	
	public String getComprimento() {
		return comprimento;
	}
	
	public void setComprimento(String comprimento) {
		this.comprimento = comprimento;
	}
}

Digamos que um produto possa ser criado com apenas dois atributos, id e nome e, além disso, qualquer outra combinação possível de atributos. Nesse caso, precisamos de um construtor para atender esse requisito O código abaixo mostra a classe Produto com seu construtor.

import java.math.BigDecimal;

public class Produto {
	
	private Long id;
	private String nome;
	private String descricao;
	private BigDecimal valor;
	private String cor;
	private String altura;
	private String largura;
	private String comprimento;
	
	public Produto(Long id, String nome, String descricao, BigDecimal valor, String cor, String altura, String largura,
			String comprimento) {
		super();
		this.id = id;
		this.nome = nome;
		this.descricao = descricao;
		this.valor = valor;
		this.cor = cor;
		this.altura = altura;
		this.largura = largura;
		this.comprimento = comprimento;
	}

	public Long getId() {
		return id;
	}
	
	public void setId(Long id) {
		this.id = id;
	}
	
	public String getNome() {
		return nome;
	}
	
	public void setNome(String nome) {
		this.nome = nome;
	}
	
	public String getDescricao() {
		return descricao;
	}
	
	public void setDescricao(String descricao) {
		this.descricao = descricao;
	}
	
	public BigDecimal getValor() {
		return valor;
	}
	
	public void setValor(BigDecimal valor) {
		this.valor = valor;
	}
	
	public String getCor() {
		return cor;
	}
	
	public void setCor(String cor) {
		this.cor = cor;
	}
	
	public String getAltura() {
		return altura;
	}
	
	public void setAltura(String altura) {
		this.altura = altura;
	}
	
	public String getLargura() {
		return largura;
	}
	
	public void setLargura(String largura) {
		this.largura = largura;
	}
	
	public String getComprimento() {
		return comprimento;
	}
	
	public void setComprimento(String comprimento) {
		this.comprimento = comprimento;
	}
}

O problema dessa abordagem é a verbosidade no código para criar produtos. Veja, por exemplo, como a criação de alguns produtos com diferentes atributos pode poluir facilmente o código. Além disso, essa abordagem é muito propícia a erros. Na hora de criar um produto você deverá redobrar a atenção, muitas vezes olhando a classe produto várias vezes, para não trocar a posição de atributos do mesmo tipo.

Produto produtoBasico = new Produto(1L, "Camiseta Básica", null, null, null, null, null, null);
Produto produtoBasicoComCor = new Produto(1L, "Camiseta Básica", null, null, "Azul", null, null, null);

É preciso passar null para os atributos que não desejamos, tornando o código menos legível, mais trabalhoso e muito propenso a erros. Uma possível solução é fazer uma sobrecarga de métodos construtores, que recebem apenas os atributos que queremos. Nessa abordagem, a classe produto ficaria assim:

import java.math.BigDecimal;

public class Produto {
	
	private Long id;
	private String nome;
	private String descricao;
	private BigDecimal valor;
	private String cor;
	private String altura;
	private String largura;
	private String comprimento;
	
	public Produto(Long id, String nome) {
		super();
		this.id = id;
		this.nome = nome;
	}

	public Produto(Long id, String nome, String cor) {
		super();
		this.id = id;
		this.nome = nome;
		this.cor = cor;
	}

	public Produto(Long id, String nome, String descricao, BigDecimal valor, String cor, String altura, String largura,
			String comprimento) {
		super();
		this.id = id;
		this.nome = nome;
		this.descricao = descricao;
		this.valor = valor;
		this.cor = cor;
		this.altura = altura;
		this.largura = largura;
		this.comprimento = comprimento;
	}

	public Long getId() {
		return id;
	}
	
	public void setId(Long id) {
		this.id = id;
	}
	
	public String getNome() {
		return nome;
	}
	
	public void setNome(String nome) {
		this.nome = nome;
	}
	
	public String getDescricao() {
		return descricao;
	}
	
	public void setDescricao(String descricao) {
		this.descricao = descricao;
	}
	
	public BigDecimal getValor() {
		return valor;
	}
	
	public void setValor(BigDecimal valor) {
		this.valor = valor;
	}
	
	public String getCor() {
		return cor;
	}
	
	public void setCor(String cor) {
		this.cor = cor;
	}
	
	public String getAltura() {
		return altura;
	}
	
	public void setAltura(String altura) {
		this.altura = altura;
	}
	
	public String getLargura() {
		return largura;
	}
	
	public void setLargura(String largura) {
		this.largura = largura;
	}
	
	public String getComprimento() {
		return comprimento;
	}
	
	public void setComprimento(String comprimento) {
		this.comprimento = comprimento;
	}
}

E a criação dos produtos ficaria assim:

Produto produtoBasico = new Produto(1L, "Camiseta Básica");
Produto produtoBasicoComCor = new Produto(1L, "Camiseta Básica", "Azul");

Essa abordagem melhora bastante a facilidade de criar os objetos, mas ainda há um problema. E se quisermos também uma forma de criar produtos apenas com o id, o nome e a descrição. Parece que é só criar um construtor que receba um long e duas strings e o problema está resolvido, mas como já existe um método construtor com essa assinatura (o construtor que cria um produto básico com cor), não é possível adicionar um novo método construtor com essa especificação.

Isso é um problema. Mas é um problema já conhecido e resolvido por aí. Por isso a importância dos padrões de projeto. Para resolver isso, vamos utilizar o padrão Effective Java’s Builder, que nos ajuda a criar de forma segura e elegante classes com muitos atributos opcionais.

O padrão Effective Java’s Builder tem como base o conceito de interface fluente, onde os métodos podem ser encadeados e chamados como se estivesse escrevendo um texto. No caso do builder, a ideia é construir um objeto básico com os atributos obrigatórios e depois ir chamando outros métodos para os atributos opcionais de forma encadeada. Veja como fica a classe Produto com o padrão builder implementado.

import java.math.BigDecimal;

public class Produto {
	
	private Long id;
	private String nome;
	private String descricao;
	private BigDecimal valor;
	private String cor;
	private String altura;
	private String largura;
	private String comprimento;

	private Produto(Builder builder) {
		super();
		this.id = builder.id;
		this.nome = builder.nome;
		this.descricao = builder.descricao;
		this.valor = builder.valor;
		this.cor = builder.cor;
		this.altura = builder.altura;
		this.largura = builder.largura;
		this.comprimento = builder.comprimento;
	}

	public Long getId() {
		return id;
	}
	
	public void setId(Long id) {
		this.id = id;
	}
	
	public String getNome() {
		return nome;
	}
	
	public void setNome(String nome) {
		this.nome = nome;
	}
	
	public String getDescricao() {
		return descricao;
	}
	
	public void setDescricao(String descricao) {
		this.descricao = descricao;
	}
	
	public BigDecimal getValor() {
		return valor;
	}
	
	public void setValor(BigDecimal valor) {
		this.valor = valor;
	}
	
	public String getCor() {
		return cor;
	}
	
	public void setCor(String cor) {
		this.cor = cor;
	}
	
	public String getAltura() {
		return altura;
	}
	
	public void setAltura(String altura) {
		this.altura = altura;
	}
	
	public String getLargura() {
		return largura;
	}
	
	public void setLargura(String largura) {
		this.largura = largura;
	}
	
	public String getComprimento() {
		return comprimento;
	}
	
	public void setComprimento(String comprimento) {
		this.comprimento = comprimento;
	}
	
	public static class Builder {
		
		private Long id;
		private String nome;
		private String descricao;
		private BigDecimal valor;
		private String cor;
		private String altura;
		private String largura;
		private String comprimento;
				
		public Builder (Long id, String nome) {
			this.id = id;
			this.nome = nome;
		}
		
		public Builder descricao(String descricao) {
			this.descricao = descricao;
			return this;
		}
		
		public Builder valor(BigDecimal valor) {
			this.valor = valor;
			return this;
		}
		
		public Builder cor(String cor) {
			this.cor = cor;
			return this;
		}
		
		public Builder altura(String altura) {
			this.altura = altura;
			return this;
		}
		
		public Builder largura(String largura) {
			this.largura = largura;
			return this;
		}
		
		public Builder comprimento(String comprimento) {
			this.comprimento = comprimento;
			return this;
		}
		
		public Produto build() {
			return new Produto(this);
		}
	}
}

A principal mudança foi a criação de uma classe interna chamada Builder para construir instâncias da classe Produto. O construtor da classe produto foi deixado com privado, dessa forma só é possível construir uma instância de produto por meio da classe interna Builder.

A classe Builder possui todos os atributos de produto e um construtor para um produto básico que recebe o id e o nome. Além disso, cada método para os demais atributos (descricao, valor, cor, etc) retorna o próprio builder, tornando possível as chamadas encadeadas. 

Para construir uma instância da classe Produto é necessário chamar o método build(), que chama o construtor privado da classe Produto passando o builder com os atributos de produto. O construtor da classe Produto apenas obtém os dados do builder.

Veja agora como fica a criação de um produto básico e de um produto com outros atributos.

Produto produtoBasico = new Produto.Builder(1L, "Camiseta Básica").build();
		
Produto produtoBasicoComCor = new Produto.Builder(1L, "Camiseta Básica")
				.cor("Azul")
				.build();

Produto produtoBasicoComCorDescricao = new Produto.Builder(1L, "Camiseta Básica")
				.cor("Azul")
				.descricao("Camiseta de Algodão")
				.build();

A construção do objeto ficou muito mais simples, com uma escrita natural, além de tornar o código menos poluído e menos propenso a erros na construção dos objetos.

Além disso, o código fica mais flexível. É possível adicionar novas funcionalidades no builder, por exemplo um método validate() que, antes de construir o objeto, valida os dados e pode lançar uma exceção do tipo IllegalStateExeception caso haja alguma inconsistência nos dados.

 

 

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *