[Delphi] Design Patterns GoF – 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 isso existe. Estamos basicamente nos referindo ao objetivo do padrão de projeto Interpreter!

Introdução

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.

Analogia

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 o texto “26 quilômetros em milhas”.

Elementos

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!), TerminalExpression representa expressões independentes que podem avaliar a entrada de modo imediato. NonTerminalExpression, 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.

Para se ter uma ideia, com o Interpreter é possível construir um compilador. Aliás, este pode ser um dos melhores propósitos do padrão de projeto.

Exemplo de codificação do Interpreter

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:

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

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.

Classe Context

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:

Classe AbstractExpression

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.

Classes TerminalExpression

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:

A segunda classe identificará as colunas:

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

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

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.

Em ação!

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:

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

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

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. Imagine! 🙂

Conclusão

Este cenário apresentando no artigo 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, é possível adicionar 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!

 

Fico por aqui, pessoal.
Grande abraço!


 

André Celestino