27. Visitor (Visitante) - Patrón de Comportamiento

El patrón Visitor permite definir nuevas operaciones sobre elementos de una jerarquía sin modificar las clases de dichos elementos. Se basa en el doble despacho: los objetos aceptan visitantes y delegan en ellos el código específico para cada tipo concreto.

Visitor resulta adecuado cuando la jerarquía es estable pero las operaciones cambian con frecuencia. Permite extraer lógica relacionada con informes, validaciones, serialización o transformaciones a una familia de visitantes intercambiables.

27.1 Problema y contexto

En sistemas con jerarquías complejas (AST de compiladores, estructuras de documentos, catálogos de productos) podría ser necesario agregar funcionalidad recurrentemente. Extender cada clase concreta introduce duplicaciones y viola el principio Abierto/Cerrado. Visitor elimina la necesidad de tocar esas clases encapsulando las operaciones externas en objetos dedicados.

Al centralizar la lógica en visitantes, es posible crear familias de operaciones sin recompilar la jerarquía original, manteniendo su integridad.

27.2 Intención y motivación

La intención del patrón es representar una operación que se realizará sobre los elementos de una estructura, separando la operación de la estructura misma. Es pertinente cuando:

  • La jerarquía de clases es estable pero se deben agregar nuevas operaciones con frecuencia.
  • Se requiere aplicar diferentes comportamientos dependiendo del tipo concreto del elemento.
  • Se busca mantener las clases del dominio enfocadas en su responsabilidad principal.
  • Se necesita recorrer estructuras complejas aplicando políticas variadas (auditar, exportar, transformar).

Visitor facilita combinar operaciones sin contaminar la jerarquía de elementos con métodos que solo son relevantes para algunos contextos.

27.3 Participantes y estructura

Los elementos clave del patrón son:

  • Visitor: interfaz que declara métodos visitX para cada tipo concreto de elemento.
  • ConcreteVisitor: implementaciones que encapsulan operaciones específicas.
  • Element: interfaz o clase abstracta que declara Accept(IVisitorPromocion).
  • ConcreteElement: clases de la jerarquía que implementan Accept invocando al método correspondiente del visitante.
  • ObjectStructure: opcional, responsable de proporcionar una interfaz para recorrer la colección de elementos.

La clave está en el doble despacho: el elemento delega en el visitante, que ejecuta la operación apropiada para el tipo concreto.

27.4 Variantes de extensibilidad

Visitor favorece agregar nuevas operaciones sin modificar los elementos, pero dificulta agregar nuevos tipos de elementos porque obliga a actualizar cada visitante. La conveniencia del patrón depende de cuál de las dos dimensiones (operaciones o elementos) cambia más frecuentemente.

En Jerarquías evolutivas es posible reducir la fricción usando visitantes por defecto que deleguen en un método genérico o aprovechar generics para simplificar la interfaz.

27.5 Escenario: motor de promociones omnicanal

Un marketplace gestiona promociones aplicables a diferentes dominios: productos, categorías y vendedores. Se requiere generar reportes de impacto, aplicar descuentos y evaluar cumplimiento de políticas fiscales sin duplicar código. Cada nueva operación debe agregarse rápidamente para responder a campañas.

Visitor permite encapsular cada operación en un visitante: uno calcula descuentos, otro genera un resumen contable y otro verifica restricciones regulatorias. La jerarquía de promociones permanece intacta.

27.6 Implementación en C#

El ejemplo siguiente define elementos de promoción y dos visitantes en C#: uno para calcular bonificaciones y otro para generar reportes:

using System;
using System.Collections.Generic;
using System.Text;

public interface IVisitorPromocion
{
    void Visit(PromocionProducto promocion);
    void Visit(PromocionCategoria promocion);
    void Visit(PromocionVendedor promocion);
}

public interface IPromocion
{
    void Accept(IVisitorPromocion visitor);
}

public sealed class PromocionProducto : IPromocion
{
    public string Sku { get; }
    public decimal Porcentaje { get; }
    public DateTime Vigencia { get; }

    public PromocionProducto(string sku, decimal porcentaje, DateTime vigencia)
    {
        Sku = sku ?? throw new ArgumentNullException(nameof(sku));
        Porcentaje = porcentaje;
        Vigencia = vigencia;
    }

    public void Accept(IVisitorPromocion visitor)
    {
        visitor.Visit(this);
    }
}

public sealed class PromocionCategoria : IPromocion
{
    public string Categoria { get; }
    public decimal MontoFijo { get; }

    public PromocionCategoria(string categoria, decimal montoFijo)
    {
        Categoria = categoria ?? throw new ArgumentNullException(nameof(categoria));
        MontoFijo = montoFijo;
    }

    public void Accept(IVisitorPromocion visitor)
    {
        visitor.Visit(this);
    }
}

public sealed class PromocionVendedor : IPromocion
{
    public string VendedorId { get; }
    public decimal Tope { get; }

    public PromocionVendedor(string vendedorId, decimal tope)
    {
        VendedorId = vendedorId ?? throw new ArgumentNullException(nameof(vendedorId));
        Tope = tope;
    }

    public void Accept(IVisitorPromocion visitor)
    {
        visitor.Visit(this);
    }
}

public sealed class VisitorBonificacion : IVisitorPromocion
{
    public decimal TotalBonificaciones { get; private set; }

    public void Visit(PromocionProducto promocion)
    {
        TotalBonificaciones += promocion.Porcentaje;
    }

    public void Visit(PromocionCategoria promocion)
    {
        TotalBonificaciones += promocion.MontoFijo;
    }

    public void Visit(PromocionVendedor promocion)
    {
        TotalBonificaciones += promocion.Tope * 0.05m;
    }
}

public sealed class VisitorReporte : IVisitorPromocion
{
    private readonly StringBuilder _builder = new();

    public void Visit(PromocionProducto promocion)
    {
        _builder.Append("Producto ")
            .Append(promocion.Sku)
            .Append(": descuento ")
            .Append(promocion.Porcentaje)
            .Append("% hasta ")
            .Append(promocion.Vigencia.ToShortDateString())
            .AppendLine();
    }

    public void Visit(PromocionCategoria promocion)
    {
        _builder.Append("Categoria ")
            .Append(promocion.Categoria)
            .Append(": monto fijo ")
            .Append(promocion.MontoFijo)
            .AppendLine();
    }

    public void Visit(PromocionVendedor promocion)
    {
        _builder.Append("Vendedor ")
            .Append(promocion.VendedorId)
            .Append(": tope ")
            .Append(promocion.Tope)
            .AppendLine();
    }

    public string Generar() => _builder.ToString();
}

public sealed class EstructuraPromociones
{
    private readonly List<IPromocion> _promociones = new();

    public void Agregar(IPromocion promocion)
    {
        _promociones.Add(promocion ?? throw new ArgumentNullException(nameof(promocion)));
    }

    public void Aplicar(IVisitorPromocion visitor)
    {
        foreach (var promocion in _promociones)
        {
            promocion.Accept(visitor);
        }
    }
}

public static class AplicacionVisitor
{
    public static void Main()
    {
        var estructura = new EstructuraPromociones();
        estructura.Agregar(new PromocionProducto("SKU-100", 10m, DateTime.Today.AddDays(30)));
        estructura.Agregar(new PromocionCategoria("Electro", 1500m));
        estructura.Agregar(new PromocionVendedor("VEN-09", 50000m));

        var visitorBonificacion = new VisitorBonificacion();
        estructura.Aplicar(visitorBonificacion);
        Console.WriteLine($"Total bonificaciones estimadas: {visitorBonificacion.TotalBonificaciones}");

        var visitorReporte = new VisitorReporte();
        estructura.Aplicar(visitorReporte);
        Console.WriteLine("Reporte de promociones:");
        Console.WriteLine(visitorReporte.Generar());
    }
}

27.7 Explicación del flujo

La estructura de promociones recorre cada elemento y ejecuta Accept, que invoca el método especializado del visitante. El visitante de bonificaciones suma distintos valores según el tipo; el visitante de reportes formatea la información. Agregar una tercera operación (por ejemplo, validar impuestos) implicaría crear un nuevo visitante sin tocar la jerarquía de promociones.

El patrón demuestra su utilidad cuando las operaciones se multiplican, manteniendo las clases de dominio libres de responsabilidades accesorias.

27.8 Visitor en el ecosistema .NET

El compilador Roslyn expone visitantes como SyntaxVisitor y CSharpSymbolVisitor para recorrer árboles de sintaxis y tablas de símbolos, facilitando analizadores y refactorizaciones. También aparecen visitantes en motores de workflow, librerías de validación o serialización (por ejemplo, pipelines de ASP.NET Core) para aplicar transformaciones sin modificar las clases de los nodos.

27.9 Variantes y extensiones

Visitor admite adaptaciones para diferentes escenarios:

  • Visitor jerárquico: utiliza clases abstractas que proveen métodos por defecto para simplificar visitantes parciales.
  • Visitor acumulador: devuelve valores agregados (por ejemplo, sumas o colecciones) en lugar de operar solo por efectos secundarios.
  • Visitor con contexto: se combina con objetos de configuración o estrategias para ajustar el comportamiento durante la visita.

27.10 Riesgos y malas prácticas

Un riesgo es abusar de Visitor en jerarquías que cambian con frecuencia, ya que agregar un nuevo elemento obliga a modificar todos los visitantes. También es problemático si los visitantes acumulan demasiada lógica, convirtiéndose en monolitos difíciles de mantener.

Otro anti-patrón consiste en mezclar estados mutables compartidos entre visitantes, generando efectos colaterales inesperados.

27.11 Buenas prácticas para aplicar Visitor

  • Documentar qué operaciones se encapsulan en visitantes y cuándo agregar una nueva clase.
  • Mantener visitantes cohesionados, enfocados en una tarea específica.
  • Combinar con pruebas que cubran cada tipo de elemento y visitante para prevenir regresiones.
  • Utilizar patrones complementarios (como Strategy o Template Method) para evitar duplicar lógica interna en los visitantes.

27.12 Relación con otros patrones

Visitor se complementa con Composite para recorrer estructuras jerárquicas y aplicar operaciones externas. Puede trabajarse junto a Interpreter para evaluar nodos de un árbol de expresiones y con Iterator para separar el recorrido de la lógica aplicada. Además, se relaciona con Strategy cuando cada visitante representa una estrategia alternativa de procesamiento.