Patrón Interpreter
Define una representación para la gramática de un lenguaje junto con un intérprete que usa la representación para interpretar oraciones. Un patrón de diseño de comportamiento para mini-lenguajes.
Resumen
El Patrón Interpreter es un patrón de diseño de comportamiento que define una representación para la gramática de un lenguaje junto con un intérprete que usa la representación para interpretar oraciones en el lenguaje. Es ideal para construir lenguajes específicos de dominio (DSL), evaluadores de expresiones, analizadores de consultas y motores de reglas.
Cuándo usarlo
Usa el Patrón Interpreter cuando:
- Tengas una gramática simple que necesita ser analizada y evaluada frecuentemente
- Quieras construir un lenguaje específico de dominio (DSL) para configuración o reglas
- Las oraciones del lenguaje puedan representarse como árboles de sintaxis abstracta (AST)
- La simplicidad de la gramática hace que un generador de parsers (ANTLR, yacc) sea excesivo
- Ejemplos: expresiones aritméticas, consultas tipo SQL, motores de reglas booleanas, motores regex
Solución
Python
from abc import ABC, abstractmethod
from typing import Any
class Expression(ABC):
@abstractmethod
def interpret(self, context: dict) -> Any:
pass
class Number(Expression):
def __init__(self, value: float):
self.value = value
def interpret(self, context: dict) -> float:
return self.value
class Variable(Expression):
def __init__(self, name: str):
self.name = name
def interpret(self, context: dict) -> Any:
return context.get(self.name, 0)
class Add(Expression):
def __init__(self, left: Expression, right: Expression):
self.left = left
self.right = right
def interpret(self, context: dict) -> float:
return self.left.interpret(context) + self.right.interpret(context)
class Subtract(Expression):
def __init__(self, left: Expression, right: Expression):
self.left = left
self.right = right
def interpret(self, context: dict) -> float:
return self.left.interpret(context) - self.right.interpret(context)
# Uso: evaluar "(10 + x) - 5" donde x = 3
expression = Subtract(
Add(Number(10), Variable("x")),
Number(5)
)
context = {"x": 3}
print(expression.interpret(context)) # 8
JavaScript
class Expression {
interpret(context) {
throw new Error("Las subclases deben implementar interpret()");
}
}
class NumberLiteral extends Expression {
constructor(value) {
super();
this.value = value;
}
interpret(context) {
return this.value;
}
}
class Variable extends Expression {
constructor(name) {
super();
this.name = name;
}
interpret(context) {
return context[this.name] ?? 0;
}
}
class Add extends Expression {
constructor(left, right) {
super();
this.left = left;
this.right = right;
}
interpret(context) {
return this.left.interpret(context) + this.right.interpret(context);
}
}
class Subtract extends Expression {
constructor(left, right) {
super();
this.left = left;
this.right = right;
}
interpret(context) {
return this.left.interpret(context) - this.right.interpret(context);
}
}
// Uso: evaluar "(10 + x) - 5" donde x = 3
const expression = new Subtract(
new Add(new NumberLiteral(10), new Variable("x")),
new NumberLiteral(5)
);
console.log(expression.interpret({ x: 3 })); // 8
Java
import java.util.Map;
public interface Expression {
double interpret(Map<String, Double> context);
}
public class NumberLiteral implements Expression {
private final double value;
public NumberLiteral(double value) {
this.value = value;
}
public double interpret(Map<String, Double> context) {
return value;
}
}
public class Variable implements Expression {
private final String name;
public Variable(String name) {
this.name = name;
}
public double interpret(Map<String, Double> context) {
return context.getOrDefault(name, 0.0);
}
}
public class Add implements Expression {
private final Expression left, right;
public Add(Expression left, Expression right) {
this.left = left;
this.right = right;
}
public double interpret(Map<String, Double> context) {
return left.interpret(context) + right.interpret(context);
}
}
public class Subtract implements Expression {
private final Expression left, right;
public Subtract(Expression left, Expression right) {
this.left = left;
this.right = right;
}
public double interpret(Map<String, Double> context) {
return left.interpret(context) - right.interpret(context);
}
}
// Uso
Expression expr = new Subtract(
new Add(new NumberLiteral(10), new Variable("x")),
new NumberLiteral(5)
);
System.out.println(expr.interpret(Map.of("x", 3.0))); // 8.0
Explicación
El Patrón Interpreter se construye alrededor de un árbol de sintaxis abstracta (AST):
- Expresión Abstracta (
Expression): Declara la interfazinterpret() - Expresión Terminal (
NumberLiteral,Variable): Representa nodos hoja en el AST - Expresión No Terminal (
Add,Subtract): Representa nodos compuestos que combinan otras expresiones - Contexto (
dict/Map): Mantiene estado global (variables) disponible durante la interpretación
El cliente construye un AST y luego llama interpret() en el nodo raíz, que evalúa recursivamente el árbol.
Variantes
| Variante | Descripción | Caso de uso |
|---|---|---|
| Tree Walker | Recorre un AST pre-construido | Motores de reglas, evaluadores de consultas |
| Visitor + Interpreter | Separa la lógica de evaluación de los nodos del árbol | Cuando las estrategias de evaluación varían |
| Interpreter Memoizado | Cachea resultados de sub-expresiones | Evaluaciones repetidas con el mismo contexto |
| Compiler + VM | Compila a bytecode, luego ejecuta | Ejecución repetida de alto rendimiento |
Lo que funciona
- Mantén la gramática simple — las gramáticas complejas se manejan mejor con generadores de parsers
- Construye el AST primero, luego interpreta — no interpretes durante el parsing
- Usa nodos de expresión inmutables para seguridad de hilos y reproducibilidad
- Documenta la precedencia de operadores claramente si soportas múltiples operadores
- Considera agregar
toString()para depuración y serialización de expresiones
Errores comunes
- Usar Interpreter para lenguajes complejos donde un generador de parsers (ANTLR, PEG.js) es más apropiado
- Mezclar la lógica de parsing con la lógica de interpretación, creando código spaghetti
- No manejar errores de tipo o variables faltantes gracefulmente
- Construir el AST manualmente en lugar de usar un parser para gramáticas más grandes
- Ignorar la precedencia de operadores, llevando a orden de evaluación incorrecto
Temas Avanzados
Escenario: Interpreter para DSL de Configuracion
// Interpreter: evaluar expresiones de un DSL
// Gramatica: SET key value | GET key | DEL key | EXISTS key
interface Expression {
interpret(context: Map<string, string>): string;
}
// Terminal: SET
class SetExpression implements Expression {
constructor(private key: string, private value: string) {}
interpret(context: Map<string, string>): string {
context.set(this.key, this.value);
return `OK: ${this.key} = ${this.value}`;
}
}
// Terminal: GET
class GetExpression implements Expression {
constructor(private key: string) {}
interpret(context: Map<string, string>): string {
const value = context.get(this.key);
return value !== undefined ? value : `NIL: ${this.key}`;
}
}
// Terminal: DEL
class DelExpression implements Expression {
constructor(private key: string) {}
interpret(context: Map<string, string>): string {
const existed = context.delete(this.key);
return existed ? `OK: ${this.key} deleted` : `NIL: ${this.key}`;
}
}
// Non-terminal: secuencia de expresiones
class SequenceExpression implements Expression {
constructor(private expressions: Expression[]) {}
interpret(context: Map<string, string>): string {
return this.expressions.map(e => e.interpret(context)).join("\n");
}
}
// Parser: convertir texto a arbol de expresiones
class ConfigParser {
parse(input: string): Expression {
const lines = input.trim().split("\n");
const expressions: Expression[] = [];
for (const line of lines) {
const parts = line.split(" ");
const cmd = parts[0].toUpperCase();
if (cmd === "SET") expressions.push(new SetExpression(parts[1], parts.slice(2).join(" ")));
else if (cmd === "GET") expressions.push(new GetExpression(parts[1]));
else if (cmd === "DEL") expressions.push(new DelExpression(parts[1]));
else throw new Error(`Unknown command: ${cmd}`);
}
return new SequenceExpression(expressions);
}
}
// Uso
const parser = new ConfigParser();
const context = new Map<string, string>();
const program = parser.parse("SET name Alice\nSET role admin\nGET name\nDEL role");
console.log(program.interpret(context));
// OK: name = Alice
// OK: role = admin
// Alice
// OK: role deleted
Lecciones:
- Interpreter evalua expresiones de un DSL (Domain Specific Language)
- Cada expresion implementa interpret(): patron recursivo
- Terminal: SET, GET, DEL. Non-terminal: Sequence (compone)
- El parser convierte texto en arbol de expresiones
- Ideal para configs, queries, reglas de negocio simples
- Para DSLs complejos, usar parser generators (ANTLR, nearley)
### Interpreter vs Visitor: cual uso?
Interpreter evalua un arbol de expresiones: cada nodo sabe como interpretarse. Visitor recorre un arbol de objetos: el visitor sabe que hacer con cada nodo. Interpreter es para ejecutar un DSL. Visitor es para operaciones sobre una estructura. Usa Interpreter cuando necesitas un lenguaje custom (config, query, rules). Usa Visitor cuando necesitas anadir operaciones a una estructura sin modificar las clases. Preguntas frecuentes
¿Es este patrón adecuado para proyectos pequeños?
Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.
¿Cómo se compara este patrón con alternativas?
Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.
¿Puedo aplicar este patrón parcialmente?
Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.
Recursos Relacionados
Patrón Chain of Responsibility
Pasa solicitudes a lo largo de una cadena de manejadores hasta que uno la procese. Un patrón de comportamiento para desacoplar emisores y receptores.
PatternPatrón Strategy
Define una familia de algoritmos, encapsula cada uno y los hace intercambiables. Patrón de diseño conductual para selección flexible de comportamiento.
PatternPatrón Command
Encapsula una petición como un objeto, permitiendo parametrizar clientes con colas, logs y operaciones deshacibles. Patrón de diseño conductual.
PatternPatrón Memento
Captura y restaura el estado interno de un objeto sin violar el encapsulamiento. Un patrón de comportamiento para deshacer/rehacer.
PatternPatrón Template Method
Define el esqueleto de un algoritmo en una clase base, permitiendo que las subclases sobreescriban pasos específicos sin cambiar la estructura del algoritmo. Un patrón de diseño de comportamiento.
PatternPatrón Iterator
Proporciona una forma de acceder a los elementos de una colección secuencialmente sin exponer su representación subyacente. Un patrón de comportamiento para recorrido.