28. Interpreter (Intérprete) - Patrón de Comportamiento

El patrón Interpreter proporciona una forma de definir gramáticas para lenguajes especializados y evaluarlas creando una jerarquía de clases que representan reglas gramaticales. Cada expresión de la gramática sabe cómo interpretarse a sí misma dentro de un contexto.

Es especialmente útil cuando una aplicación necesita integrar un mini-lenguaje o reglas de negocio declarativas: filtros de consultas, expresiones matemáticas, lenguajes de autorización o scripts de configuración.

28.1 Problema y contexto

Muchas soluciones requieren que usuarios avanzados definan lógica mediante expresiones flexibles. Implementar esa lógica con condicionales dispersos genera código poco mantenible. Un lenguaje ad hoc brinda flexibilidad, pero su interpretación requiere una estructura formal que traduzca expresiones a acciones.

Interpreter define la gramática como un conjunto de clases y permite evaluarla sistemáticamente. Cada regla del lenguaje se modela como un objeto que participa en la interpretación.

28.2 Intención y motivación

La intención del patrón es proporcionar una representación para la gramática de un lenguaje y un intérprete que use esa representación para evaluar frases del lenguaje. Aplica cuando:

  • La gramática es relativamente sencilla y estable.
  • Se necesita agregar, combinar o reutilizar reglas sintácticas de forma modular.
  • Los usuarios o administradores construyen expresiones que se evaluan en tiempo de ejecución.
  • Se busca aislar la interpretación del resto de la aplicación.

Interpreter fomenta que el lenguaje evolucione con clases reutilizables que mantienen la semántica encapsulada.

28.3 Participantes y estructura

Los componentes principales son:

  • AbstractExpression: declara la interfaz interpret(Contexto).
  • TerminalExpression: representa elementos indivisibles de la gramática (variables, literales).
  • NonterminalExpression: combina otras expresiones según reglas (por ejemplo, operadores lógicos).
  • Context: almacena información global necesaria para la interpretación (variables, servicios).
  • Client: construye la expresión abstracta correspondiente a la frase y desencadena la interpretación.

La estructura suele expresar el lenguaje mediante un árbol de sintaxis abstracta donde cada nodo conoce cómo evaluarse.

28.4 Alcances y limitaciones

Interpreter es provechoso para gramáticas acotadas. A medida que el lenguaje gana complejidad pueden crecer exponencialmente las clases necesarias, volviendo recomendable generar intérpretes mediante herramientas como ANTLR o combinarlos con analizadores más sofisticados.

Para mini-lenguajes de configuración, filtros o reglas declarativas con combinaciones limitadas resulta una alternativa simple y flexible.

28.5 Escenario: reglas de segmentación de clientes

Un departamento de marketing define segmentos mediante expresiones como categoria:PREMIUM AND compras>50 OR ultimaVisita<30d. Necesitan probar nuevas combinaciones con rapidez sin modificar el sistema principal.

Interpreter permite crear un conjunto de clases que representan operadores lógicos y comparaciones, evaluando la expresión contra el perfil de cada cliente para activar campañas personalizadas.

28.6 Implementación en Java

El siguiente ejemplo interpreta expresiones lógicas sencillas sobre un contexto de cliente:

package tutorial.interpreter;

import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;

public interface Expresion {
    boolean interpretar(ContextoCliente contexto);
}

class ContextoCliente {
    private final Map<String, Object> atributos;

    ContextoCliente(Map<String, Object> atributos) {
        this.atributos = atributos;
    }

    @SuppressWarnings("unchecked")
    public <T> T obtener(String clave, Class<T> tipo) {
        Object valor = atributos.get(clave);
        if (valor == null) {
            return null;
        }
        if (!tipo.isInstance(valor)) {
            throw new IllegalArgumentException("Tipo inesperado para " + clave);
        }
        return (T) valor;
    }
}

class ExpresionTerminalIgual implements Expresion {
    private final String atributo;
    private final String valorEsperado;

    ExpresionTerminalIgual(String atributo, String valorEsperado) {
        this.atributo = Objects.requireNonNull(atributo);
        this.valorEsperado = Objects.requireNonNull(valorEsperado);
    }

    @Override
    public boolean interpretar(ContextoCliente contexto) {
        String valor = contexto.obtener(atributo, String.class);
        return valorEsperado.equalsIgnoreCase(valor);
    }
}

class ExpresionMayorQue implements Expresion {
    private final String atributo;
    private final int limite;

    ExpresionMayorQue(String atributo, int limite) {
        this.atributo = atributo;
        this.limite = limite;
    }

    @Override
    public boolean interpretar(ContextoCliente contexto) {
        Integer valor = contexto.obtener(atributo, Integer.class);
        return valor != null && valor > limite;
    }
}

class ExpresionUltimaVisitaMenorQue implements Expresion {
    private final Duration umbral;

    ExpresionUltimaVisitaMenorQue(Duration umbral) {
        this.umbral = umbral;
    }

    @Override
    public boolean interpretar(ContextoCliente contexto) {
        Instant ultimaVisita = contexto.obtener("ultimaVisita", Instant.class);
        if (ultimaVisita == null) {
            return false;
        }
        Duration transcurrido = Duration.between(ultimaVisita, Instant.now());
        return transcurrido.compareTo(umbral) <= 0;
    }
}

class ExpresionOr implements Expresion {
    private final Expresion izquierda;
    private final Expresion derecha;

    ExpresionOr(Expresion izquierda, Expresion derecha) {
        this.izquierda = izquierda;
        this.derecha = derecha;
    }

    @Override
    public boolean interpretar(ContextoCliente contexto) {
        return izquierda.interpretar(contexto) || derecha.interpretar(contexto);
    }
}

class ExpresionAnd implements Expresion {
    private final Expresion izquierda;
    private final Expresion derecha;

    ExpresionAnd(Expresion izquierda, Expresion derecha) {
        this.izquierda = izquierda;
        this.derecha = derecha;
    }

    @Override
    public boolean interpretar(ContextoCliente contexto) {
        return izquierda.interpretar(contexto) && derecha.interpretar(contexto);
    }
}

class ExpresionNot implements Expresion {
    private final Expresion expresion;

    ExpresionNot(Expresion expresion) {
        this.expresion = expresion;
    }

    @Override
    public boolean interpretar(ContextoCliente contexto) {
        return !expresion.interpretar(contexto);
    }
}

class ConstructorExpresiones {
    public Expresion categoriaEs(String categoria) {
        return new ExpresionTerminalIgual("categoria", categoria);
    }

    public Expresion comprasMayoresA(int minimo) {
        return new ExpresionMayorQue("compras", minimo);
    }

    public Expresion ultimaVisitaDentroDe(Duration duracion) {
        return new ExpresionUltimaVisitaMenorQue(duracion);
    }

    public Expresion y(Expresion izquierda, Expresion derecha) {
        return new ExpresionAnd(izquierda, derecha);
    }

    public Expresion o(Expresion izquierda, Expresion derecha) {
        return new ExpresionOr(izquierda, derecha);
    }

    public Expresion no(Expresion expresion) {
        return new ExpresionNot(expresion);
    }
}

class AplicacionInterpreter {
    public static void main(String[] args) {
        ConstructorExpresiones builder = new ConstructorExpresiones();
        Expresion segmento =
                builder.o(
                        builder.y(
                                builder.categoriaEs("PREMIUM"),
                                builder.comprasMayoresA(50)
                        ),
                        builder.ultimaVisitaDentroDe(Duration.ofDays(30))
                );

        ContextoCliente clienteA = new ContextoCliente(Map.of(
                "categoria", "PREMIUM",
                "compras", 80,
                "ultimaVisita", Instant.now().minus(Duration.ofDays(60))
        ));
        ContextoCliente clienteB = new ContextoCliente(Map.of(
                "categoria", "REGULAR",
                "compras", 10,
                "ultimaVisita", Instant.now().minus(Duration.ofDays(5))
        ));

        System.out.println("Cliente A aplica segmento? " + segmento.interpretar(clienteA));
        System.out.println("Cliente B aplica segmento? " + segmento.interpretar(clienteB));
    }
}

28.7 Explicación del flujo

Las expresiones terminales evalúan condiciones básicas como igualdad o comparaciones. Las expresiones no terminales combinan resultados mediante operadores lógicos. El cliente construye la expresión una sola vez y la reutiliza para evaluar distintos contextos. El constructor de expresiones simplifica la creación de árboles legibles.

Este diseño permite agregar operadores adicionales (por ejemplo, ExpresionMenorQue) sin romper el código existente.

28.8 Interpreter en el ecosistema Java

Las API de expresiones de la plataforma adoptan ideas de este patrón. La documentación de MessageFormat describe cómo se interpretan plantillas con placeholders. Asimismo, bibliotecas de expresiones como Jakarta Expression Language o el motor de scripts de Nashorn (hasta Java 14) aplican principios similares para evaluar lenguajes personalizados.

28.9 Variantes y extensiones

Interpreter se puede adaptar de varias maneras:

  • Analizador híbrido: combina Interpreter con un parser generado (ANTLR, JavaCC) para construir el árbol de expresiones a partir de una cadena.
  • Expresiones cacheadas: almacena árboles interpretados para reutilizarlos en evaluaciones posteriores.
  • Interpretación diferida: en lugar de evaluar inmediatamente, se construyen expresiones que luego se traducen a consultas SQL o mensajes de otro sistema.

28.10 Riesgos y malas prácticas

El patrón puede generar muchas clases pequeñas que dificultan la navegación si la gramática crece. También es importante cuidar el rendimiento: evaluaciones muy profundas o recursivas pueden consumir memoria y CPU.

Otro error común es mezclar lógica de negocio dentro de expresiones terminales. Lo ideal es que se limiten a leer datos del contexto y deleguen cálculos complejos a servicios externos.

28.11 Buenas prácticas para aplicar Interpreter

  • Documentar la gramática y proporcionar ejemplos claros para los usuarios del lenguaje.
  • Implementar validaciones estáticas para detectar expresiones inválidas antes de interpretarlas.
  • Combinar con patrones como Flyweight para compartir instancias de expresiones repetidas.
  • Agregar monitoreo o logging para auditar qué expresiones interpretan los usuarios y optimizar su rendimiento.

28.12 Relación con otros patrones

Interpreter se integra con Composite para estructurar el árbol de expresiones, con Iterator para recorrer nodos y con Visitor para aplicar operaciones adicionales sobre el árbol (por ejemplo, imprimirlo u optimizarlo). También puede usar Command para encapsular expresiones como comandos reutilizables.