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:
Interpretar(ContextoCliente)
.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 en C# interpreta expresiones lógicas sencillas sobre un contexto de cliente:
using System;
using System.Collections.Generic;
public interface IExpresion
{
bool Interpretar(ContextoCliente contexto);
}
public sealed class ContextoCliente
{
private readonly Dictionary<string, object> _atributos;
public ContextoCliente(IDictionary<string, object> atributos)
{
_atributos = new Dictionary<string, object>(atributos);
}
public bool TryObtener<T>(string clave, out T valor)
{
if (_atributos.TryGetValue(clave, out var bruto) && bruto is T convertido)
{
valor = convertido;
return true;
}
valor = default!;
return false;
}
}
public sealed class ExpresionTerminalIgual : IExpresion
{
private readonly string _atributo;
private readonly string _valorEsperado;
public ExpresionTerminalIgual(string atributo, string valorEsperado)
{
_atributo = atributo ?? throw new ArgumentNullException(nameof(atributo));
_valorEsperado = valorEsperado ?? throw new ArgumentNullException(nameof(valorEsperado));
}
public bool Interpretar(ContextoCliente contexto)
{
return contexto.TryObtener<string>(_atributo, out var valor) &&
string.Equals(valor, _valorEsperado, StringComparison.OrdinalIgnoreCase);
}
}
public sealed class ExpresionMayorQue : IExpresion
{
private readonly string _atributo;
private readonly int _limite;
public ExpresionMayorQue(string atributo, int limite)
{
_atributo = atributo;
_limite = limite;
}
public bool Interpretar(ContextoCliente contexto)
{
return contexto.TryObtener<int>(_atributo, out var valor) && valor > _limite;
}
}
public sealed class ExpresionUltimaVisitaMenorQue : IExpresion
{
private readonly TimeSpan _umbral;
public ExpresionUltimaVisitaMenorQue(TimeSpan umbral)
{
_umbral = umbral;
}
public bool Interpretar(ContextoCliente contexto)
{
if (!contexto.TryObtener<DateTime>("ultimaVisita", out var ultimaVisita))
{
return false;
}
var transcurrido = DateTime.UtcNow - ultimaVisita;
return transcurrido <= _umbral;
}
}
public sealed class ExpresionOr : IExpresion
{
private readonly IExpresion _izquierda;
private readonly IExpresion _derecha;
public ExpresionOr(IExpresion izquierda, IExpresion derecha)
{
_izquierda = izquierda;
_derecha = derecha;
}
public bool Interpretar(ContextoCliente contexto)
{
return _izquierda.Interpretar(contexto) || _derecha.Interpretar(contexto);
}
}
public sealed class ExpresionAnd : IExpresion
{
private readonly IExpresion _izquierda;
private readonly IExpresion _derecha;
public ExpresionAnd(IExpresion izquierda, IExpresion derecha)
{
_izquierda = izquierda;
_derecha = derecha;
}
public bool Interpretar(ContextoCliente contexto)
{
return _izquierda.Interpretar(contexto) && _derecha.Interpretar(contexto);
}
}
public sealed class ExpresionNot : IExpresion
{
private readonly IExpresion _expresion;
public ExpresionNot(IExpresion expresion)
{
_expresion = expresion;
}
public bool Interpretar(ContextoCliente contexto)
{
return !_expresion.Interpretar(contexto);
}
}
public sealed class ConstructorExpresiones
{
public IExpresion CategoriaEs(string categoria) =>
new ExpresionTerminalIgual("categoria", categoria);
public IExpresion ComprasMayoresA(int minimo) =>
new ExpresionMayorQue("compras", minimo);
public IExpresion UltimaVisitaDentroDe(TimeSpan duracion) =>
new ExpresionUltimaVisitaMenorQue(duracion);
public IExpresion Y(IExpresion izquierda, IExpresion derecha) =>
new ExpresionAnd(izquierda, derecha);
public IExpresion O(IExpresion izquierda, IExpresion derecha) =>
new ExpresionOr(izquierda, derecha);
public IExpresion No(IExpresion expresion) =>
new ExpresionNot(expresion);
}
public static class AplicacionInterpreter
{
public static void Main()
{
var builder = new ConstructorExpresiones();
IExpresion segmento = builder.O(
builder.Y(
builder.CategoriaEs("PREMIUM"),
builder.ComprasMayoresA(50)
),
builder.UltimaVisitaDentroDe(TimeSpan.FromDays(30))
);
var clienteA = new ContextoCliente(new Dictionary<string, object>
{
["categoria"] = "PREMIUM",
["compras"] = 80,
["ultimaVisita"] = DateTime.UtcNow.AddDays(-60)
});
var clienteB = new ContextoCliente(new Dictionary<string, object>
{
["categoria"] = "REGULAR",
["compras"] = 10,
["ultimaVisita"] = DateTime.UtcNow.AddDays(-5)
});
Console.WriteLine($"Cliente A aplica segmento? {segmento.Interpretar(clienteA)}");
Console.WriteLine($"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 invocando Interpretar
sobre 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.
En .NET, System.Linq.Expressions
permite construir árboles de expresiones que se interpretan o compilan en tiempo de ejecución, habilitando DSLs para filtrado y reglas de negocio. Frameworks como NRules o Dynamic LINQ emplean estos árboles para traducir expresiones declarativas en consultas y workflows sin acoplarse al código de aplicación.
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.