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.
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.
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:
Visitor facilita combinar operaciones sin contaminar la jerarquía de elementos con métodos que solo son relevantes para algunos contextos.
Los elementos clave del patrón son:
visitX
para cada tipo concreto de elemento.Accept(IVisitorPromocion)
.Accept
invocando al método correspondiente del visitante.La clave está en el doble despacho: el elemento delega en el visitante, que ejecuta la operación apropiada para el tipo concreto.
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.
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.
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());
}
}
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.
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.
Visitor admite adaptaciones para diferentes escenarios:
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.
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.