[Delphi] Design Patterns – Interpreter

Boa noite, pessoal, tudo certo?
Quando solicitamos o build de um projeto no Delphi, o compilador é acionado para interpretar as instruções do código-fonte, gerando um executável como artefato.
Imagine se existisse uma forma de interpretar regras de negócio através de uma sintaxe definida, produzindo um resultado, semelhante a um compilador? Bom, a boa notícia é que existe, sim! Estamos basicamente nos referindo ao objetivo do padrão de projeto Interpreter!

 

Antes de iniciar o artigo, gostaria de prestar um grande agradecimento a duas pessoas: Luís Gustavo Fabbro, do Balaio Tecnológico, por gentilmente ter respondido o meu e-mail sobre o Interpreter; e Wagner Landgraf, da TMS Software, por apresentar e explicar alguns exemplos reais de aplicação do Interpreter. A propósito, recomendo os componentes da TMS. São excelentes!

Durante o desenvolvimento de um software, embora não tão comum, pode surgir uma situação em que seja necessário interpretar fórmulas, frases ou símbolos com aspecto dinâmico, ou seja, que não trazem consigo uma estrutura fixa. Por exemplo, considere uma funcionalidade de tradução de um conteúdo em um idioma específico (como português) para uma linguagem computacional. Para que isso seja factível, é preciso criar um interpretador que analise a sintaxe do conteúdo e faça as conversões corretamente.
O padrão de projeto Interpreter apresenta uma ótima solução para trabalhar com cenários que partilham dessa característica. Ao utilizá-lo, basta fornecer um dado de entrada e solicitar que o padrão de projeto execute as interpretações, respeitando uma sintaxe, e produza um resultado esperado.

 

Pode apresentar alguns exemplos?
Claro! O Google Tradutor é um exemplo clássico de aplicação do Interpreter. O serviço é capaz de interpretar o texto digitado pelo usuário e traduzi-lo para o idioma desejado, desde que a sintaxe (neste caso, a ortografia do idioma de origem) esteja correta. Caso contrário, a palavra ou o texto não terá tradução. Outro exemplo tradicional do Interpreter, bem comum de encontrar na internet, é a conversão de algarismos romanos em números decimais. Com base em expressões (“V” igual a 5, “X” igual a 10, etc), o padrão de projeto realiza os cálculos considerando a gramática de cada algarismo e o número de casas, retornando o valor em decimal.
Há ainda outros exemplos, como a conversão de medidas através de frases em linguagem natural, como “26 quilômetros em milhas”.

Para empregar o Interpreter, deve-se trabalhar com alguns elementos sugeridos pelo padrão. O primeiro é o Context, que representa tanto os dados a serem processados quanto o resultado do processamento, portanto, geralmente possui duas variáveis: uma entrada e uma saída. Essa divisão ocorre em virtude de três motivos:

  • Os dados de entrada não devem sofrer alterações durante o processamento, ou seja, cada objeto que realiza uma interpretação deve receber os dados de forma primitiva para avaliá-los;
  • Em acréscimo ao primeiro item, o resultado de cada interpretação é armazenado na variável de saída, evitando modificações nos dados de entrada;
  • Após o processamento, talvez seja necessário apresentar a entrada e a saída para efeitos de comparação ou análise.

Os objetos que interpretam o Context recebem o nome de Expressions, em uma analogia às expressões de uma linguagem de programação. Cada Expression herda de uma classe AbstractExpression – que declara um método abstrato de interpretação – e pode ser categorizado como TerminalExpression ou NonTerminalExpression, nomenclaturas que também fazem parte do dialeto da ciência da computação. Em poucas palavras (poucas mesmo!), o primeiro representa expressões independentes que podem avaliar a entrada de modo imediato. O segundo, por sua vez, depende de outras expressões para avaliar os valores, ou melhor, é composto por outras expressões para interpretar o contexto. Para compreender melhor estes conceitos, imagine que objetos TerminalExpression são como variáveis e objetos NonTerminalExpression são semelhantes a operadores lógicos.

 

Nossa, então vamos construir um compilador?
Com o Interpreter é possível, sim, construir um compilador. Aliás, este pode ser um dos melhores propósitos do padrão de projeto!
Porém, aqui no blog, o meu objetivo é associar padrões de projeto a contextos mais próximos do trabalho que realizamos no dia-a-dia, muitas vezes envolvendo requisitos de negócio. Como aplicação de exemplo, utilizaremos o Interpreter para traduzir uma frase simples (em português mesmo) para uma instrução SQL. Por exemplo, ao digitar o texto:

Atualizar o nome do cliente 2 para André Celestino

A nossa aplicação irá interpretá-lo para devolver a seguinte instrução SQL:

UPDATE clientes SET nome = "André Celestino" WHERE ID = 2

Para isso, claro, é obrigatório que a frase obedeça uma sintaxe definida. Por exemplo, para atualizar uma informação, o usuário deve usar a palavra “Atualizar”, no infinitivo, ou a nossa aplicação não será capaz de compreendê-la. Na verdade, este não deixa de ser o mesmo comportamento por trás da compilação do código-fonte. Ao digitar ShoMessage ao invés de ShowMessage, o compilador levantará uma crítica de identificador não encontrado.

 

A nossa codificação inicia-se com o Context. A classe é bem pequena e contém apenas as variáveis que armazenarão os valores de entrada e saída:

type
  { Context }
  TContext = class
  public
    Entrada: string;
    Saida: string;
  end;

 

Em seguida, codificaremos a classe AbstractExpression, que será uma abstração para todas as expressões concretas. Vale lembrar que, quando digo “expressões”, me refiro aos “interpretadores” do contexto.
Já que será necessário interpretar diferentes partes de uma string, utilizaremos uma variável do tipo TStringList com visibilidade protegida – que será acessível nas classes herdadas – para evitar a repetição de código.

type
  { AbstractExpression }
  TAbstractExpression = class
  protected
    // Variável que armanezará as partes da entrada
    Partes: TStringList;
  public
    constructor Create;
    destructor Destroy; override;
 
    // Método que será sobrescrito por todas as classes herdadas
    procedure Interpretar(Contexto: TContext); virtual; abstract;
  end;
 
implementation
 
uses
  SysUtils;
 
{ TAbstractExpression }
 
constructor TAbstractExpression.Create;
begin
  Partes := TStringList.Create;
end;
 
destructor TAbstractExpression.Destroy;
begin
  FreeAndNil(Partes);
end;

 

O próximo passo será bem extenso. Criaremos quatro classes TerminalExpression para interpretar cada parte do contexto de entrada. A primeira delas será responsável pela interpretação do comando:

type
  { TerminalExpression }
  TComandoExpression = class(TAbstractExpression)
  public
    procedure Interpretar(Contexto: TContext); override;
  end;
 
implementation  
 
{ TComandoExpression }
 
procedure TComandoExpression.Interpretar(Contexto: TContext);
begin
  // Se existir a palavra "selecionar", traduz para "Select"
  if Pos('selecionar', LowerCase(Contexto.Entrada)) > 0 then
    Contexto.Saida := 'Select'
 
  // Se existir a palavra "atualizar", traduz para "Update"
  else if Pos('atualizar', LowerCase(Contexto.Entrada)) > 0 then
    Contexto.Saida := 'Update'
 
  // Se existir a palavra "excluir", traduz para "Delete"
  else if Pos('excluir', LowerCase(Contexto.Entrada)) > 0 then
    Contexto.Saida := 'Delete';
end;

 

A segunda classe identificará as colunas:

type
  { TerminalExpression }
  TColunasExpression = class(TAbstractExpression)
  public
    procedure Interpretar(Contexto: TContext); override;
  end;
 
implementation
 
{ TColunasExpression }
 
procedure TColunasExpression.Interpretar(Contexto: TContext);
var
  PosicaoQuebra: integer;
  PosicaoEspaco: integer;
begin
  // Extrai as strings da entrada do contexto
  ExtractStrings([' '], [], PWideChar(Contexto.Entrada), Partes);
 
  if Pos('Select', Contexto.Saida) > 0 then
  begin
    PosicaoQuebra := Pos('dos', LowerCase(Contexto.Entrada))
                   + Pos('das', LowerCase(Contexto.Entrada));
 
    // Se não existirem as palavras "dos" ou "das",
    // então seleciona-se todas as colunas (*)
    if PosicaoQuebra = 0 then
    begin
      Contexto.Saida := Format('%s *', [Contexto.Saida, Partes[1]]);
      Exit;
    end;
 
    // Caso contrário, obtém as colunas informadas
    PosicaoEspaco := Pos(' ', Contexto.Entrada);
    Contexto.Saida := Format('%s %s', [Contexto.Saida,
      Copy(Contexto.Entrada, PosicaoEspaco, PosicaoQuebra - PosicaoEspaco)]);
  end;
end;

 

A próxima classe irá contribuir com o nome da tabela:

type
  { TerminalExpression }
  TTabelaExpression = class(TAbstractExpression)
  public
    procedure Interpretar(Contexto: TContext); override;
  end;
 
implementation
 
{ TTabelaExpression }
 
procedure TTabelaExpression.Interpretar(Contexto: TContext);
var
  PosicaoQuebra: integer;
  PosicaoEspaco: integer;
begin
  // Extrai as strings da entrada do contexto
  ExtractStrings([' '], [], PWideChar(Contexto.Entrada), Partes);
 
  if Pos('Select', Contexto.Saida) > 0 then
  begin
    PosicaoQuebra := Pos('dos', LowerCase(Contexto.Entrada))
                   + Pos('das', LowerCase(Contexto.Entrada));
 
    // Se não existirem as palavras "dos" ou "das",
    // a segunda parte do texto é o nome da tabela
    if PosicaoQuebra = 0 then
    begin
      Contexto.Saida := Format('%s from %s', [Contexto.Saida, Partes[1]]);
      Exit;
    end;
 
    // Caso contrário, é necessário calcular o nome da tabela
    // após as palavras "dos" ou "das"
    Inc(PosicaoQuebra, 4);
    PosicaoEspaco := PosEx(' ', Contexto.Entrada, PosicaoQuebra);
 
    if PosicaoEspaco = 0 then
      PosicaoEspaco := Length(Contexto.Entrada);
 
    Contexto.Saida := Concat(Contexto.Saida,
      Format(' from %s',
      [Copy(Contexto.Entrada, PosicaoQuebra, Abs(PosicaoQuebra - PosicaoEspaco))]));
 
    Exit;
  end;
 
  // Se o comando for Update, a quarta parte do texto é o nome da tabela
  if Pos('Update', Contexto.Saida) > 0 then
  begin
    Contexto.Saida := Format('%s %s', [Contexto.Saida, Partes[3] + 's']);
    Exit;
  end;
 
  // Se o comando for Delete, a segunda parte do texto é o nome da tabela
  if Pos('Delete', Contexto.Saida) > 0 then
  begin
    Contexto.Saida := Format('%s from %s', [Contexto.Saida, Partes[1] + 's']);
  end;
end;

 

Por fim, a quarta e última classe TerminalExpression será encarregada de interpretar a condição:

type
  { TerminalExpression }
  TCondicaoExpression = class(TAbstractExpression)
  public
    procedure Interpretar(Contexto: TContext); override;
  end;
 
implementation
 
{ TCondicaoExpression }
 
procedure TCondicaoExpression.Interpretar(Contexto: TContext);
var
  Posicao: integer;
  Valor: string;
begin
  // Extrai as strings da entrada do contexto
  ExtractStrings([' '], [], PWideChar(Contexto.Entrada), Partes);
 
  // Se existir a palavra "de", significa que a busca é por cidade
  Posicao := Pos(' de ', LowerCase(Contexto.Entrada));
  if Posicao > 0 then
  begin
    Valor := Copy(Contexto.Entrada, Posicao + 4, Length(Contexto.Entrada));
    Contexto.Saida := Concat(Contexto.Saida,  Format(' where cidade = "%s"', [Valor]));
    Exit;
  end;
 
  // Se existir a frase "com nome", significa que a busca é por nome
  Posicao := Pos('com nome', LowerCase(Contexto.Entrada));
  if Posicao > 0 then
  begin
    Valor := Copy(Contexto.Entrada, Posicao + 9, Length(Contexto.Entrada));
    Contexto.Saida := Concat(Contexto.Saida,  Format(' where nome = "%s"', [Valor]));
    Exit;
  end;
 
  // Se existir a palavra "para", significa que é uma atualização (Update)
  Posicao := Pos('para', LowerCase(Contexto.Entrada));
  if Posicao > 0 then
  begin
    Valor := Copy(Contexto.Entrada, Posicao + 5, Length(Contexto.Entrada));
    Contexto.Saida := Concat(Contexto.Saida,
      Format(' set %s = "%s" where ID = %s', [Partes[1], Valor, Partes[4]]));
    Exit;
  end;
 
  // Se for um comando Delete,
  // apenas identifica se o critério de exclusão é o ID ou Nome
  if Pos('Delete', Contexto.Saida) > 0 then
  begin
    if StrToIntDef(Partes[2], 0) > 0 then
      Contexto.Saida := Format('%s where ID = %s', [Contexto.Saida, Partes[2]])
    else
      Contexto.Saida := Format('%s where nome = "%s"', [Contexto.Saida, Partes[2]]);
  end;
end;

 

Pessoal, é importante ressaltar que, para que o código (e o artigo) não ficasse muito extenso, as classes acima foram codificadas para interpretar comandos bem básicos e não possuem validações para criticar sintaxes incorretas. Como o objetivo é exemplificar o padrão de projeto de forma didática, procurei codificar o mínimo de detalhes possível.

O último passo é escrever o consumidor das nossas classes, ou seja, o Client, que será um formulário composto por dois campos de texto (um para informar a entrada e outro para exibir a saída) e um botão para executar a interpretação. Você observará, a seguir, que para armazenar todas as expressões responsáveis por interpretar o contexto, é necessário criar uma Árvore Sintática, ou Syntax Tree. Em uma analogia, essa árvore assemelha-se com o conjunto de funções que o compilador executa no código-fonte: análise da sintaxe, verificação das diretivas e inspeção da semântica para expor hints e warnings. No nosso caso, cada função da árvore é uma expressão que interpreta uma parte da frase:

var
  Contexto: TContext;
  ArvoreSintatica: TObjectList;
  Contador: integer;
begin
  // Cria o contexto
  Contexto := TContext.Create;
 
  // Cria a árrvore sintática
  ArvoreSintatica := TObjectList.Create;
  try
    // Preenche a entrada do contexto
    Contexto.Entrada := EditInstrucao.Text;
 
    // Configura a árvore sintática com as expressões
    ArvoreSintatica.Add(TComandoExpression.Create);
    ArvoreSintatica.Add(TColunasExpression.Create);
    ArvoreSintatica.Add(TTabelaExpression.Create);
    ArvoreSintatica.Add(TCondicaoExpression.Create);
 
    // Percorre as expressões para traduzir a entrada em instrução SQL
    for Contador := 0 to Pred(ArvoreSintatica.Count) do
      TAbstractExpression(ArvoreSintatica[Contador]).Interpretar(Contexto);
 
    // Exibe a saída do contexto (SQL)
    EditSaida.Text := Contexto.Saida;
  finally
    // Libera as variáveis da memória
    FreeAndNil(ArvoreSintatica);
    FreeAndNil(Contexto);
  end;
end;

 

Concluído!
Com a aplicação acima, as frases abaixo…

Selecionar clientes com nome Beatriz
Selecionar ID, Nome, CPF dos clientes de São Paulo
Excluir cliente João

…são transformadas nas seguintes instruções SQL:

SELECT * FROM clientes WHERE nome = "Beatriz"
SELECT  ID, Nome, CPF  FROM clientes WHERE cidade = "São Paulo"
DELETE FROM clientes WHERE nome = "João"

Pode parecer um exemplo simples, mas considere que essa funcionalidade seja associada a comandos por voz em um aplicativo móvel. Bastaria o usuário falar “Selecionar clientes de São Paulo” para que a rotina executasse uma consulta no banco de dados. Imagina! 🙂

 

André, qual a diferença de implementar o código sem o padrão de projeto, criando e usando as classes de interpretação em sequência?
Este cenário apresentando no artigo realmente poderia ser implementado sem o Interpreter. Neste caso, bastariam apenas as classes de expressões (que teriam outro nome, claro) para processar os dados, logo, o Context e a Syntax Tree não existiram. Porém, com a implementação do Interpreter, é possível enumerar vantagens relevantes:
1) Não é necessário criar variáveis locais para preencher os parâmetros de entrada e/ou para receber o resultado de cada função;
2) As classes de expressões herdam de uma mesma abstração, implicando que todas elas possuem um método principal – no exemplo, “Interpretar” – além de recursos protegidos compartilhados;
3) Como mencionado nos artigos anteriores, a separação de responsabilidades estimula o baixo acoplamento. Cada elemento do Interpreter possui somente uma atribuição na arquitetura;
4) Caso seja necessário adicionar uma nova expressão, a única modificação no Client será apenas a inclusão de uma nova linha na montagem da árvore sintática;
5) Como o contexto é uma classe, pode-se adicionar algumas formatações ou validações tanto na entrada como na saída de dados.

 

Leitores, para poupar o tempo de copy/paste dos códigos deste artigo, baixe o projeto de exemplo no link abaixo. Coloquei algumas frases de modelo no formulário principal para que você possa testá-las!

Exemplo de Interpreter com Delphi

 

Fico por aqui, pessoal.
Grande abraço!


 

Compartilhe!
Share on FacebookTweet about this on TwitterShare on LinkedInShare on Google+Pin on PinterestEmail this to someone

Deixe uma resposta

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

Preencha o campo abaixo * Time limit is exhausted. Please reload CAPTCHA.