Refatoração com Class Interceptors e Métodos Anônimos

Quando assunto é manipulação de bancos de dados, há poucas ferramentas tão práticas e convenientes quanto os DataSets do Delphi. Instacie um dataset qualquer, informe uma consulta sql, chame o método open e bingo! temos uma cópia dos registros do banco (e uma infinidade de metadados) em memória, prontos para serem manipulados.

procedure Foo;
var
  qry: TSqlQuery;
begin
  qry := TSqlQuery.Create;
  try
    qry.CommandText := 'select some fields from some table';
    qry.Open;
    while not qry.Eof do
    begin
      //processa o registro atual..
      //e avança para o próximo
      qry.Next;
   end;
   finally
     qry.Close;
     FreeAndNil(qry);
   end;
end;

Prático e simples, não é?

Pois bem! Image dar manutenção em um sistema onde há milhares de funções exatamente iguais a esta, espalhadas por centenas de units e você começará sentir calafrios.


Há algo muito errado aqui!

 - Dont' repeat yourself, don't repeat yourself, don't repeat yourself - eu repetia em minha mente cada vez que esbarrava em mais uma cópia deste código.

Normalmente, num caso destes, a saída é bem simples: criar uma classe descentente de TSqlQuery e implementar um método que encapsule a lógica de criação,abetura, navegação, fechamento e liberação da query. Mas, para funcionar, é preciso alterar todas as ocorrências de TSqlQuery para a nova classe criada e isso implicaria mais trabalho chato, repetitivo e sujeito a erros.


Class Interceptors


Interceptor Design Pattern
A sintaxe do object pascal (ou delphi language, como foi rebatizada nas versões mais recentes), permite declarar classes com o mesmo nome se elas estiverem em units diferentes. Quando isto ocorrer, a segunda classe irá interceptar a primeira e irá extendê-la, no escopo de sua unit, combinando as duas classes em uma só.

Com isto em mente, resolver o problema do código repetido acima fica mais fácil. Basta interceptar a classe TSqlQuery nas units em que eu estou trabalhando e extendê-las com uma função mais inteligente para iterar sobre seus dados.

Veja o código abaixo:

unit SQLExprInterceptor;

interface

uses
  SqlExpr;

type
  TSqlQueryRowCallback = reference to procedure;
  TSqlQuery = class( SqlExpr.TSqlQuery )
  public
    procedure ForEachRow( const sql : string; callback: TSqlQueryRowCallback ); static;
  end;

implementation

procedure TSqlQuery.ForEachRow(const sql: string; callback: TSqlQueryRowCallback);
var
  qry : TSqlQuery;
begin
  qry := TSqlQuery.Create;
  qry.Close;
  qry.CommandText := sql;
  try
    qry.Open;
    while not qry.Eof do begin
      callback;
      qry.Next;
    end;
  finally
    qry.Close;
    FreeAndNil(qry);
  end;
end;

end.

A classe TSqlQuery foi declarada como um referência para a classe original ( que está dentro da unit SqlExpr )


TSqlQuery = class( SqlExpr.TSqlQuery )

e extendemos sua declaração com um método que recebe um sql e uma referência para nosso callback, cuja assintatura é de um método simples, sem parâmetros.

TSqlQueryRowCallback = reference to procedure;

Agora, com a ajuda dos métodos anônimos, podemos eliminar todo o código repetitivo do início do texto e substituí-lo pelo código abaxo:

procedure Foo;
begin
   TSqlQry.ForEachRow('select some fields from some table', 
     procedure begin
        //processa o registro atual...
     end);
end;

Agora sim! Está muito melhor. Note que o único requisito para que os class interceptors funcionem, é que a referência à classe original apareça antes da unit que contém as extensões para a classe.

Abraços.