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:
|
|
Código 1: |
Por exemplo, um Builder para montar veículos seria assim:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
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 . |
Olá Sergio!!! Parabéns pelo weblog!! Sensacional!
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.
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);
}
}
}
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)
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.
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.
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.