Padrão de projeto Builder na pŕatica com Java & Typescript
- #TypeScript
- #Java
O padrão Builder é, sem dúvida, uma das melhores soluções para construir objetos complexos, ou seja, com muitos atributos. O Builder é um dos Padrões de Projeto (também chamado de Design Patterns) da programação, que nada mais são do que soluções para problemas um pouco mais genéricos. Além disso, o Builder também faz parte da família de "padrões de projetos criacionais", que, como o nome sugere, fornecem meios para a criação de um objeto, sendo isso definido pela famosa "Gang Of Four".
A "Gang Of Four", mais conhecida como "GoF" (Gangue dos Quatros) se refere a um grupo de quatro grande autores na área de engenharia de software que escreveram o livro "Design Patterns: Elements of Reusable Object-Oriented Software". Publicado em 1994, o livro introduziu o famoso conceito de padrão de projeto.
O problema:
Imagine uma classe com bastante atributos, como por exemplo, Pessoa com DEZ atributos:
Para a instanciar uma classe dessas, o código ficaria menos legível e mais "feio".
Typescript:
class Pessoa {
nome?: string;
idade?: number;
cidade?: string;
bairro?: string;
telefone?: string;
email?: string;
peso?: number;
altura?: number;
casado?: boolean;
profissao?: string;
constructor(nome?: string, idade?: number, cidade?: string, bairro?: string, telefone?: string,email?: string, peso?: number, altura?: number, casado?: boolean,
profissao?: string) {
this.nome = nome;
this.idade = idade;
this.cidade = cidade;
this.bairro = bairro;
this.telefone = telefone;
this.email = email;
this.peso = peso;
this.altura = altura;
this.casado = casado;
this.profissao = profissao;
}
}
const pessoa: Pessoa = new Pessoa("Pedro", 21, "Icó", "Centro", "4002-8922",
"pedro@gmail.com", 85.5, 1.87, true, "Pedreiro");
Java:
public class Pessoa {
private String nome;
private Integer idade;
private String cidade;
private String bairro;
private String telefone;
private String email;
private BigDecimal peso;
private BigDecimal altura;
private boolean casado;
private String profissao;
// ------------------------------------------------------------------------------
public Pessoa(String nome, Integer idade, String cidade, String bairro,
String telefone, String email, BigDecimal peso, BigDecimal altura,
boolean casado, String profissao) {
this.nome = nome;
this.idade = idade;
this.cidade = cidade;
this.bairro = bairro;
this.telefone = telefone;
this.email = email;
this.peso = peso;
this.altura = altura;
this.casado = casado;
this.profissao = profissao;
}
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
public Integer getIdade() {
return idade;
}
public void setIdade(Integer idade) {
this.idade = idade;
}
public String getCidade() {
return cidade;
}
public void setCidade(String cidade) {
this.cidade = cidade;
}
public String getBairro() {
return bairro;
}
public void setBairro(String bairro) {
this.bairro = bairro;
}
public String getTelefone() {
return telefone;
}
public void setTelefone(String telefone) {
this.telefone = telefone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public BigDecimal getPeso() {
return peso;
}
public void setPeso(BigDecimal peso) {
this.peso = peso;
}
public BigDecimal getAltura() {
return altura;
}
public void setAltura(BigDecimal altura) {
this.altura = altura;
}
public boolean isCasado() {
return casado;
}
public void setCasado(boolean casado) {
this.casado = casado;
}
public String getProfissao() {
return profissao;
}
public void setProfissao(String profissao) {
this.profissao = profissao;
}
}
public class Main {
public static void main(String[] args) {
Pessoa pessoa = new Pessoa("Pedro", 21, "Icó", "Centro",
"4002-8922", "pedro@gmail.com", BigDecimal.valueOf(85.5),
BigDecimal.valueOf(1.87), true,"Pedreiro");
}
}
Além do código ficar menos legível, fica também muito complicado lembrar quais os valores que estão sendo passados, sem a necessidade de ficar toda hora olhando o construtor ou apertar algum comando de "auto-complete" para mostrar os parâmetros que o construtor recebe (como o CTRL Espaço no IntelliJ, por exemplo). E no caso do Java, em necessidade de alguns atributos não serem obrigatórios, seria preciso mais construtores, poluindo ainda mais o código.
Solução: Padrão Builder
A idéia deste padrão de projeto é colocar a lógica da instanciação do objeto dentro de um outro objeto e fazê-la passo-a-passo.
O padrão Builder foi descrito no livro "Design Patterns: Elements of Reusable Object-Oriented Software" com quatro elementos. No entanto, para facilitar a compreensão, optei por abordar apenas três deles neste artigo (caso queira saber como é feito na forma original, clique aqui):
Builder: Interface com métodos para configurar os atributos. Sua utilização é semelhante a um método setter, com a diferença de que, em vez de não ter retorno (void), retorna uma instância da classe que implementará a interface. Além disso, apresenta o método "build" para criar o objeto complexo (neste caso, o objeto "Pessoa").
Concrete Builder: Basicamente, é a classe que implementa a interface Builder. Deve conter também todos os atributos do objeto complexo que é responsável por criar.
Objeto Complexo: Refere-se ao objeto com múltiplos atributos, como o objeto "Pessoa". Também deve possuir um método estático que retorna uma instância da classe que implementa a interface Builder.
Implementação do padrão Builder com Java e Typescript:
Interface de builder:
Typescript:
interface PessoaBuilder {
addNome(nome: string): PessoaBuilder;
addIdade(idade: number): PessoaBuilder;
addCidade(cidade: string): PessoaBuilder;
addBairro(bairro: string): PessoaBuilder;
addTelefone(telefone: string): PessoaBuilder;
addEmail(email: string): PessoaBuilder;
addPeso(peso: number): PessoaBuilder;
addAltura(altura: number): PessoaBuilder;
addCasado(casado: boolean): PessoaBuilder;
addProfissao(profissao: string): PessoaBuilder;
build(): Pessoa;
}
Java:
public interface PessoaBuilder {
PessoaBuilder nome(String nome);
PessoaBuilder idade(Integer idade);
PessoaBuilder cidade(String cidade);
PessoaBuilder bairro(String bairro);
PessoaBuilder telefone(String telefone);
PessoaBuilder email(String email);
PessoaBuilder peso(BigDecimal peso);
PessoaBuilder altura(BigDecimal altura);
PessoaBuilder casado(boolean casado);
PessoaBuilder profissao(String profissao);
Pessoa build();
}
Concret builder (a classe que implementa a interface de builder):
Typescript:
class PessoaBuilderImpl implements PessoaBuilder {
private nome: string;
private idade: number;
private cidade: string;
private bairro: string;
private telefone: string;
private email: string;
private peso: number;
private altura: number;
private casado: boolean;
private profissao: string;
addNome(nome: string): PessoaBuilder {
this.nome = nome;
return this;
}
addIdade(idade: number): PessoaBuilder {
this.idade = idade;
return this;
}
addCidade(cidade: string): PessoaBuilder {
this.cidade = cidade;
return this;
}
addBairro(bairro: string): PessoaBuilder {
this.bairro = bairro;
return this;
}
addTelefone(telefone: string): PessoaBuilder {
this.telefone = telefone;
return this;
}
addEmail(email: string): PessoaBuilder {
this.email = email;
return this;
}
addPeso(peso: number): PessoaBuilder {
this.peso = peso;
return this;
}
addAltura(altura: number): PessoaBuilder {
this.altura = altura;
return this;
}
addCasado(casado: boolean): PessoaBuilder {
this.casado = casado;
return this;
}
addProfissao(profissao: string): PessoaBuilder {
this.profissao = profissao;
return this;
}
build(): Pessoa {
return new Pessoa(this.nome, this.idade, this.cidade, this.bairro, this.telefone,
this.email, this.peso, this.altura, this.casado, this.profissao);
}
}
Java:
public class PessoaBuilderImpl implements PessoaBuilder {
private String nome;
private Integer idade;
private String cidade;
private String bairro;
private String telefone;
private String email;
private BigDecimal peso;
private BigDecimal altura;
private boolean casado;
private String profissao;
@Override
public PessoaBuilder nome(String nome) {
this.nome = nome;
return this;
}
@Override
public PessoaBuilder idade(Integer idade) {
this.idade = idade;
return this;
}
@Override
public PessoaBuilder cidade(String cidade) {
this.cidade = cidade;
return this;
}
@Override
public PessoaBuilder bairro(String bairro) {
this.bairro = bairro;
return this;
}
@Override
public PessoaBuilder telefone(String telefone) {
this.telefone = telefone;
return this;
}
@Override
public PessoaBuilder email(String email) {
this.email = email;
return this;
}
@Override
public PessoaBuilder peso(BigDecimal peso) {
this.peso = peso;
return this;
}
@Override
public PessoaBuilder altura(BigDecimal altura) {
this.altura = altura;
return this;
}
@Override
public PessoaBuilder casado(boolean casado) {
this.casado = casado;
return this;
}
@Override
public PessoaBuilder profissao(String profissao) {
this.profissao = profissao;
return this;
}
@Override
public Pessoa build() {
var pessoa = new Pessoa();
pessoa.setNome(this.nome);
pessoa.setIdade(this.idade);
pessoa.setCidade(this.cidade);
pessoa.setBairro(this.bairro);
pessoa.setTelefone(this.telefone);
pessoa.setAltura(this.altura);
pessoa.setPeso(this.peso);
pessoa.setEmail(this.email);
pessoa.setCasado(this.casado);
pessoa.setProfissao(this.profissao);
return pessoa;
}
}
Classe do objeto complexo:
Typescript:
class Pessoa {
nome?: string;
idade?: number;
cidade?: string;
bairro?: string;
telefone?: string;
email?: string;
peso?: number;
altura?: number;
casado?: boolean;
profissao?: string;
constructor(nome?: string, idade?: number, cidade?: string, bairro?: string,
telefone?: string, email?: string, peso?: number, altura?: number,
casado?: boolean, profissao?: string) {
this.nome = nome;
this.idade = idade;
this.cidade = cidade;
this.bairro = bairro;
this.telefone = telefone;
this.email = email;
this.peso = peso;
this.altura = altura;
this.casado = casado;
this.profissao = profissao;
}
//método estático para retornar uma instância do builder
public static PessoaBuilder() {
return new PessoaBuilderImpl();
}
}
Java:
public class Pessoa {
private String nome;
private Integer idade;
private String cidade;
private String bairro;
private String telefone;
private String email;
private BigDecimal peso;
private BigDecimal altura;
private boolean casado;
private String profissao;
// ------------------------------------------------------------------------------------------------
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
public Integer getIdade() {
return idade;
}
public void setIdade(Integer idade) {
this.idade = idade;
}
public String getCidade() {
return cidade;
}
public void setCidade(String cidade) {
this.cidade = cidade;
}
public String getBairro() {
return bairro;
}
public void setBairro(String bairro) {
this.bairro = bairro;
}
public String getTelefone() {
return telefone;
}
public void setTelefone(String telefone) {
this.telefone = telefone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public BigDecimal getPeso() {
return peso;
}
public void setPeso(BigDecimal peso) {
this.peso = peso;
}
public BigDecimal getAltura() {
return altura;
}
public void setAltura(BigDecimal altura) {
this.altura = altura;
}
public boolean isCasado() {
return casado;
}
public void setCasado(boolean casado) {
this.casado = casado;
}
public String getProfissao() {
return profissao;
}
public void setProfissao(String profissao) {
this.profissao = profissao;
}
// ------------------------------------------------------------------------------------------------
//método estático para retornar uma instância do builder
public static PessoaBuilder builder(): PessoaBuilder {
return new PessoaBuilderImpl();
}
@Override
public String toString() {
String casado = this.casado ? "Sim" : "Não";
return "Nome: " + this.nome + "\n" +
"Idade: " + this.idade + "\n" +
"Cidade: " + this.cidade + "\n" +
"Bairro: " + this.bairro + "\n" +
"Telefone: " + this.telefone + "\n" +
"Email: " + this.email + "\n" +
"Peso: " + this.peso + "\n" +
"Altura" + this.altura + "\n" +
"Casado: " + casado + "\n" +
"Profissão: " + profissao + "\n";
}
}
Instanciação do objeto complexo:
Typescript:
const pessoa: Pessoa = Pessoa
.builder()
.addNome("Pedro Lucas")
.addIdade(21)
.addCidade("Icó")
.addBairro("Centro")
.addTelefone("4002-8922")
.addEmail("pedrolucas@gmail.com")
.addPeso(83.5)
.addAltura(1.83)
.addCasado(true)
.addProfissao("Pedreiro")
.build();
Resultado no console da aplicação:
Pessoa {
nome: 'Pedro Lucas',
idade: 21,
cidade: 'Icó',
bairro: 'Centro',
telefone: '4002-8922',
email: 'pedrolucas@gmail.com',
peso: 83.5,
altura: 1.83,
casado: true,
profissao: 'Pedreiro'
}
Java:
public class Main {
public static void main(String[] args) {
//Instanciando o objeto complexo com builder
Pessoa pessoa = Pessoa
.builder()
.nome("Pedro Lucas")
.idade(21)
.cidade("Icó")
.bairro("Centro")
.telefone("4002-8922")
.email("pedrolucas@gmail.com")
.peso(BigDecimal.valueOf(83.5))
.altura(BigDecimal.valueOf(1.83))
.casado(true)
.profissao("Pedreiro")
.build();
System.out.println(pessoa);
}
}
Resultado no console da aplicação:
Nome: Pedro Lucas
Idade: 21
Cidade: Icó
Bairro: Centro
Telefone: 4002-8922
Email: pedrolucas@gmail.com
Peso: 83.5
Altura1.83
Casado: Sim
Profissão: Pedreiro
Como dito anteriormente, a instanciação é feita passo-a-passo. Perceba também que é possível também fazer encadeamento, isso graças aos métodos nas classes que implementam a interface de builder retornarem suas próprias instâncias (o "return this").
Bônus: Utilizando o Builder no Java com Lombok de maneira super fácil
Além disso, no Java é possível fazer uso deste padrão de uma forma ainda mais fácil, com o Lombok. O Lombok é uma biblioteca com anotations (anotações) para reduzir o uso de códigos repetitivos (como por exemplo, getters, setters, construtores, toString, hashCode, equals, entre outros). Vejamos o código abaixo:
@Data
@Builder
public class Pessoa {
private String nome;
private Integer idade;
private String cidade;
private String bairro;
private String telefone;
private String email;
private BigDecimal peso;
private BigDecimal altura;
private boolean casado;
private String profissao;
}
Primeiramente, usamos a anotação @Data, que gera getters, setters, hashCode, equals e entre outras coisas de forma automática. Logo em seguida, usamos o @Builder para que ele use este padrão de forma automática. E pronto! Já está feito! Incrível, não é?
Vejamos abaixo o código de instanciação desta classe (Pessoa):
public class Main {
public static void main(String[] args) {
Pessoa pessoa = Pessoa
.builder()
.nome("Pedro Lucas")
.idade(21)
.cidade("Icó")
.bairro("Centro")
.telefone("4002-8922")
.email("pedrolucas@gmail.com")
.peso(BigDecimal.valueOf(83.5))
.altura(BigDecimal.valueOf(1.83))
.casado(true)
.profissao("Pedreiro")
.build();
System.out.println(pessoa);
}
}
Resultado no console:
Pessoa(nome=Pedro Lucas, idade=21, cidade=Icó, bairro=Centro, telefone=4002-8922, email=pedrolucas@gmail.com, peso=83.5, altura=1.83, casado=true, profissao=Pedreiro)