Builder

O padrão Builder

O padrão Builder (Montador) é um projeto que ajuda a criar objetos complexos; quer seja porque contém muitas propriedades e várias configurações possíveis, ou porque contém uma estrutura aninhada de objetos.

O problema

Ao codificar orientado a objetos é comum representarmos entidades do sistema como estruturas de objetos aninhados ou utilizar objetos no padrão Bean com várias propriedades. A construção de objetos deste tipo pode ser complexa e obrigar a escrever muito código burocrático devido à estrutura interna do objeto estar encapsulada.

Construir um objeto complexo é uma tarefa chata, propensa a erros e pouco controlável.

A solução

A solução é definir um outro objeto que será responsável pela criação correta do objeto que queremos construir. Para que não haja confusão com o conceito de construtor, passarei a referir à responsabilidade do objeto que implementa o padrão Builder como “montar”.

O objeto Builder é responsável por montar um outro objeto. Este outro objeto é chamado, genericamente, de produto.

O objeto Builder conta com vários métodos na sua interface de forma a poder ser informado das características que o produto terá e um método que realmente irá montar o produto e entregá-lo. Opcionalmente a montagem pode ser explicitada em um outro método de forma a deixar mais claro quando acontece a montagem.

Em Código

A estrutura clássica de uma implementação de Builder tem este aspecto:

01
02 public class Builder<Produto> {
03
04 public void setPropriedadeA ( String propriedadeA ){ }
05 public void setPropriedadeB ( Long propriedadeB ){ }
06
07 public void montaProduct (){ }
08 public Produto getProduct (){ }
09
10
11 }

Código 1:

Por exemplo, um Builder para montar veículos seria assim:

01
02 public class BuilderVehicle {
03
04 public void setColor ( Color color ){ }
05 public void setEngime ( Engine engine ){ }
06
07 public void assemble (){ }
08 public Vehicle getVehicle (){ }
09
10 }

Código 2:

Note que as propriedades e os nomes dos métodos montador e acessor estão vinculados ao objeto a ser contruído e fazem sentido para esse objeto e dentro do domínio em que ele se insere. Isto não é uma regra do padrão, mas é uma boa prática para que o programador se sinta confortável ao usar o objeto montador.

Interface Fluente

Utilizar o Builder resulta em invocar vários métodos no objeto Builder e no fim invocar o método montador. Isto rapidamente se torna prolixo em Java se não tivermos certos cuidados. Tomando o exemplo do montador de veículos ficaria mais ou menos assim:

01
02 BuilderVehicle builder = new BuilderVehicle () ;
03 builder.setColor ( Color.WHITE ) ;
04 builder.setEngine ( new TurboEngineAs234 ()) ;
05 builder.setDoorQuantity ( 5 ) ;
06 builder.setFuelType ( FuelType.FLEX ) ;
07 builder.assemble () ;
08
09 Vehicle vehicle = builder.getVehicle () ;

Código 3:

Como se vê é muita coisa para escrever. Imagine que existem mais campos ou que o nome da variável é maior. Muita coisa para escrever. O ideal seria poder encaixar as invocações com o mínimo possível de esforço , algo assim:

1
2 Vehicle vehicle = new BuilderVehicle ()
3 .setColor ( Color.WHITE )
4 .setEngine ( new TurboEngineAs234 ())
5 .setDoorQuantity ( 5 )
6 .setFuelType ( FuelType.FLEX )
7 .assemble ()
8 .getVehicle () ;

Código 4:

Nada mau, mas poderíamos melhorar mais ainda. Porque o objeto Builder tem que montar o produto ele conhece a sua estrutura e as opções dessa montagem, por isso é natural definir um Builder mais inteligente. Por outro lado se sempre invocamos assemble() antes de getVehicle() podemos fundir os dois. O resultado é ainda menos coisas para escrever:

1
2 Vehicle vehicle = new BuilderVehicle ()
3 .setColor ( Color.WHITE )
4 .setTurboEngineAs234 ()
5 .setFiveDoors ()
6 .setFuelFlex ()
7 .assembleVehicle () ;
8 }

Código 5:

Esta forma de fazer encaixar os métodos de forma a que funcionem como uma macro instrução é conhecido como Interface Fluente. Interface aqui se refere ao contrato do Builder e Fluente porque diz muito com poucas palavras (pouca escrita).

“Interface Fluente” é um nome bonito para algo antigo mas pouco utilizado devido ao vício de usar o padrão Bean para todos os objetos ignorando a real responsabilidade do objeto. A classe StringBuffer já tradicional na API Java sempre contou com uma interface fluente (o método append).

Implementar uma interface fluente é simples, basta que cada método sempre retorne o próprio objeto (this) permitindo o encadeamento das invocações de forma simples.

O uso da Interface Fluente é especialmente útil quando em conjunção com o padrão Builder, pois quase sempre estamos interessados em passar alguma informação – várias, na realidade – para ele e raramente queremos pedir alguma coisa dele. O único método que não segue esta regra é aquele que retorna o produto já montado. Contudo, nesse momento, a nossa configuração do objeto montador já terminou.

Builder, QueryObject e DSL

Uma das grandes utilidades do padrão Builder é utilizá-lo para construir estruturas que implementem o padrão Composite ou sejam, de alguma forma, composições de objetos. Um exemplo importante são as implementações de QueryObject. O objetivo de um QueryObject é ser um objeto que representa toda a informação necessária para impor critérios de pesquisas. Uma String com SQL é uma forma “primitiva” de QueryObject. Os QueryObject de hoje – conhecidos como Criteria – correspondem a objetos especiais que serão interpretados para criar o SQL ou interpretados diretamente para obter os resultados da pesquisa. Um montador de objetos que implementam QueryObject seria mais ou menos assim:

1
2 Criteria criteria = CriteriaBuilder.search ( Customer. class )
3 .and ( “active” ) .eq ( true )
4 .and ( “name” ) .match ( “%io” )
5 .orderBy ( “name” ) .asc ()
6 .build () ;

Código 6:

O Builder acaba fazendo o papel de “linguagem de programação” específica para o domínio do objeto contruído já que os seus métodos mapeiam condições comuns que queremos adicionar em um critério de pesquisa. Torna-se, portanto, claro que ao implementar um Builder o ganho é tanto maior quanto mais a interface fluente se aproximar da forma com o programador pensa quando pensa em montar o objeto produto. Os Criteria têm muita semelhança com frases SQL, pelo que é de notar que um Builder de Criteria tenha uma interface fluente especifica semelhante a uma frase SQL.

Estas interfaces fluentes especificamente úteis em certo domínio ganharam a denominação de Linguagens Específicas de Domínio (DSL em inglês). A Interface Fluente e um Builder são características comuns às DSL quando implementadas em Java. Outrs tipos possíveis que incluem o uso de linguagens realmente específicas (com seu compilador e interpretador) são também uma hipótese – e até possível de integrar com java via Java Scripting Framework mas agora um Builder com interface fluente é muito mais simples de construir e muito util mesmo assim.

Uma última nota é relativa à implementação de um código como o mostrado acima. Vimos que a interface fluente é conseguida retornando o próprio objeto no método de forma a poder encadear as ações. Contudo, repare que a frase .and(“active”).eq(true) atua como uma unidade não sendo possível escrever “.and(“active”).and(“name”)”. Para conseguir isto o método and não retorna o builder de Criteria mas sim um builder para a “frase” de filtro. Apenas quando a frase tiver todos os seus componentes ela é adicionada internamente ao builder e o builder é retornado voltado a ser possível invocar and novamente ou outro método como orderBy. (no qual usamos a mesma técnica para invocar asc())

Esta mecânica de montadores compostos é interessante porque permite um maior controle sobre a ordem e seqüencia permitida das operações que o programador pode invocar no Builder. Nem sempre ela é útil, mas quanto mais complexo for o objeto a construir, mais útil se torna.

Licença

Creative Commons License Sérgio Taborda
Este trabalho é licenciado sob a
Licença Creative Commons Atribuição-Uso Não-Comercial-Não a obras derivadas 3.0 Genérica .

7 opiniões sobre “Builder”

  1. Sérgio, parabéns pelo excelente artigo. Agradeceria se pudesse responder as seguintes dúvidas.

    Tomando como exemplo as classes Vehicle e BuilderVehicle:

    1) Qual seria a forma ideal de configurar a visibilidade da classe Vehicle dentro do pacote que a contém? Tornar a classe Vehicle pública com construtor “default” estaria correto? Dessa forma, só o Builder seria capaz de enxergá-la (o Builder seria público, claro);

    2) Onde seria o local adequado para conferir a consistência do objeto montado: no construtor de Vehicle ou no Builder?

    Desde já obrigado pela atenção. Abraços.

  2. E fazendo apenas uma observação, o Joshua Bloch sugere implementar o Builder como uma classe estática dentro da classe montada por ele (Effective Java segunda edição). Achei interessante a abordagem. A classe Vehicle então poderia ficar assim:

    public class Vehicle {
    // atributos

    // Note o construtor privado
    private Vehicle(parâmetros) {

    }

    public static class Builder {
    public Builder() {

    }

    // Métodos para setar os parâmetros

    public Vehicle build() {
    return new Vehicle(parâmetros);
    }
    }
    }

  3. Tarso,

    1) O Vehicle é uma classe comum , um bean, com contruttor publico e possivelmente até com argumentos. Pode ser que em casos particulares vc queira restringir a visibilidade mas não no caso geral. O Builder não tem por objetivo encapsular a criação do objeto ( isso é responsabilidade de uma Factory) mas apenas simplificar a criação. De certa forma a função do builder é adicionar fluência na criação manual do objeto ( por oposição a criação automática feita por uma factory ). Se vc quiser esconder o contrutor então protected (se quiser possibilitar herança fora do pacote) )ou default (se não quiser herança fora do pacote ) )é o modificador adequado.
    Vc pode incluir o builder como objeto estático mas isso me parece não fazer muito sentido já que isso acopla o veiclo ao builder. Essa solução apenas é boa se o construtor precisar ser private, o que é raro.

    2) Por definição um builder cria objetos em estado consistente. Contudo, é da responsabilidade do Veiculo garantir essa consistencia. Portanto é nele que a consistência acontece.
    Pode existir uma consistência mista em que o builder força certas regras a mais que o veiculo. Uma forma de fazer isso é com builders aninhados como no exemplo que dei do .and(”active”).eq(true). Desta forma a consistência é forçada via encadeamento correto e não ha necessidade de ifs. Contudo, cada caso é um caso.
    Vc pode adicionar consistência no builder, mas é o veiculo que tem essa responsabilidade e portanto ele sempre deve ter a consistencia, mesmo quando o builder tb a tem.
    A exceção seria se o objeto é puramente criado pelo builder ( por exemplo, o builder cria objetos de uma interface , portanto ele usa a sua propria implementação)

  4. Entendi. Então sempre devo construir o bean como se não existisse seu builder; dessa forma, eu evito o acoplamento. É interessante notar que o builder pode funcionar como uma DSL para a criação do bean.

    Obrigado pelas respostas.

  5. Sergio,

    Haveria algum problema em referir como Builder no caso abaixo onde a construção é feita dentro de um Objeto Builder em um caso semelhante:
    ObjetoBuilder {
    public Objeto createFinalObjeto(Dados dados) {
    Objeto o = new Objeto();
    o.setParte1(createParte1(dados));
    o.setParte2(createParte2(dados));
    return o;
    }
    private Parte1 createParte1(Dados dados) { return new Parte1() }
    private Parte2 createParte2(Dados dados) { return new Parte2() }
    }

    Ou ficaria melhor referi-lo com outro nome (já que a construção é direta de um ponto de vista externo)?

    E outra questão, essa passagem de dados para os métodos internos em tua opinião ficaria melhor como parâmetro ou colocando o objeto com os dados um atributos?

    Obrigado e parabéns pelo ótimo post.

    1. Não ha problema com o seu código. Internamente ao Builder vc programa como quiser. usar métodos utilitários internos (private) é uma tecnica comum e não tem mal algum. Tlv melhorar os nomes para explicar que partes são essas, por exemplo

      NotaBuilder{
      public Nota createFinalObjeto(DadosPedido dados) {
      Nota o = new Nota();
      nota.setCabecalho(createCabecalho(dados));
      nota.setItens(createItens(dados));
      return nota;
      }

      }

      Uma pequena alteração que eu faria é que normalmente num builder o método criador não tem parametros. E existe um ou vários métodos usados para setar os dados.

Deixe um comentário