Builder, pra quê te quero?
Que os Padrões de Projeto (GoF) são uma mão na roda, todo mundo sabe. Mas que nunca olhou para este ou aquele padrão e perguntou “Quando é que eu vou precisar desta bomba”? Minha proposta é te ajudar a responder esta pergunta (ou não).
Hoje vamos começar pela padrão Builder.
Na DB1 Global Software, empresa em que trabalho, estamos participando de um jogo chamado “mestre dos códigos”. Entre as tarefas deste jogo está o uso de alguns padrões de projeto e também de teste unitários. Então em pensei: por que não usar os dois? Faço essa pequena introdução pensando nas pessoas poderiam perguntar: mas por que você não usa framework para mockar os objetos? Oras, porque eu queria usar builders!
Enquanto no DB2, o SQL seria semelhante a
Por isso, optei por criar uma RTL para o componente. E o componente em si funcionaria como façade para essa RTL.
A essa altura, a adoção do builder parecia a melhor solução. E era.
Usando Generics, essa estrutura representa o básico do padrão builder. Como você pode perceber, qualquer classe pode ser utilizada aqui.
Especificando um pouco mais, temos as implementações básicas no nível da RTL.
Mas ainda será necessário mais um nível de abstração. Como você pode ver, cada classe possui um builder particular:
Vejamos um exemplo:
Aqui nós temos a interface para criação de colunas. Lembre-se que a coluna tem o nome, tem um nome virtual (select colunas as Nome_virtual) e ela também pode ter uma tabela de origem (para isso, o método AdicionarTabela).
Perceba que a maioria dos métodos são abstratos. Isso porque não será instanciado diretamente. Seguindo o padrão Template, essa classe contém código básico (e importante) para a construção dos objetos.
A parte que muda, porém, é implementada nos filhos. Estes filhos podem ser as classes do componente, utilizando a RTL
Ou os descendentes podem ser utilizados nos testes para mockar os dados:
Diferente da primeira implementação, são usados constantes (COLUNA_SEM_ALIAS) e valores fixos para construção do objeto. Ainda criar uma camada intermediária, que ficaria responsável única e exclusivamente para inicializar a geração para o tipo certo de banco
Obrigado e aproveite o café!
Hoje vamos começar pela padrão Builder.
Na DB1 Global Software, empresa em que trabalho, estamos participando de um jogo chamado “mestre dos códigos”. Entre as tarefas deste jogo está o uso de alguns padrões de projeto e também de teste unitários. Então em pensei: por que não usar os dois? Faço essa pequena introdução pensando nas pessoas poderiam perguntar: mas por que você não usa framework para mockar os objetos? Oras, porque eu queria usar builders!
Story
A proposta era criar um componente onde o cliente pudesse informar colunas, tabelas, condições, junções e etc. Ao final do processo, o componente teria de ser capaz de gerar SQL de acordo com as informações inseridas. Indo um pouco além, me propus que este componente pudesse gerar SQL para vários bancos diferentes. Assim se eu dissesse que o sql deveria trazer os 10 primeiros registros, para o firebird ele precisa colocarselect limit 10 * from tabela
Enquanto no DB2, o SQL seria semelhante a
select * from Tabela fetch firts 10 rows only
Por isso, optei por criar uma RTL para o componente. E o componente em si funcionaria como façade para essa RTL.
Tudo caminhou bem, até que…
Em determinada altura do projeto, percebi que a operação de criação dos objetos era repetitiva demais e pouca coisa mudava de um processo de criação para o outro. Quando escrevi os testes (não fiz TDD), percebi que o mesmo acontecia. Principalmente porque eu precisava criar várias vezes vários objetos e precisava me assegurar que eles seriam criados da mesma forma. Do contrário, os testes poderiam apresentar falsos-negativosA essa altura, a adoção do builder parecia a melhor solução. E era.
Mão na massa!
Eu optei por criar uma versão genérica do padrão, pensando em reaproveitar a estrutura. Isso é opcionalIBuilder<T> = interface(IInterface) ['{5D20996A-F678-4288-8D59-7F5CBA3305A1}'] procedure ConstruirNovaInstancia; function getObjeto: T; end;
TBuilder<ObjetoSQL> = class(TInterfacedObject, IBuilder<ObjetoSQL>) protected FObjeto: ObjetoSQL; public procedure ConstruirNovaInstancia; virtual; abstract; function getObjeto: ObjetoSQL; end;
(...)
function TBuilder<ObjetoSQL>.getObjeto: ObjetoSQL; begin result := FObjeto; end;
Usando Generics, essa estrutura representa o básico do padrão builder. Como você pode perceber, qualquer classe pode ser utilizada aqui.
Especificando um pouco mais, temos as implementações básicas no nível da RTL.
ISQLBuilder<T> = interface(IBuilder<T>) ['{0F47928E-1F1F-4564-9E81-923139328755}'] procedure SetOtimizarPara(const AOtimizarPara: TOtimizarPara); procedure buildSQL; end;Repare que herdei uma nova interface da básica IBuilder. Assim eu tenho um contrato de Builder’s para a minha RTL. Chamo atenção para o tipo enumerado TOtimizarPara que é um tipo enumerado, contendo o banco para o qual o SQL será gerado. Uma fábrica será responsável por gerar os objetos corretos para o banco informado.
Mas ainda será necessário mais um nível de abstração. Como você pode ver, cada classe possui um builder particular:
Cada classe possui um builder especializado em contruí-la
Vejamos um exemplo:
type IBuilderColuna = interface(ISQLBuilder<ISQLColuna>) ['{0F95620B-5C5D-401F-A86D-CE036E1A9B2F}'] procedure buildNome(); procedure buildNomeVirtual(); procedure buildTabela(); procedure AdicionarTabela(const ATabela: ISQLTabela); end;
Aqui nós temos a interface para criação de colunas. Lembre-se que a coluna tem o nome, tem um nome virtual (select colunas as Nome_virtual) e ela também pode ter uma tabela de origem (para isso, o método AdicionarTabela).
TBuilderColuna = class(TSQLBuilder<ISQLColuna>, IBuilderColuna) private FTabela: ISQLTabela; protected function getTabela: ISQLTabela; public class function New: IBuilderColuna; procedure ConstruirNovaInstancia; override; procedure AdicionarTabela(const ATabela: ISQLTabela); procedure buildNome(); virtual; abstract; procedure buildNomeVirtual(); virtual; abstract; procedure buildTabela(); virtual; abstract; end;
Perceba que a maioria dos métodos são abstratos. Isso porque não será instanciado diretamente. Seguindo o padrão Template, essa classe contém código básico (e importante) para a construção dos objetos.
{ TBuilderColuna }
procedure TBuilderColuna.AdicionarTabela(const ATabela: ISQLTabela); begin FTabela := ATabela; end;
procedure TBuilderColuna.ConstruirNovaInstancia; begin FObjeto := TFabrica.New(getOtimizarPara).Coluna; end;
function TBuilderColuna.getTabela: ISQLTabela; begin result := FTabela end;
class function TBuilderColuna.New: IBuilderColuna; begin result := Create; end;
A parte que muda, porém, é implementada nos filhos. Estes filhos podem ser as classes do componente, utilizando a RTL
type // CB = Concrete Builder TCBColuna = class(TBuilderColuna) private FColuna: TColuna; public constructor Create(const AColuna: TColuna; const OtimizarPara: TOtimizarPara); reintroduce; class function New(const AColuna: TColuna; const OtimizarPara: TOtimizarPara): IBuilderColuna; reintroduce; procedure buildNome; override; procedure buildNomeVirtual; override; procedure buildTabela; override; procedure buildSQL; override; end;
(...)
procedure TCBColuna.buildNome; begin inherited; //FColuna é um objeto, vindo do Façade/Componente, que contém a //entrada do usuário. Esse objeto é passado para cada builder, que //fica encarregado de construir corretamente o objeto à partir da //RTL FObjeto.setColuna(FColuna.Nome); end;
procedure TCBColuna.buildNomeVirtual; begin inherited; FObjeto.setNomeVirtual(FColuna.NomeVirtual); end;
Ou os descendentes podem ser utilizados nos testes para mockar os dados:
TCBColunaTotalmenteVirtual = class; TCBColunaNomeVirtual = class; TCBColunaSimples = class; TCBColunaSimples = class(TCBColunaBase) public procedure buildNome; override; procedure buildNomeVirtual; override; procedure buildTabela; override; procedure AfterConstruction; override; end;
(...)
{ TBuilderColunaSimples }
procedure TCBColunaSimples.AfterConstruction; begin inherited; end;
procedure TCBColunaSimples.buildNome; begin FObjeto.setColuna(COLUNA_SEM_ALIAS); end;
procedure TCBColunaSimples.buildNomeVirtual; begin FObjeto.setNomeVirtual(''); end;
procedure TCBColunaSimples.buildTabela; begin inherited; // Método vazio para evitar abstract erros. end;
Diferente da primeira implementação, são usados constantes (COLUNA_SEM_ALIAS) e valores fixos para construção do objeto. Ainda criar uma camada intermediária, que ficaria responsável única e exclusivamente para inicializar a geração para o tipo certo de banco
TCBColunaBase = class(TBuilderColuna) public procedure AfterConstruction; override; end;
(...)
procedure TCBColunaBase.AfterConstruction; begin inherited; //tipo de banco inicializado em uma unit de constantes //exclusiva para os testes setOtimizarPara(OTIMIZAR_PARA); end;
Ainda não acabou!
Até aqui, definimos a estrutura básica para criação. Mas existe outro papel neste padrão: o Director. Este cara é responsável por fazer com que o builder tenha todas as suas etapas executadas. O director, assim como os builders, também está dividido em camadas. Todavia, como a forma de construir os objetos não muda de projeto (componente ou teste), sua abstração desce até o nível da RTL. Assim, temos o geral:type IDirector<Builder, ObjetoSQL> = interface(IInterface) ['{15286B75-18B5-4A9D-B839-11E02BDAF6CE}'] procedure setBuilder(const ABuilder: Builder); procedure Construir; function getObjetoPronto: ObjetoSQL; end;
TDirector<T, R> = class(TInterfacedObject, IDirector<T, R>) protected FObjeto: R; FBuilder: T; public class function New: IDirector<T, R>; procedure setBuilder(const ABuilder: T); procedure Construir; virtual; abstract; function getObjetoPronto: R; end;E o director para a RTL. Note que não é preciso uma interface para o nível RTL. A Interface geral já é capaz de garantir o tipo do Director
type TDirectorColuna = class(TDirector<IBuilderColuna, ISQLColuna>) public procedure Construir(); override; end;
implementation
{ TDirectorColuna } procedure TDirectorColuna.Construir; begin inherited; FBuilder.ConstruirNovaInstancia; FBuilder.buildSQL; FBuilder.buildNome; FBuilder.buildNomeVirtual; FBuilder.buildTabela; FObjeto := FBuilder.getObjeto; end;
Mas não é muito código?
Sim. Você escreve um bocado. Mas agora imagine repetir o processo de criação de colunas de SQL para cada teste, levando em consideração que eu tenho teste para Colunas, Condicoes, Tabela, Junções e Select. A classe Tabela compõe todas as demais classes. Imagina como seria repetitivo? E ainda: como o código estava em construção, a estrutura de algumas classes poderia mudar (e mudou!). Daí, imagine refatorar todas as ocorrências? Quando você se depara com um código como:procedure TCBColuna.buildTabela; var _director: IDirector<IBuilderTabela, ISQLTabela>; _concreteBuilder: IBuilderTabela; begin inherited; if FColuna.Tabela.Nome.Trim.IsEmpty then exit; _concreteBuilder := TCBTabela.New(FColuna.Tabela, getOtimizarPara); _director := TDirectorTabela.New; _director.setBuilder(_concreteBuilder); _director.Construir; FObjeto.setTabela(_director.getObjetoPronto); end;Você percebe que cada linha a mais é, na verdade, algumas linhas (e horinhas) a menos.
Você ficou curioso sobre padrões de projeto?
Meu amigo André Celestino escreveu uma série sobre os 23 padrões de projeto. Vale muito à pena dar uma conferida e deixar o site nos favoritos como Guia Rápido de Padrões GoF do Tio Dédo. Você pode acompanhar este projeto no meu GitHub. Tem um tempinho que eu não subo nada, mas é porque estou ampliando e reorganizando as pastas do projeto. Em breve, novidades!Obrigado e aproveite o café!
Comentários
Postar um comentário