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.
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.
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:
Interpreter fomenta que el lenguaje evolucione con clases reutilizables que mantienen la semántica encapsulada.
Los componentes principales son:
interpret(Contexto)
.La estructura suele expresar el lenguaje mediante un árbol de sintaxis abstracta donde cada nodo conoce cómo evaluarse.
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.
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.
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));
}
}
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.
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.
Interpreter se puede adaptar de varias maneras:
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.
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.