Design Patterns - Interpreter

Propósito

El patrón Interpreter (Intérprete) proporciona una manera de evaluar sentencias en un lenguaje, permitiendo interpretar dichas sentencias dentro del programa.

Complejo, ahora de forma simple: Supongamos que tenemos un libro de instrucciones para armar un juguete, pero está escrito en un idioma que no entendemos. El Interpreter es como un traductor, toma esas instrucciones complicadas y las convierte en pasos simples que podemos entender y seguir.

Problema

El problema que resuelve es la necesidad de interpretar y procesar lenguajes o expresiones definidos por reglas gramaticales o sintácticas, especialmente en dominios como compiladores, motores de reglas o procesadores de lenguajes de programación y scripting.

Solución

El patrón Interpreter soluciona este problema implementando una clase para cada regla gramatical en el lenguaje. Cada regla se expresa como una expresión que puede ser interpretada para evaluar la sentencia.

El Árbol de Sintaxis Abstracta

Al hablar del patrón Interpreter, debemos mencionar el Arbol de Sintaxis Abstracta (AST), una representación clave de la estructura de una expresión.

Un AST es una estructura de árbol donde cada nodo representa una operación o entidad en la expresión, como un número o una operación matemática:

  • Nodos Hoja (Leaf Nodes): Representan valores o operandos, como números en una expresión matemática. Por ejemplo, ‘1’ y ‘2’ en “1 + 2”.
  • Nodos Internos (Internal Nodes): Representan operadores que toman uno o más operandos. Por ejemplo, el ‘+’ en “1 + 2” sería un nodo interno que conecta los nodos hoja ‘1’ y ‘2’.

¿Cómo se Relaciona con el Patrón Interpreter?

Cuando se usa el patrón Interpreter para interpretar una expresión, generalmente se construye un AST como parte del proceso de interpretación.

Cada tipo de expresión en la gramática del lenguaje (números, operaciones matemáticas, etc.) tiene una clase correspondiente en el sistema, y cada instancia de esas clases representa un nodo en el AST.

Por ejemplo, para la expresión “1 + 2”:

  1. Se crea un nodo hoja para ‘1’.

  2. Se crea un nodo hoja para ‘2’.

  3. Se crea un nodo interno para ‘+’, con los nodos ‘1’ y ‘2’ como hijos.

Este árbol se construye analizando la expresión y creando los nodos correspondientes. Luego, para interpretar la expresión, el sistema recorrería este árbol y realizaría las operaciones definidas en cada nodo.

interpreter

El uso del árbol permite al sistema descomponer las expresiones en partes manejables y luego interpretarlas de una manera estructurada y coherente.

Estructura

interpreter

Participantes

  • AbstractExpression: Declara una operación interpretativa para interpretar un contexto.
  • TerminalExpression: Implementa la interpretación para símbolos terminales, específicos del lenguaje.
  • NonTerminalExpression: Representa reglas gramaticales para símbolos no terminales y, usualmente, suelen ser otras instancias de AbstractExpression.
  • Context: Contiene información global utilizada durante la interpretación.
  • Client: Construye la representación sintáctica del lenguaje y luego invoca la interpretación.

Cuándo Usarlo

Este patrón es recomendable cuando:

  • hay un lenguaje que interpretar y se puede representar como un árbol de sintaxis abstracta.
  • se necesita interpretar expresiones definidas en un lenguaje o notación simple.

Ventajas

Flexibilidad: Permite interpretar nuevos tipos de expresiones.

Extensibilidad: Fácil de ampliar y modificar el lenguaje o gramática.

Desventajas

Complejidad: Puede volverse complejo si la gramática del lenguaje es complicada.

Rendimiento: Interpretar expresiones puede ser menos eficiente que otros métodos de procesamiento.

Ejemplo: Sistema de interpretación de expresiones matemáticas

Debemos crear un sistema educativo cuyo propósito es interpretar y evaluar expresiones matemáticas simples ingresadas por los usuarios.

Estas expresiones pueden incluir operaciones básicas como suma, resta, multiplicación y división.

Problema

El sistema debe ser capaz de entender y procesar una variedad de expresiones matemáticas ingresadas en formato texto. Implementar un enfoque directo para interpretar estas expresiones puede resultar en un código complejo y difícil de mantener, especialmente al añadir más operaciones o cambiar la gramática de las expresiones.

Solución planteada

Elegimos utilizar el patrón Interpreter porque ofrece una solución estructurada y extensible para interpretar lenguajes. Definimos una gramática para las expresiones matemáticas, construimos un intérprete que entienda esa gramática y luego lo usamos para interpretar las expresiones en el lenguaje definido.

Definición de la gramática

Vamos a interpretar una expresión matemática básica: “1 + 2”

En este caso:

  • “1” y “2” son números que queremos sumar.
  • “+” es la operación que queremos realizar con esos números.

Al usar el patrón Interpreter descomponemos esta expresión en partes que el sistema puede entender y procesar una por una.

Cómo se interpreta “1 + 2”?:

  1. Interpretar “1”: El sistema ve el primer ‘1’ y lo reconoce como un número. Utilizamos un NumberExpression para representar este número.

  2. Interpretar “+”: Luego ve el símbolo ‘+’, que representa la operación de suma. El sistema utiliza una AddExpression para prepararse para sumar los números que encuentra.

  3. Interpretar “2”: Finalmente, ve el ‘2’ y lo reconoce como otro número, utilizando otro NumberExpression para representarlo.

  4. Calcular el Resultado: Ahora, el sistema tiene todo lo que necesita para calcular el resultado. La AddExpression toma los dos NumberExpression (representando ‘1’ y ‘2’) y los suma, produciendo el resultado ‘3’.

En resumen, el sistema toma la expresión “1 + 2”, la descompone en partes manejables (‘1’, ‘+’, ‘2’), y luego las procesa paso a paso para llegar al resultado de ‘3’. Todo esto se hace siguiendo las reglas y la estructura definidas en el patrón Interpreter para entender y procesar la expresión.

Declaramos la interfaz Expression con el método interpret. Implementamos las clases NumberExpression y AddExpression, por un lado la lógica para interpretar números y por otro la operación de suma. El cliente construye una expresión y luego la evalúa llamando al método interpret().

interpreter

Código Java

Creamos la Interfaz Expression:


              
  interface Expression {
      int interpret();
  }
  
              

Implementamos el Terminal Expression:


              
  class NumberExpression implements Expression {
      private int number;
  
      public NumberExpression(int number) {
          this.number = number;
      }
  
      @Override
      public int interpret() {
          return number;
      }
  }
              
              

Creamos un Non Terminal Expression:


              
  class AddExpression implements Expression {
      private Expression firstExpression, secondExpression;

      public AddExpression(Expression first, Expression second) {
          this.firstExpression = first;
          this.secondExpression = second;
      }

      @Override
      public int interpret() {
          return firstExpression.interpret() + secondExpression.interpret();
      }
  }

  // Puedo agregar clases para restar, multiplicar, etc.
              
              

El código cliente:


              
  public class Client {
      public static void main(String[] args) {
          Expression expression = new AddExpression(new NumberExpression(1), new NumberExpression(2));
          int result = expression.interpret();
          System.out.println("El resultado es: " + result);
      }
  }
              
              

Mapeo (del ejemplo a Participantes)

Los participantes que vimos antes son: AbstractExpression, TerminalExpression, NonTerminalExpression, Context, Client:

  • Expression (AbstractExpression): Interfaz común para todas las expresiones.
  • NumberExpression (TerminalExpression): Representa números en la expresión.
  • AddExpression (NonterminalExpression): Representa la operación de suma.
  • Client (Client): Construye y evalúa la expresión.

Conclusiones

Este ejemplo muestra cómo usar el patrón Interpreter para construir y evaluar una expresión matemática.

Este enfoque nos dió flexibilidad para extenderlo (podemos agregar fácilmente otras operaciones).

Patrones relacionados

  • Composite

Se puede usar para construir la estructura de árbol de sintaxis abstracta.

  • Flyweight

Puede utilizarse para compartir instancias de TerminalExpression.