Patrón de diseño de intérprete en Java

1. Información general

En este tutorial, presentaremos uno de los patrones de diseño de comportamiento de GoF: el intérprete.

Al principio, daremos una descripción general de su propósito y explicaremos el problema que intenta resolver.

Luego, veremos el diagrama UML de Interpreter y la implementación del ejemplo práctico.

2. Patrón de diseño del intérprete

En resumen, el patrón define la gramática de un idioma en particular de una manera orientada a objetos que puede ser evaluada por el propio intérprete.

Teniendo esto en cuenta, técnicamente podríamos construir nuestra expresión regular personalizada, un intérprete DSL personalizado o podríamos analizar cualquiera de los lenguajes humanos, construir árboles de sintaxis abstracta y luego ejecutar la interpretación.

Estos son solo algunos de los casos de uso potenciales, pero si pensamos por un tiempo, podríamos encontrar aún más usos, por ejemplo en nuestros IDE, ya que están interpretando continuamente el código que estamos escribiendo y, por lo tanto, nos proporcionan consejos invaluables.

El patrón de intérprete generalmente debe usarse cuando la gramática es relativamente simple.

De lo contrario, podría resultar difícil de mantener.

3. Diagrama UML

El diagrama anterior muestra dos entidades principales: el contexto y la expresión .

Ahora, cualquier idioma debe expresarse de alguna manera, y las palabras (expresiones) tendrán algún significado según el contexto dado.

AbstractExpression define un método abstracto que toma el contextocomo parámetro. Gracias a eso, cada expresión afectará el contexto , cambiará su estado y continuará la interpretación o devolverá el resultado en sí.

Por lo tanto, el contexto será el titular del estado global de procesamiento y se reutilizará durante todo el proceso de interpretación.

Entonces, ¿cuál es la diferencia entre TerminalExpression y NonTerminalExpression ?

Una NonTerminalExpression puede tener una o más otras AbstractExpressions asociadas, por lo tanto, se puede interpretar de forma recursiva. Al final, el proceso de interpretación tiene que terminar con una TerminalExpression que devolverá el resultado.

Vale la pena señalar que NonTerminalExpression es un compuesto.

Finalmente, el rol del cliente es crear o usar un árbol de sintaxis abstracta ya creado , que no es más que una oración definida en el lenguaje creado.

4. Implementación

Para mostrar el patrón en acción, construiremos una sintaxis simple similar a SQL de una manera orientada a objetos, que luego será interpretada y nos devolverá el resultado.

Primero, definiremos las expresiones Seleccionar, Desde y Dónde , construiremos un árbol de sintaxis en la clase del cliente y ejecutaremos la interpretación.

La interfaz Expression tendrá el método interpret:

List interpret(Context ctx);

A continuación, definimos la primera expresión, la clase Select :

class Select implements Expression { private String column; private From from; // constructor @Override public List interpret(Context ctx) { ctx.setColumn(column); return from.interpret(ctx); } }

Obtiene el nombre de la columna a seleccionar y otra Expresión concreta de tipo From como parámetros en el constructor.

Tenga en cuenta que en el método interpret () anulado establece el estado del contexto y pasa la interpretación a otra expresión junto con el contexto.

De esa forma, vemos que es una NonTerminalExpression.

Otra expresión es la clase From :

class From implements Expression { private String table; private Where where; // constructors @Override public List interpret(Context ctx) { ctx.setTable(table); if (where == null) { return ctx.search(); } return where.interpret(ctx); } }

Ahora, en SQL, la cláusula where es opcional, por lo tanto, esta clase es una expresión terminal o no terminal.

Si el usuario decide no utilizar una cláusula where, la expresión From terminará con la llamada ctx.search () y devolverá el resultado. De lo contrario, se seguirá interpretando.

La expresión Where está modificando nuevamente el contexto estableciendo el filtro necesario y termina la interpretación con la llamada de búsqueda:

class Where implements Expression { private Predicate filter; // constructor @Override public List interpret(Context ctx) { ctx.setFilter(filter); return ctx.search(); } }

Por ejemplo, la clase Context contiene los datos que imitan la tabla de la base de datos.

Tenga en cuenta que tiene tres campos clave que son modificados por cada subclase de Expresión y el método de búsqueda:

class Context { private static Map
    
      tables = new HashMap(); static { List list = new ArrayList(); list.add(new Row("John", "Doe")); list.add(new Row("Jan", "Kowalski")); list.add(new Row("Dominic", "Doom")); tables.put("people", list); } private String table; private String column; private Predicate whereFilter; // ... List search() { List result = tables.entrySet() .stream() .filter(entry -> entry.getKey().equalsIgnoreCase(table)) .flatMap(entry -> Stream.of(entry.getValue())) .flatMap(Collection::stream) .map(Row::toString) .flatMap(columnMapper) .filter(whereFilter) .collect(Collectors.toList()); clear(); return result; } }
    

Una vez realizada la búsqueda, el contexto se borra solo, por lo que la columna, la tabla y el filtro se establecen en los valores predeterminados.

De esa manera, cada interpretación no afectará a la otra.

5. Prueba

Para propósitos de prueba, echemos un vistazo a la clase InterpreterDemo :

public class InterpreterDemo { public static void main(String[] args) { Expression query = new Select("name", new From("people")); Context ctx = new Context(); List result = query.interpret(ctx); System.out.println(result); Expression query2 = new Select("*", new From("people")); List result2 = query2.interpret(ctx); System.out.println(result2); Expression query3 = new Select("name", new From("people", new Where(name -> name.toLowerCase().startsWith("d")))); List result3 = query3.interpret(ctx); System.out.println(result3); } }

Primero, construimos un árbol de sintaxis con expresiones creadas, inicializamos el contexto y luego ejecutamos la interpretación. El contexto se reutiliza, pero como mostramos anteriormente, se limpia solo después de cada llamada de búsqueda.

Al ejecutar el programa, la salida debería ser la siguiente:

[John, Jan, Dominic] [John Doe, Jan Kowalski, Dominic Doom] [Dominic]

6. Desventajas

Cuando la gramática se vuelve más compleja, se vuelve más difícil de mantener.

Puede verse en el ejemplo presentado. Sería razonablemente fácil agregar otra expresión, como Límite , pero no será demasiado fácil de mantener si decidiéramos seguir extendiéndola con todas las demás expresiones.

7. Conclusión

El patrón de diseño del intérprete es ideal para una interpretación gramatical relativamente simple , que no necesita evolucionar ni extenderse mucho.

En el ejemplo anterior, mostramos que es posible construir una consulta similar a SQL de una manera orientada a objetos con la ayuda del patrón de intérprete.

Finalmente, puede encontrar este uso de patrón en JDK, particularmente, en java.util.Pattern , java.text.Format o java.text.Normalizer .

Como de costumbre, el código completo está disponible en el proyecto Github.