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.