Patrones de Diseño (XVI): Patrones de Comportamiento - Interpreter

El patrón que vamos a destripar hoy es Interpreter, cuyo objetivo es interpretar un lenguaje definido previamente mediante su gramática.

Todos los que conozcáis la Teoría de Gramáticas Formales, entenderéis este patrón fácilmente. Para los que no la conozcáis, os recomiendo echar un vistazo a algún tutorial o la definición de la Wikipedia antes de continuar, pues se emplearán conceptos específicos que deben ser previamente conocidos.

Lo que trata este patrón, como su propio nombre indica, es implementar un intérprete para un lenguaje concreto. Un ejemplo rápido de lenguaje para saber de lo que hablamos sería el de las expresiones aritméticas: dada una expresión aritmética debemos ser capaces de construir un intérprete que tomándola como entrada obtenga el resultado de evaluar dicha expresión.

Veamos la plantilla y un ejemplo para conocer en detalle cómo debemos implementar este patrón.

Interpreter


Nombre: Interpreter

Problema y Contexto:

Nuestro sistema tiene que ser capaz de reconocer sentencias de un lenguaje previamente conocido (mediante su gramática), poder evaluar expresiones del mismo y ser capaz de ejecutar las sentencias recibidas.

Por ejemplo tenemos un sistema que recibe como entradas números romanos, y debemos de poder tratarlas como números enteros para poder trabajar computacionalmente con ellos.

Se aplica cuando:

  • Debemos trabajar con sentencias de un lenguaje que nuestro lenguaje de programación no reconoce automáticamente.
  • La gramática del lenguaje con el que debemos trabajar es sencilla. Para gramáticas complejas se deben emplear técnicas concretas de la Teoría de Gramáticas Formales (scanners y parsers).
  • No debe utilizarse si ya existe alguna clase nativa que interprete éste lenguaje. Por ejemplo en Python, si recibimos una expresión aritmética mediante un String, utilizaremos la función eval() en lugar de implementar un Interpreter.



Solución y Estructura:

La solución es representar la gramática del lenguaje (previamente definida) mediante una jerarquía de objetos. Los nodos terminales de las producciones los representaremos creando clases TerminalExpression y los nodos no terminales con NonterminalExpression. Ambas clases implementan una interfaz común (o heredan de una clase abstracta común) llamada AbstractExpression y que contendrá la declaración del métdo interpret(), que se encargará de evaluar el nodo en concreto.

Además puede existir un contexto común a todas las expresiones que defina ciertos valores, funciones o características del lenguaje que estamos interpretando. Este contexto será representado con la clase Context. El cliente se encargará de construir el árbol sintáctico de la expresión y asignar el contexto en caso de haberlo.

La estructura es la siguiente:

https://upload.wikimedia.org/wikipedia/commons/2/21/Interpreter_design_pattern.png

Donde:

AbstractExpression: interfaz o clase abstracta que se encargará de ejecutar una operación.

TerminalExpression: define una implementación concreta para un nodo terminal de la gramática.

NonterminalExpression: define una implementación concreta para un nodo no-terminal de la gramática.

Context: Representa el contexto que envuelve al lenguaje. Puede contener información específica aplicable a todas las expresiones. Si no hay información específica, Context es prescindible.

Client: Se encarga de representar las expresiones del lenguaje objetivo mediante objetos TerminalExpression y NonterminalExpression, construyendo el árbol sintáctico de la expresión.

Consecuencias:


POSITIVAS:

  • Fácil modificación y ampliación de los elementos gramaticales (al ser representados mediante una jerarquía de clases).
  • Su implementación es sencilla (en comparación con otros métodos para implementar esta funcionalidad).
  • La implementación de los métodos para cada elemento del lenguaje es dinámica (puede cambiarse en tiempo de ejecución).

NEGATIVAS:

  • No es muy eficiente.
  • No cubre gramáticas complejas.


Patrones Relacionados: Composite, Flyweight, Iterator y Visitor.

Ejemplo:

Vamos a ilustrar el uso de este patrón mediante un ejemplo en Java. Supongamos que nuestro sistema tiene que sumar y/o restar números del 0 al 9 en formato textual del tipo "uno mas cinco menos cuatro", y nuestro sistema debe dar un resultado entero, en este caso 2.

// En primer lugar definiremos el Context, que nos permitirá gestionar las
// entradas y salidas del sistema
public class Context{

private String nextOp = "";
private int operator = 0;
private int result = 0;

public int getInteger(String in)
{
if(in.toLowerCase().equals("cero"))
return 0;
else if(in.toLowerCase().equals("uno"))
return 1;
else if(in.toLowerCase().equals("dos"))
return 2;
else if(in.toLowerCase().equals("tres"))
return 3;
else if(in.toLowerCase().equals("cuatro"))
return 4;
else if(in.toLowerCase().equals("cinco"))
return 5;
else if(in.toLowerCase().equals("seis"))
return 6;
else if(in.toLowerCase().equals("siete"))
return 7;
else if(in.toLowerCase().equals("ocho"))
return 8;
else if(in.toLowerCase().equals("nueve"))
return 9;
else
return -1;
}

public void setOperator(int operator)
{
this.operator = operator;
}

public void setOperation(String operation)
{
if(operation.toLowerCase().equals("mas"))
this.nextOp = "+";
else if(operation.toLowerCase().equals("menos"))
this.nextOp = "-";
}

public void calculate()
{
if(this.nextOp.toLowerCase().equals("") ||
this.nextOp.toLowerCase().equals("+") )
this.result += operator;
else if(this.nextOp.toLowerCase().equals("-"))
this.result -= operator;
}

pubic int getResult()
{
return this.result;
}
}

// A continuación definiremos la interfaz para AbstractExpression

public interface Expression
{
public abstract void interpret(Context context);
}

// En este ejemplo, por simplicidad, sólo utilizaremos nodos terminales.
// Definiremos 2 tipos: OperatorExpression y NumericExpression

// Definimos OperationExpression para las operaciones mas y menos

public class OperationExpression implements Expression
{

private String operation;

pubilc OperationExpression(String token)
{
this.operator = token;
}
public interpret(Context context)
{
context.setOperation(this.operation);
context.calculate();
}
}

// Definimos NumericExpression para los operadores numéricos

public class NumericExpression implements Expression
{
private String value;

pubilc NumericExpression(String token)
{
this.value = token;
}
public interpret(Context context)
{
context.setOperator(context.getInteger(this.value));
context.calculate();
}
}

// Una vez tenemos todos los componentes, pasemos a definir el cliente,
// que se encargará de montar el árbol sintáctico y evaluar la expresión.

// La ejecución de nuestro programa se realizará de la siguiente manera:
// java Interpreter uno mas cinco menos cuatro

public class Interpreter{
public static void main(String [] args)
{
// Creamos el arbol de expresiones y el contexto
ArrayList tree = new ArrayList();
Context context = new Context();

// Añadimos los tokens pasados como argumentos
for(String token : args)
{
if(context.getInteger(token) >= 0)
tree.add(new NumericExpression(token));
else
tree.add(new OperationExpression(token));
}

// Interpretamos cada expresión
for(Expression e : tree)
e.interpret(context);

// Mostramos el resultado
System.out.println("El resultado de la interpretación es " + context.getResult());
}
}

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP