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

  1. El problema en PideYa: reglas de promoción como texto
  2. La gramática del mini-lenguaje
  3. Intención y estructura del patrón
  4. Implementación Java completa
  5. ¿Y quién construye el árbol? El parser
  6. Cuándo usarlo y cuándo no (honestidad incluida)
  7. Relación con otros patrones
  8. Errores comunes
  9. 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 == 28001

Cada 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 | booleano

Y la frase total > 30 Y dia == VIERNES se convierte en este árbol de sintaxis abstracta (AST):

flowchart TD
    Y["Y (no terminal)"] --> C1["&gt; (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 total en 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 Map resuelven; 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 NullPointerException en 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 Comparacion recibe Pedido directamente, cada cambio del modelo rompe la gramática. Para eso está el ContextoPedido.

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

Módulo 2: Patrones Creacionales

Módulo 3: Patrones Estructurales

Módulo 4: Patrones de Comportamiento

Módulo 5: Aplicación de Patrones de Diseño

Módulo 6: Patrones de Diseño Avanzados

Módulo 7: Recursos Adicionales y Conclusión

© Copyright 2026. Todos los derechos reservados