🚨 ¡Nueva review! ¡Mi teclado ideal! ⌨️ Perfecto para programar, el Logitech MX Keys S . ¡Échale un ojo! 👀

El Patrón de diseño Visitor

Añade funcionalidades a tus clases sin tocarlas.

Escrito por domin el 7 de diciembre de 2025 · Actualizado el 8 de febrero de 2026

🧳 El Patrón Visitor: Añade Operaciones Sin Tocar Tus Clases

El Patrón Visitor es un patrón de comportamiento que resuelve un problema bastante concreto: tienes una estructura de objetos estable y quieres añadir nuevas operaciones sobre ella sin modificar esas clases.

Imagina que tienes Circulo, Cuadrado y Triangulo. Un día necesitas exportar a XML, otro día a JSON y otro día calcular el perímetro. Sin Visitor, cada nueva operación significa abrir y modificar todas las clases. Con Visitor, creas una clase externa que contiene el algoritmo y sabe cómo ejecutarlo para cada tipo concreto.

Paquementiendas: tus clases le abren la puerta al visitante y le dicen “aquí tienes mis datos, haz lo que necesites”. Así puedes añadir exportaciones, validaciones, cálculos o lo que te dé la gana sin tocar ni una línea de las clases originales.

Diagrama del Patrón Visitor.

🏙️ 1. El Turista y los Edificios

Imagina una ciudad con edificios: un museo, un parque y una tienda. Estos edificios no cambian, llevan ahí décadas.

Los edificios no cambian, son los mismos. Pero lo que se hace en ellos depende de quién los visita. Y lo más importante, si mañana llega un tasador de seguros, los edificios no necesitan reformarse para recibirle.

¿Cuál es el objetivo principal del patrón Visitor?


🛠️ 2. Los roles del patrón

Interfaz Visitor

Declara un método visitar para cada tipo de elemento concreto: visitarCirculo(), visitarCuadrado()... Cada visitor concreto implementa estos métodos con su propia lógica.

Interfaz Elemento

Declara el método aceptar(Visitor $v). Es el contrato que permite que cualquier visitor pueda operar sobre el elemento. Los elementos concretos implementan este método llamando al visitor correspondiente.

Double Dispatch

La magia del patrón. El elemento llama a $v->visitarEsteElemento($this). Así el método que se ejecuta depende del tipo del visitante Y del tipo del elemento. Dos niveles de polimorfismo.

La clave es que los elementos no saben qué van a hacer los visitors con ellos. Solo les abren la puerta y les pasan $this. El visitor ya sabe qué hacer con cada tipo.


🧩 3. Double Dispatch: La magia explicada

En PHP, y en la mayoría de lenguajes orientados a objetos, solo tienes single dispatch, esto quiere decir el método que se ejecuta depende únicamente del tipo del objeto que lo invoca. Pero con Visitor necesitamos que la operación dependa de dos tipos: el del visitor y el del elemento.

El truco es encadenar dos llamadas:

// Paso 1: El cliente llama a aceptar() en el elemento
$circulo->aceptar($exportadorXML);

// Paso 2: Dentro de Circulo::aceptar(), se hace:
public function aceptar(Visitor $v): void {
    $v->visitarCirculo($this);  // <-- Double Dispatch
}

// Paso 3: PHP ya sabe:
// - $v es un ExportarXMLVisitor (primer dispatch)
// - El método es visitarCirculo, no visitarCuadrado (segundo dispatch)

Con este mecanismo, el visitor correcto ejecuta el método correcto para el tipo correcto de elemento. Sin instanceof, sin switch, sin if/else. Polimorfismo puro.

¿Qué es el Double Dispatch en el contexto del patrón Visitor?


🧑‍💻 4. Ejemplo práctico en PHP: Exportación de formas

Tienes Circulo y Cuadrado, y quieres exportar a XML, JSON y calcular áreas, todo sin modificar las clases de las formas.

<?php

// 1. Interfaz Visitor
interface FormaVisitor
{
    public function visitarCirculo(Circulo $c): void;
    public function visitarCuadrado(Cuadrado $c): void;
}

// 2. Interfaz Elemento
interface Forma
{
    public function aceptar(FormaVisitor $v): void;
}

// 3. Elementos concretos (NO se tocan cuando añades operaciones)
class Circulo implements Forma
{
    public function __construct(
        public readonly float $radio
    ) {}

    public function aceptar(FormaVisitor $v): void
    {
        $v->visitarCirculo($this);
    }
}

class Cuadrado implements Forma
{
    public function __construct(
        public readonly float $lado
    ) {}

    public function aceptar(FormaVisitor $v): void
    {
        $v->visitarCuadrado($this);
    }
}

// 4. Visitor concreto: Exportar a XML
class ExportarXMLVisitor implements FormaVisitor
{
    private string $buffer = "";

    public function visitarCirculo(Circulo $c): void
    {
        $this->buffer .= "    <circulo radio=\"{$c->radio}\" />\n";
    }

    public function visitarCuadrado(Cuadrado $c): void
    {
        $this->buffer .= "    <cuadrado lado=\"{$c->lado}\" />\n";
    }

    public function obtenerResultado(): string
    {
        return "<formas>\n{$this->buffer}</formas>";
    }
}

// 5. Visitor concreto: Exportar a JSON
class ExportarJSONVisitor implements FormaVisitor
{
    private array $data = [];

    public function visitarCirculo(Circulo $c): void
    {
        $this->data[] = ['tipo' => 'circulo', 'radio' => $c->radio];
    }

    public function visitarCuadrado(Cuadrado $c): void
    {
        $this->data[] = ['tipo' => 'cuadrado', 'lado' => $c->lado];
    }

    public function obtenerResultado(): string
    {
        return json_encode($this->data, JSON_PRETTY_PRINT);
    }
}

// 6. Visitor concreto: Calcular área total
class CalcularAreaVisitor implements FormaVisitor
{
    private float $areaTotal = 0;

    public function visitarCirculo(Circulo $c): void
    {
        $this->areaTotal += M_PI * $c->radio ** 2;
    }

    public function visitarCuadrado(Cuadrado $c): void
    {
        $this->areaTotal += $c->lado ** 2;
    }

    public function obtenerResultado(): float
    {
        return round($this->areaTotal, 2);
    }
}

// 7. Uso
$formas = [
    new Circulo(5),
    new Cuadrado(10),
    new Circulo(2),
];

$xml = new ExportarXMLVisitor();
$json = new ExportarJSONVisitor();
$area = new CalcularAreaVisitor();

foreach ($formas as $forma) {
    $forma->aceptar($xml);
    $forma->aceptar($json);
    $forma->aceptar($area);
}

echo $xml->obtenerResultado();
// <formas>
//     <circulo radio="5" />
//     <cuadrado lado="10" />
//     <circulo radio="2" />
// </formas>

echo $json->obtenerResultado();
// [{"tipo":"circulo","radio":5}, ...]

echo "Área total: " . $area->obtenerResultado();
// Área total: 191.27

Fíjate en lo bonito que es esto: para añadir una nueva operación (calcular perímetro, validar dimensiones, generar SVG…), solo creas un nuevo visitor. Las clases Circulo y Cuadrado no se tocan.

¿Qué método deben implementar los elementos para participar en el patrón Visitor?


💼 5. Ejemplo real: Validador de documentos

Imagina un sistema de gestión documental con diferentes tipos de documentos. Necesitas aplicar múltiples operaciones como validar, calcular tamaño de almacenamiento y generar resúmenes. Sin Visitor, cada operación nueva significaría modificar todas las clases de documento.

<?php

// Interfaz Visitor
interface DocumentoVisitor
{
    public function visitarPDF(DocumentoPDF $doc): void;
    public function visitarImagen(DocumentoImagen $doc): void;
    public function visitarHojaCalculo(DocumentoHojaCalculo $doc): void;
}

// Interfaz Elemento
interface Documento
{
    public function aceptar(DocumentoVisitor $v): void;
    public function getNombre(): string;
}

// Elementos concretos
class DocumentoPDF implements Documento
{
    public function __construct(
        private string $nombre,
        private int $paginas,
        private bool $tieneImagenes,
        private float $tamanoMB
    ) {}

    public function aceptar(DocumentoVisitor $v): void
    {
        $v->visitarPDF($this);
    }

    public function getNombre(): string { return $this->nombre; }
    public function getPaginas(): int { return $this->paginas; }
    public function tieneImagenes(): bool { return $this->tieneImagenes; }
    public function getTamanoMB(): float { return $this->tamanoMB; }
}

class DocumentoImagen implements Documento
{
    public function __construct(
        private string $nombre,
        private int $anchoPixeles,
        private int $altoPixeles,
        private string $formato // jpg, png, webp
    ) {}

    public function aceptar(DocumentoVisitor $v): void
    {
        $v->visitarImagen($this);
    }

    public function getNombre(): string { return $this->nombre; }
    public function getAnchoPixeles(): int { return $this->anchoPixeles; }
    public function getAltoPixeles(): int { return $this->altoPixeles; }
    public function getFormato(): string { return $this->formato; }
}

class DocumentoHojaCalculo implements Documento
{
    public function __construct(
        private string $nombre,
        private int $filas,
        private int $columnas,
        private bool $tieneMacros
    ) {}

    public function aceptar(DocumentoVisitor $v): void
    {
        $v->visitarHojaCalculo($this);
    }

    public function getNombre(): string { return $this->nombre; }
    public function getFilas(): int { return $this->filas; }
    public function getColumnas(): int { return $this->columnas; }
    public function tieneMacros(): bool { return $this->tieneMacros; }
}

// Visitor 1: Validador de seguridad
class ValidadorSeguridadVisitor implements DocumentoVisitor
{
    private array $errores = [];

    public function visitarPDF(DocumentoPDF $doc): void
    {
        if ($doc->getTamanoMB() > 50) {
            $this->errores[] = "{$doc->getNombre()}: PDF excede 50MB";
        }
    }

    public function visitarImagen(DocumentoImagen $doc): void
    {
        $pixeles = $doc->getAnchoPixeles() * $doc->getAltoPixeles();
        if ($pixeles > 25_000_000) {
            $this->errores[] = "{$doc->getNombre()}: imagen supera 25 megapíxeles";
        }
        if ($doc->getFormato() === 'bmp') {
            $this->errores[] = "{$doc->getNombre()}: formato BMP no permitido";
        }
    }

    public function visitarHojaCalculo(DocumentoHojaCalculo $doc): void
    {
        if ($doc->tieneMacros()) {
            $this->errores[] = "{$doc->getNombre()}: las macros no están permitidas";
        }
        if ($doc->getFilas() > 100_000) {
            $this->errores[] = "{$doc->getNombre()}: excede 100.000 filas";
        }
    }

    public function esValido(): bool
    {
        return empty($this->errores);
    }

    public function getErrores(): array
    {
        return $this->errores;
    }
}

// Visitor 2: Calculador de almacenamiento
class AlmacenamientoVisitor implements DocumentoVisitor
{
    private float $totalMB = 0;

    public function visitarPDF(DocumentoPDF $doc): void
    {
        $this->totalMB += $doc->getTamanoMB();
    }

    public function visitarImagen(DocumentoImagen $doc): void
    {
        $bpp = match ($doc->getFormato()) {
            'jpg' => 0.3,  // ~0.3 bytes por píxel comprimido
            'png' => 1.0,
            'webp' => 0.2,
            default => 0.5,
        };
        $pixeles = $doc->getAnchoPixeles() * $doc->getAltoPixeles();
        $this->totalMB += ($pixeles * $bpp) / (1024 * 1024);
    }

    public function visitarHojaCalculo(DocumentoHojaCalculo $doc): void
    {
        // Estimación: ~50 bytes por celda
        $celdas = $doc->getFilas() * $doc->getColumnas();
        $this->totalMB += ($celdas * 50) / (1024 * 1024);
    }

    public function getTotalMB(): float
    {
        return round($this->totalMB, 2);
    }
}

// Uso
$documentos = [
    new DocumentoPDF('informe-anual.pdf', 120, true, 15.5),
    new DocumentoImagen('logo.png', 1920, 1080, 'png'),
    new DocumentoHojaCalculo('ventas-2024.xlsx', 50000, 20, false),
    new DocumentoImagen('banner.jpg', 3840, 2160, 'jpg'),
    new DocumentoHojaCalculo('macros.xlsm', 500, 10, true),
];

$validador = new ValidadorSeguridadVisitor();
$almacenamiento = new AlmacenamientoVisitor();

foreach ($documentos as $doc) {
    $doc->aceptar($validador);
    $doc->aceptar($almacenamiento);
}

if (!$validador->esValido()) {
    echo "Errores encontrados:\n";
    foreach ($validador->getErrores() as $error) {
        echo "  ❌ {$error}\n";
    }
}
// Errores encontrados:
//   ❌ macros.xlsm: las macros no están permitidas

echo "Almacenamiento estimado: {$almacenamiento->getTotalMB()} MB\n";
// Almacenamiento estimado: 19.38 MB

Mañana el cliente pide un visitor que genere miniaturas, otro que comprima documentos, otro que genere metadatos… Creas una nueva clase y listo. Ni DocumentoPDF ni DocumentoImagen se enteran de que existen.


😱 6. El horror de no usar Visitor

Veamos qué pasa si metes la lógica directamente en las clases:

// ❌ MAL: Cada operación nueva = modificar TODAS las clases

class Circulo
{
    public function toXML(): string { /* ... */ }
    public function toJSON(): string { /* ... */ }
    public function calcularArea(): float { /* ... */ }
    public function validar(): bool { /* ... */ }
    public function generarSVG(): string { /* ... */ }     // nueva op
    public function calcularPerimetro(): float { /* ... */ } // otra más
    // Y la clase crece y crece sin parar...
}

class Cuadrado
{
    public function toXML(): string { /* ... */ }
    public function toJSON(): string { /* ... */ }
    public function calcularArea(): float { /* ... */ }
    public function validar(): bool { /* ... */ }
    public function generarSVG(): string { /* ... */ }
    public function calcularPerimetro(): float { /* ... */ }
    // La misma historia...
}

// 5 clases × 6 operaciones = 30 métodos repartidos por todo el código
// Si cambias la lógica de exportación XML, tocas 5 archivos

Con Visitor, la lógica de exportar XML está en un solo sitio: ExportarXMLVisitor. Si cambia, se toca un archivo. Si se añade una operación, se crea un archivo. Las clases de datos no se modifican.

¿Qué pasa si añades un nuevo tipo de Elemento (ej: Triángulo) a la jerarquía?


🌍 7. Visitor en el mundo real

Compiladores y ASTs

Los compiladores usan Visitor constantemente. El AST (Árbol de Sintaxis Abstracta) tiene nodos estables (IfNode, WhileNode, AssignNode...) y los visitors aplican operaciones: chequeo de tipos, optimización, generación de código. PHPStan y Psalm lo usan internamente.

Doctrine y ORMs

El QueryBuilder de Doctrine usa un SqlWalker que es un Visitor. Recorre los nodos del DQL (SELECT, FROM, WHERE...) y genera SQL concreto para cada motor de base de datos. MySQL, PostgreSQL, SQLite... cada uno tiene su walker.

DOM y XML/HTML parsers

Las APIs de DOM usan TreeWalker y NodeIterator para recorrer el árbol. Librerías como nikic/PHP-Parser exponen un NodeVisitor que recorre el código PHP parseado y permite transformar, analizar o reescribir nodos.

Exportadores y serializadores

Cuando tienes una estructura de datos compleja y necesitas exportar a múltiples formatos (PDF, CSV, XML, JSON), Visitor es la elección natural. Cada formato es un visitor, la estructura de datos no se modifica. Symfony Serializer sigue este principio.

¿Cuál es un caso de uso real del patrón Visitor?


⚖️ 8. Comparación con otros patrones

PatrónPropósitoDiferencia con Visitor
StrategyIntercambia algoritmos en un objetoStrategy cambia cómo se hace algo.Visitor añade qué se hace sobre una estructura
IteratorRecorre una colección secuencialmenteIterator solo recorre. Visitor recorre y ejecuta operaciones distintas según el tipo
CommandEncapsula una acción como objetoCommand encapsula una acción; Visitor agrupa varias operaciones sobre una jerarquía
DecoratorAñade comportamiento dinámicamenteDecorator envuelve un objeto. Visitor opera desde fuera sin modificar la estructura
CompositeTrata objetos y grupos uniformementeSe complementan: Visitor recorre la estructura Composite para operar sobre cada nodo

Visitor vs Strategy

Muchos los confunden porque ambos externalizan lógica, pero su intención es muy diferente:

La clave: Strategy opera sobre un tipo de objeto. Visitor opera sobre múltiples tipos y usa double dispatch para elegir el método correcto.

¿Cuál es la principal diferencia entre Visitor y Strategy?


🧱 9. Principios SOLID que cumple

Open/Closed (OCP)

Las clases de elementos están cerradas a modificación pero abiertas a extensión vía nuevos visitors. Puedes añadir exportar a CSV, validar, calcular... sin tocar las clases originales.

Single Responsibility (SRP)

Toda la lógica de exportar a XML está en ExportarXMLVisitor. Toda la de validar en ValidadorVisitor. Cada visitor tiene una sola responsabilidad, no está esparcida por 10 clases.

Dependency Inversion (DIP)

Los elementos dependen de la interfaz Visitor, no de visitors concretos. Y los visitors dependen de la interfaz Elemento. Todo desacoplado a través de abstracciones.

Liskov Substitution (LSP)

Cualquier visitor concreto puede sustituir a otro en el foreach del cliente. Un ExportarCSVVisitor se usa exactamente igual que un ExportarXMLVisitor. Misma interfaz, diferente comportamiento.


✅ 10. Ventajas y desventajas

Ventajas
  • Añades operaciones sin modificar las clases existentes
  • Toda la lógica de una operación queda en una sola clase
  • Los visitors pueden acumular estado mientras recorren la estructura
  • Facilita operaciones complejas sobre estructuras heterogéneas (ASTs, DOMs)
  • Sigue OCP a rajatabla: nuevas operaciones = nuevas clases
Desventajas
  • Añadir un nuevo tipo de elemento obliga a modificar todos los visitors
  • Los visitors necesitan acceso a los datos del elemento, lo que puede romper encapsulamiento
  • El double dispatch puede ser confuso si no estás familiarizado con el patrón
  • Sobreingeniería si la jerarquía de elementos cambia frecuentemente

Visitor te da libertad para añadir operaciones, pero a cambio te pide estabilidad en los elementos. Si tus tipos de elementos cambian poco pero las operaciones crecen, Visitor es perfecto. Si los tipos cambian mucho, huye.


🐛 11. Errores comunes y cómo evitarlos

Error 1: Usar Visitor cuando la jerarquía cambia frecuentemente

// ❌ MAL: Si cada mes añades un nuevo tipo de documento...
interface DocumentoVisitor
{
    public function visitarPDF(PDF $d): void;
    public function visitarWord(Word $d): void;
    public function visitarExcel(Excel $d): void;
    public function visitarPowerPoint(PowerPoint $d): void;  // nuevo
    public function visitarODS(ODS $d): void;                // otro nuevo
    // Cada tipo nuevo = modificar TODOS los visitors existentes
}

// ✅ BIEN: Usa Visitor solo si los tipos son estables
// Si cambian mucho, considera Strategy o simplemente polimorfismo normal

Error 2: Visitor que hace demasiado

// ❌ MAL: Un visitor que valida, exporta Y calcula
class SuperVisitor implements FormaVisitor
{
    public function visitarCirculo(Circulo $c): void
    {
        // Validar
        if ($c->radio <= 0) { /* ... */ }
        // Exportar
        $xml = "<circulo radio=\"{$c->radio}\" />";
        // Calcular
        $area = M_PI * $c->radio ** 2;
        // Esto viola SRP
    }
}

// ✅ BIEN: Un visitor por operación
class ValidarFormaVisitor implements FormaVisitor { /* solo valida */ }
class ExportarXMLVisitor implements FormaVisitor { /* solo exporta */ }
class CalcularAreaVisitor implements FormaVisitor { /* solo calcula */ }

Error 3: Exponer todos los datos como públicos

// ❌ MAL: Hacer todo público "para que el visitor acceda"
class Circulo implements Forma
{
    public float $radio;
    public float $x;
    public float $y;
    public string $color;
    public string $nombre;
    // Todo public = encapsulamiento muerto
}

// ✅ BIEN: Expón solo lo necesario con getters específicos
class Circulo implements Forma
{
    public function __construct(
        private float $radio,
        private float $x,
        private float $y,
    ) {}

    public function getRadio(): float { return $this->radio; }
    public function getCentro(): array { return [$this->x, $this->y]; }

    public function aceptar(FormaVisitor $v): void
    {
        $v->visitarCirculo($this);
    }
}

Error 4: Olvidar implementar aceptar() con double dispatch

// ❌ MAL: Usar instanceof en el visitor en vez de double dispatch
class ExportarXMLVisitor
{
    public function visitar(Forma $f): void
    {
        if ($f instanceof Circulo) {
            // exportar circulo
        } elseif ($f instanceof Cuadrado) {
            // exportar cuadrado
        }
        // Esto es un if/else disfrazado, no un Visitor
    }
}

// ✅ BIEN: Cada elemento implementa aceptar() correctamente
class Circulo implements Forma
{
    public function aceptar(FormaVisitor $v): void
    {
        $v->visitarCirculo($this); // El double dispatch hace el trabajo
    }
}

¿Qué problema tiene usar instanceof en vez de double dispatch dentro del Visitor?


🎯 12. Cuándo usar Visitor

Úsalo cuando...
  • Tu jerarquía de elementos es estable y rara vez cambia
  • Necesitas añadir operaciones nuevas con frecuencia
  • Las operaciones necesitan datos de tipos heterogéneos
  • Quieres agrupar la lógica de una operación en un solo lugar
Evítalo cuando...
  • Los tipos de elementos cambian frecuentemente
  • Solo tienes un tipo de elemento (Strategy es más simple)
  • Las operaciones no necesitan distinguir entre tipos
  • No quieres exponer los datos internos de los elementos

¿En qué situación NO es recomendable usar Visitor?


💡 13. Conclusión

El patrón de diseño Visitor es uno de los patrones más potentes, pero también uno de los más exigentes. Su superpoder es permitirte añadir infinitas operaciones sin tocar las clases originales. Su kryptonita es que añadir un nuevo tipo de elemento obliga a modificar todos los visitors.

Los compiladores, los ORMs, los linters y los serializadores lo usan a diario. Si trabajas con estructuras de datos complejas y estables (ASTs, documentos, árboles de nodos), Visitor será tu mejor amigo.

Recuerda: la clave es el double dispatch. El elemento le dice al visitor “soy un círculo, haz conmigo lo que sepas”. Y el visitor, dependiendo de quién sea, hace algo diferente con cada tipo.

EA, ¡saluditos y nos vemos en los bares! 🍻