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.