patron-visitor

Patrón Visitor: operaciones sobre una estructura estable

  • 5 min

El Visitor es un patrón que añade operaciones a una estructura de tipos estable mediante objetos visitantes. Favorece añadir nuevas operaciones, a cambio de hacer más costoso incorporar nuevos tipos de elemento.

Imagina un documento con nodos como párrafo, imagen y tabla. El sistema funciona bien, pero cada semana te piden una operación nueva:

  • “Ahora quiero exportar el documento a HTML.” -> Entráis en todas las clases (Parrafo, Imagen…) y añadís el método ExportHTML().
  • «Ahora quiero exportarlo a PDF.»* → Vuelves a entrar en todas y añades ExportPDF().
  • “Ahora quiero contar cuántas palabras tiene.” -> Otra vez a modificar todas las clases.

Las clases empiezan a acumular operaciones de exportación, análisis y presentación. Si esa lógica cambia por motivos distintos al propio modelo, puede tener sentido separarla.

Visitor lleva cada operación a una clase externa y usa despacho doble para seleccionar el método adecuado a cada tipo de nodo.

El Problema: El Despacho Doble (Double Dispatch)

Podríais pensar: “Vale, creo una clase Exportador con un método Exportar(Nodo nodo) y listo”.

El problema es que en lenguajes como C# o Java, si tienes una lista de List<Nodo>, no puedes hacer esto fácilmente:

foreach (Nodo n in nodos) {
    exportador.Exportar(n); // ❌ Error: ¿Qué sobrecarga llamo?
}
Copied!

El compilador solo sabe que n es un Nodo, no sabe si es un Parrafo o una Imagen en tiempo de compilación, por lo que no sabe si llamar a Exportar(Parrafo p) o Exportar(Imagen i).

La solución

El patrón Visitor soluciona esto usando una técnica llamada Double Dispatch (Despacho Doble).

El visitante no llama al elemento.

El elemento “acepta” al visitante y se llama a sí mismo.

:::

Implementación en C#

Vamos a implementar nuestro sistema de documentos y exportadores.

Las Interfaces (El Contrato)

Primero definimos qué puede hacer un visitante y qué deben hacer los elementos.

// La interfaz del Visitante: Debe saber manejar CADA tipo concreto
public interface IVisitor
{
    void Visit(Parrafo parrafo);
    void Visit(Imagen imagen);
    void Visit(Tabla tabla);
}

// La interfaz del Elemento: Debe aceptar visitantes
public interface INodoDocumento
{
    void Accept(IVisitor visitor);
}
Copied!

Los Elementos Concretos (Estructura Estable)

Fíjate en el método Accept. Es la clave. Al hacer visitor.Visit(this), this ya no es un INodoDocumento, es un Parrafo (o Imagen). El compilador ya sabe exactamente a qué método llamar.

public class Parrafo : INodoDocumento
{
    public string Texto { get; set; }

    public Parrafo(string texto) => Texto = texto;

    public void Accept(IVisitor visitor)
    {
        // Punto clave: Double Dispatch
        visitor.Visit(this); 
    }
}

public class Imagen : INodoDocumento
{
    public string Ruta { get; set; }

    public Imagen(string ruta) => Ruta = ruta;

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

public class Tabla : INodoDocumento
{
    public int Filas { get; set; }

    public Tabla(int filas) => Filas = filas;

    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }
}
Copied!

Los Visitantes (Los Algoritmos)

Ahora podemos crear tantos algoritmos como queramos sin tocar las clases de arriba.

Visitante 1: Exportar a HTML

public class HtmlExportVisitor : IVisitor
{
    public void Visit(Parrafo parrafo)
    {
        Console.WriteLine($"<p>{parrafo.Texto}</p>");
    }

    public void Visit(Imagen imagen)
    {
        Console.WriteLine($"<img src='{imagen.Ruta}' />");
    }

    public void Visit(Tabla tabla)
    {
        Console.WriteLine($"<table>... ({tabla.Filas} filas) ...</table>");
    }
}
Copied!

Visitante 2: Estadísticas (Contar cosas)

public class EstadisticasVisitor : IVisitor
{
    public int ContadorPalabras { get; private set; }
    public int ContadorImagenes { get; private set; }

    public void Visit(Parrafo parrafo)
    {
        ContadorPalabras += parrafo.Texto.Split(' ').Length;
    }

    public void Visit(Imagen imagen)
    {
        ContadorImagenes++;
    }

    public void Visit(Tabla tabla)
    {
        // Las tablas no suman palabras en este ejemplo
    }
}
Copied!

El Cliente

class Program
{
    static void Main(string[] args)
    {
        // 1. Creamos la estructura del documento (puede ser una lista, un árbol...)
        var documento = new List<INodoDocumento>
        {
            new Parrafo("Hola Mundo"),
            new Tabla(5),
            new Imagen("/img/foto.jpg"),
            new Parrafo("Fin del documento")
        };

        // 2. Queremos exportar a HTML
        Console.WriteLine("--- Exportando a HTML ---");
        var exportadorHtml = new HtmlExportVisitor();
        
        foreach (var nodo in documento)
        {
            // El segundo despacho ocurre aquí
            nodo.Accept(exportadorHtml);
        }

        // 3. Queremos calcular estadísticas
        Console.WriteLine("\n--- Calculando Estadísticas ---");
        var stats = new EstadisticasVisitor();
        
        foreach (var nodo in documento)
        {
            nodo.Accept(stats);
        }

        Console.WriteLine($"Total Palabras: {stats.ContadorPalabras}");
        Console.WriteLine($"Total Imágenes: {stats.ContadorImagenes}");
    }
}
Copied!

Salida:

--- Exportando a HTML ---
<p>Hola Mundo</p>
<table>... (5 filas) ...</table>
<img src='/img/foto.jpg' />
<p>Fin del documento</p>

--- Calculando Estadísticas ---
Total Palabras: 5
Total Imágenes: 1
Copied!

Ventajas y Desventajas

VentajasDesventajas
Open/Closed: Puedes añadir nuevas operaciones (ExportarXML, Comprimir) sin tocar las clases de los nodos.Jerarquía Rígida: Si añades un nuevo tipo de nodo (Video), tienes que modificar la interfaz IVisitor y todos los visitantes existentes.
SRP: Agrupa la lógica relacionada en una sola clase (todo el código HTML está junto).Acceso Limitado: El visitante solo puede acceder a los miembros públicos de los elementos (rompe un poco el encapsulamiento si necesitas acceder a privados).

El patrón Visitor es muy útil cuando tienes una estructura de objetos que cambia poco (ej: la sintaxis de un lenguaje, un documento XML, un árbol de escena 3D) pero quieres añadir operaciones sobre ella frecuentemente.