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:
1 |
Atualizar o nome do cliente 2 para André Celestino |
A nossa aplicação irá interpretá-lo para devolver a seguinte instrução SQL:
1 |
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.
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:
1 2 3 4 5 6 7 |
type { Context } TContext = class public Entrada: string; Saida: string; end; |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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; |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
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.
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
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…
1 2 3 |
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:
1 2 3 |
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. 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:
- 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;
- 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;
- 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;
- 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;
- 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!