Marketing quiere lanzar promociones sin esperar al siguiente despliegue: "si el total supera 30 € y es viernes, 10% de descuento"; "si es el primer pedido o el total supera 50 €, envío gratis". Cada regla nueva hoy es una clase Promocion nueva escrita por un programador. La interfaz Promocion del módulo 1 nos dio OCP —añadir sin modificar—, pero marketing quiere más: escribir las reglas como texto y que el sistema las entienda. Eso exige definir un mini-lenguaje: una gramática, árboles de expresiones y un intérprete que las evalúe. Ese es Interpreter — y esta lección incluye una dosis deliberada de honestidad, porque es el patrón GoF que menos vas a usar, y conviene saber exactamente por qué.
Contenido
- El problema en PideYa: reglas de promoción como texto
- La gramática del mini-lenguaje
- Intención y estructura del patrón
- Implementación Java completa
- ¿Y quién construye el árbol? El parser
- Cuándo usarlo y cuándo no (honestidad incluida)
- Relación con otros patrones
- Errores comunes
- Ejercicios y conclusión
El problema en PideYa: reglas de promoción como texto
Lo que marketing quiere escribir en el panel de administración:
total > 30 Y dia == VIERNES
primerPedido O total > 50
(total > 20 Y dia == LUNES) O codigoPostal == 28001Cada línea es una condición de aplicabilidad de una promoción. El descuento en sí (10%, envío gratis) es un dato aparte; lo difícil es evaluar la condición contra cada pedido. Las fuerzas:
- Las reglas combinan piezas fijas (total, día, primer pedido) con conectores (
Y,O, comparaciones, paréntesis): no es una lista cerrada de casos, es un lenguaje pequeño pero componible — hay infinitas reglas posibles. - Quien escribe las reglas no programa; quien programa no quiere desplegar por cada regla.
- Las reglas deben evaluarse contra datos de cada pedido en el momento del checkout.
Un if por regla no escala (vuelve el despliegue); un parser "a pelo" con split y expresiones regulares se derrumba con los paréntesis y la precedencia. La solución con estructura: tratar cada regla como una frase de un lenguaje, representarla como un árbol y darle a cada tipo de nodo la capacidad de interpretarse a sí mismo.
La gramática del mini-lenguaje
Todo lenguaje, por pequeño que sea, tiene una gramática. La nuestra, en notación informal (cada línea es una regla de producción; lo que puede "expandirse" más es no terminal, lo que ya no, terminal):
expresion ::= comparacion | expresion "Y" expresion | expresion "O" expresion | "(" expresion ")"
comparacion ::= variable operador literal
variable ::= "total" | "dia" | "primerPedido" | "codigoPostal"
operador ::= ">" | "<" | "=="
literal ::= número | día de la semana | booleanoY la frase total > 30 Y dia == VIERNES se convierte en este árbol de sintaxis abstracta (AST):
flowchart TD
Y["Y (no terminal)"] --> C1["> (comparación)"]
Y --> C2["== (comparación)"]
C1 --> V1["total (terminal)"]
C1 --> L1["30 (terminal)"]
C2 --> V2["dia (terminal)"]
C2 --> L2["VIERNES (terminal)"]
La idea central de Interpreter: una clase por regla de la gramática. Los nodos hoja (variables, literales) son expresiones terminales; los nodos con hijos (Y, O, comparaciones) son expresiones no terminales que se interpretan interpretando recursivamente a sus hijos. ¿Un árbol de piezas con operación recursiva uniforme? Sí: estructuralmente, Interpreter es un Composite cuya operación es interpretar.
Intención y estructura del patrón
Intención (GoF): dado un lenguaje, definir una representación de su gramática junto con un intérprete que usa esa representación para interpretar las frases del lenguaje.
classDiagram
class ExpresionRegla {
<<interface>>
+interpretar(ctx: ContextoPedido) boolean
}
class Comparacion {
-variable: String
-operador: String
-literal: String
+interpretar(ctx) boolean
}
class ExpresionY {
-izquierda: ExpresionRegla
-derecha: ExpresionRegla
+interpretar(ctx) boolean
}
class ExpresionO {
-izquierda: ExpresionRegla
-derecha: ExpresionRegla
+interpretar(ctx) boolean
}
class ContextoPedido {
+valorDe(variable: String) String
}
ExpresionRegla <|.. Comparacion
ExpresionRegla <|.. ExpresionY
ExpresionRegla <|.. ExpresionO
ExpresionY o-- ExpresionRegla : hijos
ExpresionO o-- ExpresionRegla : hijos
ExpresionRegla ..> ContextoPedido : lee
| Rol GoF | En PideYa |
|---|---|
| AbstractExpression | ExpresionRegla |
| TerminalExpression (hojas) | Comparacion (encapsula variable-operador-literal) |
| NonterminalExpression (nodos con hijos) | ExpresionY, ExpresionO |
| Context (datos externos que necesita la evaluación) | ContextoPedido |
| Client (construye/obtiene el AST y lo interpreta) | el motor de promociones |
Implementación Java completa
El contexto: la ventana por la que las expresiones ven el pedido. Aísla la gramática del modelo de dominio — si mañana Pedido cambia, solo cambia el contexto:
public class ContextoPedido {
private final Map<String, String> valores;
public ContextoPedido(Pedido pedido, Cliente cliente) {
this.valores = Map.of(
"total", pedido.getTotal().toPlainString(),
"dia", LocalDate.now().getDayOfWeek().name(), // "FRIDAY"...
"primerPedido", String.valueOf(cliente.getNumeroPedidos() == 0),
"codigoPostal", pedido.getDireccionEntrega().getCodigoPostal()
);
}
public String valorDe(String variable) {
String v = valores.get(variable);
if (v == null) {
throw new ReglaInvalidaException("Variable desconocida: " + variable);
}
return v;
}
}La interfaz y las expresiones no terminales. Mira qué pequeñas: cada una implementa una regla de la gramática delegando en sus hijos:
public interface ExpresionRegla {
boolean interpretar(ContextoPedido ctx);
}
public class ExpresionY implements ExpresionRegla {
private final ExpresionRegla izquierda, derecha;
public ExpresionY(ExpresionRegla izquierda, ExpresionRegla derecha) {
this.izquierda = izquierda;
this.derecha = derecha;
}
@Override
public boolean interpretar(ContextoPedido ctx) {
return izquierda.interpretar(ctx) && derecha.interpretar(ctx);
}
}
public class ExpresionO implements ExpresionRegla {
private final ExpresionRegla izquierda, derecha;
public ExpresionO(ExpresionRegla izquierda, ExpresionRegla derecha) {
this.izquierda = izquierda;
this.derecha = derecha;
}
@Override
public boolean interpretar(ContextoPedido ctx) {
return izquierda.interpretar(ctx) || derecha.interpretar(ctx);
}
}La expresión terminal hace el trabajo sucio de comparar contra el contexto:
public class Comparacion implements ExpresionRegla {
private final String variable, operador, literal;
public Comparacion(String variable, String operador, String literal) {
this.variable = variable;
this.operador = operador;
this.literal = literal;
}
@Override
public boolean interpretar(ContextoPedido ctx) {
String valor = ctx.valorDe(variable);
return switch (operador) {
case "==" -> valor.equalsIgnoreCase(literal);
case ">" -> new BigDecimal(valor).compareTo(new BigDecimal(literal)) > 0;
case "<" -> new BigDecimal(valor).compareTo(new BigDecimal(literal)) < 0;
default -> throw new ReglaInvalidaException("Operador desconocido: " + operador);
};
}
}Montar y evaluar la regla total > 30 Y dia == VIERNES a mano:
ExpresionRegla regla = new ExpresionY(
new Comparacion("total", ">", "30"),
new Comparacion("dia", "==", "FRIDAY")
);
boolean aplica = regla.interpretar(new ContextoPedido(pedido, cliente));Y así, la regla encaja limpiamente en la interfaz Promocion de siempre: una PromocionConRegla que guarda su ExpresionRegla y su descuento, y en aplicaA(pedido) interpreta. El motor de promociones no sabe que dentro hay un lenguaje: solo ve Promocion. OCP intacto — y ahora, sin desplegar.
¿Y quién construye el árbol? El parser
Aquí está la letra pequeña del patrón: Interpreter no dice cómo pasar del texto al árbol. El GoF lo declara explícitamente fuera de alcance. Convertir "(total > 20 Y dia == LUNES) O codigoPostal == 28001" en objetos exige un parser: trocear el texto (análisis léxico), respetar precedencias y paréntesis (análisis sintáctico) y construir el AST. Para nuestra gramática diminuta, un parser recursivo descendente son ~60 líneas asumibles (el ejercicio 2 te hace escribir una versión mínima). Pero es la parte que peor envejece: cada extensión de la gramática toca el parser y añade clases.
Para gramáticas de verdad, no reinventes: generadores de parsers como ANTLR o JavaCC generan el analizador a partir de la gramática declarada, y lenguajes embebidos ya hechos (SpEL de Spring, MVEL, JEXL, incluso JavaScript embebido con GraalVM) te dan variables, operadores, funciones y seguridad ya resueltos. La frontera práctica: si tu gramática cabe en cinco reglas de producción y va a crecer poco, Interpreter casero es razonable; si no, estás construyendo un compilador por accidente.
Cuándo usarlo y cuándo no (honestidad incluida)
Úsalo cuando:
- Existe un lenguaje pequeño, estable y componible que usuarios o configuración necesitan escribir: reglas de negocio, filtros de búsqueda, expresiones de permisos.
- La gramática es simple (pocas reglas de producción) y la eficiencia no es crítica.
- Las frases se representan bien como árboles y se evalúan recursivamente.
Evítalo cuando:
- La gramática es o será compleja: la jerarquía de clases crece con cada regla de producción, y el mantenimiento (clases + parser) se dispara. Usa ANTLR/JavaCC o un lenguaje embebido existente.
- Lo que necesitas es una lista cerrada de opciones, no un lenguaje componible: eso es Strategy o simple configuración.
- El rendimiento importa mucho: interpretar árboles de objetos es lento comparado con código compilado.
La dosis de honestidad prometida. Interpreter es, con diferencia, el patrón GoF que menos aplicarás a mano, por tres razones acumuladas: (1) pocos dominios justifican inventar un lenguaje propio; (2) cuando lo justifican, las herramientas modernas (parser generators, motores de reglas, lenguajes embebidos) hacen el trabajo mejor que una jerarquía casera; (3) su nicho original de los 90 —mini-lenguajes ad hoc— hoy lo cubren JSON/YAML para configuración y expresiones estándar para reglas. Entonces, ¿por qué estudiarlo? Porque su modelo mental —gramática → AST → evaluación recursiva— es exactamente cómo funcionan las expresiones regulares que usas a diario, el parseo de SQL, los motores de plantillas y los propios compiladores. Entender Interpreter es entender qué hay dentro de esas cajas negras; escribirlo desde cero, casi nunca es la decisión correcta en producción.
Relación con otros patrones
- Composite: el AST es un composite; Interpreter añade la operación de evaluación recursiva sobre esa estructura.
- Visitor: si además de evaluar quieres imprimir, optimizar o validar el árbol, en vez de engordar cada clase de expresión con más métodos, un visitante añade operaciones sin tocarlas — combinación clásica sobre ASTs.
- Iterator: recorrer el árbol de expresiones sin exponer su estructura.
- Flyweight: los símbolos terminales repetidos (la variable
totalen mil reglas) pueden compartirse. - Strategy: la alternativa cuando no hay lenguaje, solo variantes.
Errores comunes
- Inventar un lenguaje cuando bastaba una lista de opciones: si marketing solo necesita elegir entre cinco condiciones predefinidas, un desplegable y un
Mapresuelven; el lenguaje se justifica cuando la combinatoria es el requisito. - Dejar crecer la gramática "un poquito más" cada mes: funciones, aritmética, cadenas... A la tercera ampliación tienes un lenguaje de programación sin querer. Pon frontera por escrito desde el día uno.
- Evaluar sin validar: reglas escritas por humanos llegan mal escritas. El parser debe dar errores comprensibles ("paréntesis sin cerrar en la posición 24"), no
NullPointerExceptionen el checkout. - Olvidar la seguridad: si algún día embebes un lenguaje real (JS, expresiones con reflexión como SpEL), estás ejecutando código de usuarios — sandbox y lista blanca de variables obligatorias.
- Acoplar las expresiones al dominio: si
ComparacionrecibePedidodirectamente, cada cambio del modelo rompe la gramática. Para eso está elContextoPedido.
Ejercicios
Ejercicio 1: negación
Marketing pide reglas como NO primerPedido Y total > 15. Añade a la gramática la producción expresion ::= "NO" expresion e implementa la clase correspondiente. ¿Es terminal o no terminal? ¿Cuántas clases existentes has tocado?
Ejercicio 2: parser mínimo
Escribe ParserReglas.parsear(String) para el subconjunto sin paréntesis ni O: comparaciones unidas por Y (p. ej. total > 30 Y dia == FRIDAY Y primerPedido == true). Pista: separa por " Y ", parsea cada comparación por espacios, y pliega la lista con ExpresionY.
Ejercicio 3: criterio
Para cada caso, decide: ¿Interpreter casero, herramienta existente, o ni lenguaje siquiera? (a) los repartidores filtran pedidos por "zona == CENTRO Y total > 20"; (b) finanzas quiere fórmulas tipo hoja de cálculo con funciones, fechas y aritmética para calcular comisiones; (c) operaciones quiere activar/desactivar el control antifraude por país.
Soluciones
Solución 1: no terminal (tiene una subexpresión hija). Clases tocadas: cero — solo se añade una (OCP en la gramática, herencia del diseño Composite):
public class ExpresionNo implements ExpresionRegla {
private final ExpresionRegla interior;
public ExpresionNo(ExpresionRegla interior) { this.interior = interior; }
@Override
public boolean interpretar(ContextoPedido ctx) {
return !interior.interpretar(ctx);
}
}(El parser sí habría que tocarlo — exactamente el coste dual que señala la lección.)
Solución 2:
public class ParserReglas {
public static ExpresionRegla parsear(String texto) {
String[] partes = texto.split(" Y ");
ExpresionRegla resultado = parsearComparacion(partes[0]);
for (int i = 1; i < partes.length; i++) {
resultado = new ExpresionY(resultado, parsearComparacion(partes[i]));
}
return resultado;
}
private static ExpresionRegla parsearComparacion(String texto) {
String[] t = texto.trim().split("\\s+"); // ["total", ">", "30"]
if (t.length != 3) {
throw new ReglaInvalidaException("Comparación mal formada: '" + texto + "'");
}
return new Comparacion(t[0], t[1], t[2]);
}
}Nota cómo incluso este juguete ya valida y da mensajes útiles. Añadir O con menor precedencia que Y y paréntesis exigiría un recursivo descendente con un nivel por precedencia — el salto de complejidad que te empuja hacia ANTLR.
Solución 3: (a) Interpreter casero razonable: gramática diminuta, componible, estable — es nuestro caso de la lección con otras variables; (b) herramienta existente (motor de expresiones tipo SpEL/JEXL o incluso una librería de hojas de cálculo): funciones + aritmética + fechas es una gramática grande, la mantendrías para siempre; (c) ni lenguaje: es un booleano por país — configuración pura en ConfiguracionPideYa.
Conclusión
Interpreter cierra el trío de patrones que reifican peticiones: la cadena las encaminaba, Command las congelaba, e Interpreter va más allá y reifica frases enteras de un lenguaje: una clase por regla de la gramática, árboles de expresiones que se evalúan recursivamente contra un contexto, y marketing escribiendo promociones sin desplegar. También te llevas su letra pequeña: el parser no viene incluido, la jerarquía crece con la gramática, y en producción casi siempre ganan ANTLR o un lenguaje embebido — pero el modelo gramática → AST → evaluación es de las ideas más rentables que te llevas del catálogo, porque está dentro de cada regex, cada SQL y cada compilador que usas.
Nuestro AST era un árbol que sabíamos recorrer porque lo habíamos construido nosotros. Pero ¿y las estructuras que no queremos que nadie conozca por dentro? La carta Composite de La Bella Napoli tiene secciones dentro de secciones, y hoy cada pantalla que la recorre reimplementa la recursión por su cuenta. Recorrer sin exponer: ese es el siguiente patrón, el más humilde y el más omnipresente de todos — lo usas cada vez que escribes un for-each. Nos vemos en Iterator.
Curso de Patrones de Diseño de Software
Módulo 1: Introducción a los Patrones de Diseño
- ¿Qué son los Patrones de Diseño?
- Historia y Origen de los Patrones de Diseño
- Principios de Diseño: SOLID y Otros Fundamentos
- UML Esencial para Entender Patrones
- Clasificación de los Patrones de Diseño
- Ventajas y Desventajas de Usar Patrones de Diseño
Módulo 2: Patrones Creacionales
- Introducción a los Patrones Creacionales
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa y Elección de Patrones Creacionales
Módulo 3: Patrones Estructurales
- Introducción a los Patrones Estructurales
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa y Elección de Patrones Estructurales
Módulo 4: Patrones de Comportamiento
- Introducción a los Patrones de Comportamiento
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa y Elección de Patrones de Comportamiento
Módulo 5: Aplicación de Patrones de Diseño
- Cómo Seleccionar el Patrón Adecuado
- Ejemplos Prácticos de Uso de Patrones
- Patrones de Diseño en Proyectos Reales
- Refactorización Usando Patrones de Diseño
- Antipatrones: Cuándo los Patrones se Vuelven un Problema
Módulo 6: Patrones de Diseño Avanzados
- Patrones de Diseño en Arquitecturas Modernas
- Patrones de Diseño en Microservicios
- Patrones de Diseño en Sistemas Distribuidos
- Patrones de Concurrencia
- Patrones de Diseño en Desarrollo Ágil
