Builder, pra quê te quero?

Um homem e uma mulher, em uma construção olhando para uma planta de construção

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!

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 colocar

select 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-negativos.

A 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 é opcional

  IBuilder<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:

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!

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.