C# -  Padrão Comportamental Gof - Interpreter


Neste artigo vou apresentar o padrão comportamental Gof Interpreter.

Segundo a definição da Gang Of Four(GoF), dada uma linguagem, o padrão Interpreter define uma representação para sua gramática junto com um intérprete que usa a representação para interpretar frases na linguagem.

Este padrão é usado para avaliar/interpretar as instruções escritas em uma gramática de linguagem ou notações. Ele envolve a implementação de uma interface de expressão que diz para interpretar um contexto específico.



Assim o padrão Interpreter fornece uma maneira de avaliar a gramática ou expressão da linguagem e pode ser usado em análise SQL, mecanismo de processamento de símbolos, em compiladores, em analisadores, etc.

Este padrão é um pouco diferente dos outros padrões de projeto.  Formalmente, o padrão trata de lidar com linguagens com base em um conjunto de regras.

Assim, temos uma linguagem - qualquer linguagem - e suas regras, ou seja, a gramática.  Temos também um intérprete que usa o conjunto de regras para interpretar as sentenças da linguagem.

O uso deste padrão é específico a determinados contextos.

Interpreter : Exemplo de definição

Vamos entender o padrão Interpreter com um exemplo.

Na figura temos , o Contexto que representa o valor que desejamos interpretar.  Aqui o valor do contexto é a data atual:

   

A seguir temos a Gramática e a Expressão das datas.  Aqui temos diferentes tipos de expressão de datas usando os formatos :

- MM-DD-YYYY
- DD-MM-YYYY
- YYYY-MM-DD
- DD-YYYY


E o Interpreter :



Suponha que você deseja expressar a data no formato MM-DD-YYYY.

Então você precisa passar o valor do contexto e a expressão do formato da data que você deseja para o Interpreter que vai converter o valor do contexto no formato da data que você passou.

E se você quiser expressar a data em outros formatos basta repetir o procedimento.

Assim o Interpreter contem a lógica ou gramática para converter o objeto Contexto para um formato especifico usando uma expressão.

E assim que atua o padrão Interpreter.

Diagrama UML

O diagrama UML do padrão Interpreter segundo o Gof apresenta os seguintes participantes


1- Context - Esta é uma classe que contém os dados que queremos interpretar. Ela contém informações (entrada e saída), que são usadas pelo intérprete.

2- AbstractExpression - È uma interface que define o método de interpretação que deve ser implementado pelas subclasses.  Este método usa o objeto de contexto como parâmetro e este objeto de contexto contém os dados que queremos interpretar.

E aqui que definimos a operação Interpretar, que deve ser implementada por cada subclasse.

3- TerminalExpression - É uma classe concreta que implementa a interface AbstractExpression. Ela representa elementos na gramática que não podem ser substituídos, como símbolos.

4- NonTerminalExpression - É uma classe concreta que implementa a interface AbstractExpression. Ela representa elementos que serão substituídos durante a avaliação, como variáveis ou mesmo regras. 

Esta é a classe que implementa a expressão. Isso pode ter outras instâncias do Expression.

5- Client - É uma classe que constrói a árvore de sintaxe abstrata para um conjunto de instruções na gramática fornecida.  Esta árvore é construída com a ajuda de instâncias das classes NonTerminalExpression e TerminalExpression.

Essa é a classe que cria a árvore de sintaxe abstrata para um conjunto de instruções na gramática especificada. Essa árvore é criada com a ajuda de instâncias das classes NonTerminalExpression e TerminalExpression.

Quando podemos usar o padrão

Podemos usar o padrão Interpreter nos seguintes cenários :

- Pode ser usado quando houver uma linguagem para interpretar. Funciona melhor quando :
    - Gramática for simples
    - A eficiência não for uma preocupação crítica

- Quando puder representar sentenças da linguagem como árvores sintáticas abstratas

Vantagens do padrão

Como vantagens deste padrão temos que :

- É fácil mudar e estender a gramática. Como o padrão usa classes para representar regras gramaticais, você pode usar herança para alterar ou estender a gramática.

- As expressões existentes podem ser modificadas de forma incremental e as novas expressões podem ser definidas como variações das antigas.

- Implementar a gramática também é fácil.

Desvantagem

Como desvantagens deste padrão temos que :

- Gramáticas complexas são difíceis de manter. O padrão Interpreter define pelo menos uma classe para cada regra da gramática. Desta forma, gramáticas contendo muitas regras podem ser difíceis de gerenciar e manter.

Aplicação prática do padrão

Como exemplo de implementação vamos converter a informação de data e hora em um formato especifico usando o padrão Interpreter.

Para fazer isso vamos definir diferentes tipos de gramática.  Na figura temos um formato de data :



E vamos definir uma classe para cada tipo de gramática. 

- A classe ExpressaMes para o Mes : Mes -> ExpressaoMes
- A classe ExpressaDia para o Dia : Dia -> ExpressaoDia
- A classe ExpressaAno para o Ano : Ano -> ExpressaoAno
- A classe Separador para o Separador usado na data

Usando esta gramática podemos criar qualquer tipo de formato de data.  Aqui o problema a ser resolvido é nosso domínio que será representando no contexto  e o código de cada classe representa a regra de gramática que vamos usar.

Implementação prática

Levando em conta este cenário vamos implementar o padrão Command usando uma aplicação Console .NET Core (.NET 5.0) criada no VS 2019 Community.

A seguir temos o diagrama de classes obtido a partir do VS 2019 na implementação do padrão:

Podemos identificar as seguinte classes :

- Context - Classe que define a data que desejamos interpretar;
- AbstractExpression -  
Interface que define o método que será implementado pelas classes filhas;
- ExpressaoDia, ExpressaoMes e ExpressaoAno e Separador :
Implementam a interface AbstractExpression;
- Program - Client -
Usa a implementação;

A seguir temos o código usado na implementação:

1- A classe Context

    public class Context
    {
        public string Expressao { get; set; }
        public DateTime Data { get; set; }
        public Context(DateTime data)
        {
            Data = data;
        }
    }

2- A Interface IAbstractExpression

    public interface IAbstractExpression
    {
        void Avaliar(Context context);
    }
   

3- Classe ExpressaoDia

    public class ExpressaoDia : IAbstractExpression
    {
        public void Avaliar(Context context)
        {
            string expressao = context.Expressao;
            context.Expressao =
                expressao.Replace("DD", context.Data.Day.ToString());
        }
    }

4- Classe ExpressaoMes

    public class ExpressaoMes : IAbstractExpression
    {
        public void Avaliar(Context context)
        {
            string expressao = context.Expressao;
            context.Expressao =
                expressao.Replace("MM", context.Data.Month.ToString());
        }
    }

5- Classe ExpressaoAno

    public class ExpressaoAno : IAbstractExpression
    {
        public void Avaliar(Context context)
        {
            string expressao = context.Expressao;
            context.Expressao =
                expressao.Replace("YYYY", context.Data.Year.ToString());
        }
    }

7- Classe Separador

       public class Separador : IAbstractExpression
        {
            public void Avaliar(Context context)
            {
                string espressao = context.Expressao;
                context.Expressao = espressao.Replace(" ", "-");
            }
        }

7- Program

using System;
using System.Collections.Generic;

namespace Interpreter1
{
    class Program
    {
        static void Main(string[] args)
        {
            List<IAbstractExpression> expressoes = new List<IAbstractExpression>();

            Context context = new Context(DateTime.Now);
            Console.WriteLine($"Data atual : {context.Data}\n");

            Console.WriteLine("Selecione a expressão a usar : MM-DD-YYYY  ou  YYYY-MM-DD  " +
                "ou  DD-MM-YYYY ");

            context.Expressao = Console.ReadLine().ToUpper();

            string[] formato = context.Expressao.Split('-');

            foreach (var item in formato)
            {
                if (item == "DD")
                {
                    expressoes.Add(new ExpressaoDia());
                }
                else if (item == "MM")
                {
                    expressoes.Add(new ExpressaoMes());
                }
                else if (item == "YYYY")
                {
                    expressoes.Add(new ExpressaoAno());
                }
            }

            expressoes.Add(new Separador());           

            foreach (var obj in expressoes)
            {
                obj.Avaliar(context);
            }

            Console.WriteLine($"\nData na expressão escolhida : {context.Expressao}");
           

            Console.Read();
        }
    }
}

A execução do projeto irá apresentar o seguinte resultado:

Temos assim um exemplo básico de implementação do padrão Interpreter.

Você pode encontrar algumas semelhanças entre os padrões Command e  Interpreter. O padrão Command diz sobre a conversão das ações em comando para fins de registro ou desfazer. Aqui, os comandos são objetos, mas para o padrão Interpreter, os comandos são sentenças.

O padrão Command pode ser usado para objetos persistentes, serializando a lista de comandos e os resultados em arquivos grandes comparados com o padrão Interpreter.

O padrão Interpreter fornece uma abordagem mais fácil no tempo de execução, mas tem o custo de desenvolver um interpretador.

Pegue o código do projeto aqui :   Interpreter1.zip

"Em Deus louvarei a sua palavra, em Deus pus a minha confiança; não temerei o que me possa fazer a carne."
Salmos 56:4

Referências:


José Carlos Macoratti