28. Interpreter (Intérprete) - Patrón de Comportamiento

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.

28.1 Problema y contexto

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.

28.2 Intención y motivació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:

  • La gramática es relativamente sencilla y estable.
  • Se necesita agregar, combinar o reutilizar reglas sintácticas de forma modular.
  • Los usuarios o administradores construyen expresiones que se evaluan en tiempo de ejecución.
  • Se busca aislar la interpretación del resto de la aplicación.

Interpreter fomenta que el lenguaje evolucione con clases reutilizables que mantienen la semántica encapsulada.

28.3 Participantes y estructura

Los componentes principales son:

  • AbstractExpression: declara la interfaz Interpretar(ContextoCliente).
  • TerminalExpression: representa elementos indivisibles de la gramática (variables, literales).
  • NonterminalExpression: combina otras expresiones según reglas (por ejemplo, operadores lógicos).
  • Context: almacena información global necesaria para la interpretación (variables, servicios).
  • Client: construye la expresión abstracta correspondiente a la frase y desencadena la interpretación.

La estructura suele expresar el lenguaje mediante un árbol de sintaxis abstracta donde cada nodo conoce cómo evaluarse.

28.4 Alcances y limitaciones

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.

28.5 Escenario: reglas de segmentación de clientes

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.

28.6 Implementación en C#

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)}");
    }
}

28.7 Explicación del flujo

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.

28.8 Interpreter en el ecosistema .NET

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.

28.9 Variantes y extensiones

Interpreter se puede adaptar de varias maneras:

  • Analizador híbrido: combina Interpreter con un parser generado (ANTLR, Sprache, Irony) para construir el árbol de expresiones a partir de una cadena.
  • Expresiones cacheadas: almacena árboles interpretados para reutilizarlos en evaluaciones posteriores.
  • Interpretación diferida: en lugar de evaluar inmediatamente, se construyen expresiones que luego se traducen a consultas SQL o mensajes de otro sistema.

28.10 Riesgos y malas prácticas

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.

28.11 Buenas prácticas para aplicar Interpreter

  • Documentar la gramática y proporcionar ejemplos claros para los usuarios del lenguaje.
  • Implementar validaciones estáticas para detectar expresiones inválidas antes de interpretarlas.
  • Combinar con patrones como Flyweight para compartir instancias de expresiones repetidas.
  • Agregar monitoreo o logging para auditar qué expresiones interpretan los usuarios y optimizar su rendimiento.

28.12 Relación con otros patrones

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.