Patrón Visitor
Representa una operación a realizar sobre los elementos de una estructura de objetos sin cambiar las clases de los elementos. Un patrón de diseño de comportamiento.
Resumen
Una estructura de objetos que crece obliga a elegir entre dos opciones feas: añadir un método nuevo a cada clase de elemento, o llenar el código de cadenas de instanceof. El Patrón Visitor es un patrón de diseño de comportamiento que ofrece una tercera vía — permite definir una nueva operación sobre una estructura de objetos sin cambiar las clases de los elementos sobre los que opera.
El patrón separa los algoritmos de los objetos sobre los que operan. Añadir una operación nueva significa escribir un nuevo visitor, no tocar la jerarquía de elementos — por eso encaja en compiladores, exportadores y linters, donde los tipos de nodo son estables pero las operaciones no paran de multiplicarse.
Cuándo usarlo
Usa el Patrón Visitor cuando:
- Necesites realizar operaciones sobre todos los elementos de una estructura de objetos compleja — para estructuras de árbol, consulta el Patrón Composite.
- La estructura de objetos sea estable pero las operaciones sobre ella cambien con frecuencia — para algoritmos intercambiables, consulta el Patrón Strategy.
- Quieras evitar contaminar las clases de elementos con operaciones no relacionadas — consulta el Principio de Responsabilidad Única.
- La lógica de la operación dependa de la clase concreta del elemento, no solo de la interfaz.
- Ejemplos: recorrido de AST en compiladores, exportación de documentos (PDF, HTML), generación de reportes sobre árboles de entidades, reglas de lint sobre un árbol de sintaxis.
Evítalo cuando la jerarquía de elementos cambie a menudo — cada tipo nuevo obliga a actualizar todos los visitors. Para una aplicación concreta del patrón en TypeScript sobre un AST, consulta Visitor Pattern para Operaciones Extensibles.
Solución
Python
from abc import ABC, abstractmethod
from typing import List
class ShapeVisitor(ABC):
@abstractmethod
def visit_circle(self, circle):
pass
@abstractmethod
def visit_rectangle(self, rectangle):
pass
class Shape(ABC):
@abstractmethod
def accept(self, visitor: ShapeVisitor):
pass
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def accept(self, visitor: ShapeVisitor):
visitor.visit_circle(self)
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def accept(self, visitor: ShapeVisitor):
visitor.visit_rectangle(self)
class AreaVisitor(ShapeVisitor):
def __init__(self):
self.total = 0
def visit_circle(self, circle: Circle):
self.total += 3.14159 * circle.radius ** 2
def visit_rectangle(self, rectangle: Rectangle):
self.total += rectangle.width * rectangle.height
class DrawVisitor(ShapeVisitor):
def visit_circle(self, circle: Circle):
print(f"Drawing circle with radius {circle.radius}")
def visit_rectangle(self, rectangle: Rectangle):
print(f"Drawing rectangle {rectangle.width}x{rectangle.height}")
# Uso
shapes: List[Shape] = [Circle(5), Rectangle(4, 6)]
area_visitor = AreaVisitor()
for shape in shapes:
shape.accept(area_visitor)
print(f"Total area: {area_visitor.total}")
draw_visitor = DrawVisitor()
for shape in shapes:
shape.accept(draw_visitor)
JavaScript
class AreaVisitor {
constructor() {
this.total = 0;
}
visitCircle(circle) {
this.total += Math.PI * circle.radius ** 2;
}
visitRectangle(rectangle) {
this.total += rectangle.width * rectangle.height;
}
}
class DrawVisitor {
visitCircle(circle) {
console.log(`Drawing circle with radius ${circle.radius}`);
}
visitRectangle(rectangle) {
console.log(`Drawing rectangle ${rectangle.width}x${rectangle.height}`);
}
}
class Circle {
constructor(radius) {
this.radius = radius;
}
accept(visitor) {
visitor.visitCircle(this);
}
}
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
accept(visitor) {
visitor.visitRectangle(this);
}
}
// Uso
const shapes = [new Circle(5), new Rectangle(4, 6)];
const areaVisitor = new AreaVisitor();
shapes.forEach(s => s.accept(areaVisitor));
console.log(`Total area: ${areaVisitor.total}`);
const drawVisitor = new DrawVisitor();
shapes.forEach(s => s.accept(drawVisitor));
Java
public interface ShapeVisitor {
void visit(Circle circle);
void visit(Rectangle rectangle);
}
public interface Shape {
void accept(ShapeVisitor visitor);
}
public class Circle implements Shape {
public final double radius;
public Circle(double radius) {
this.radius = radius;
}
public void accept(ShapeVisitor visitor) {
visitor.visit(this);
}
}
public class Rectangle implements Shape {
public final double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public void accept(ShapeVisitor visitor) {
visitor.visit(this);
}
}
public class AreaVisitor implements ShapeVisitor {
public double total = 0;
public void visit(Circle circle) {
total += Math.PI * circle.radius * circle.radius;
}
public void visit(Rectangle rectangle) {
total += rectangle.width * rectangle.height;
}
}
public class DrawVisitor implements ShapeVisitor {
public void visit(Circle circle) {
System.out.println("Drawing circle with radius " + circle.radius);
}
public void visit(Rectangle rectangle) {
System.out.println("Drawing rectangle " + rectangle.width + "x" + rectangle.height);
}
}
// Uso
List<Shape> shapes = List.of(new Circle(5), new Rectangle(4, 6));
AreaVisitor area = new AreaVisitor();
shapes.forEach(s -> s.accept(area));
System.out.println("Total area: " + area.total);
DrawVisitor draw = new DrawVisitor();
shapes.forEach(s -> s.accept(draw));
Explicación
El Patrón Visitor tiene dos roles:
- Visitor (
ShapeVisitor): declara un métodovisit()para cada tipo de elemento concreto. - Elemento (
Shape): declara un métodoaccept()que recibe un visitor y llama al métodovisit()correspondiente.
Ese emparejamiento se llama doble despacho — el mecanismo sobre el que se construye todo el patrón. El primer despacho es shape.accept(visitor) — el runtime elige el elemento concreto. El segundo es visitor.visit_circle(self) dentro de accept — el visitor elige la sobrecarga correcta para ese tipo concreto. Dos llamadas virtuales resuelven ambas direcciones del par de tipos, así el visitor ejecuta código distinto por tipo de elemento sin un solo instanceof.
Añadir una operación nueva sigue siendo barato — una clase visitor más — mientras que añadir un tipo de elemento nuevo es la dirección cara: cada visitor existente necesita un método visitX, y en lenguajes tipados el compilador te obliga a recorrerlos todos. Además, los visitors suelen necesitar acceso a internals de los elementos, lo que debilita el encapsulamiento: Circle tuvo que exponer radius públicamente para que AreaVisitor funcionara. Acepta ese trade-off de forma explícita o el patrón filtrará abstracciones.
Variantes
| Variante | Descripción | Caso de uso |
|---|---|---|
| Visitor Clásico | Clase visitor separada por operación | Compiladores, recorrido de AST |
| Visitor Acíclico | Visitor usa una interfaz abstracta, no tipos concretos | Cuando la jerarquía de elementos es menos estable |
| Visitor Reflectivo | Usa reflexión para evitar métodos accept() | Prototipos, lenguajes de scripting |
| Visitor aplicado | Visitor sobre un AST en TypeScript con serialización | Operaciones extensibles sobre estructuras reales |
Lo que funciona
- Úsalo solo cuando la jerarquía de elementos sea estable — agregar un tipo nuevo obliga a cambiar todos los visitors.
- Agrupa operaciones relacionadas en un solo visitor en lugar de muchos pequeños — un
ExportVisitorque maneja PDF y HTML es mejor que dos micro-visitors. - Considera
instanceof+ sealed classes (Java 17+, o union types en TypeScript) como alternativa moderna cuando la jerarquía es pequeña. - Mantén los visitors sin estado cuando sea posible; si acumulan resultados como
total, documenta el estado y reinícialo entre recorridos. - Combínalo con Composite para recorrer estructuras de árbol — Composite es dueño de la estructura, Visitor es dueño de la operación.
Errores comunes
- Aplicar Visitor cuando la jerarquía de elementos cambia con frecuencia — el costo de mantenimiento cae sobre todos los visitors a la vez.
- Romper el encapsulamiento exponiendo más internals de los que el visitor necesita.
- Olvidar agregar métodos
accept()a los nuevos tipos de elemento — el elemento cae en silencio alvisitpor defecto o falla en runtime. - Usar Visitor cuando un simple método polimórfico sobrescrito haría el trabajo.
- Crear una clase visitor separada para cada operación pequeña — explosión de clases sin beneficio.
Solución de problemas
- Un tipo de elemento nuevo rompió todos los visitors: es el costo conocido del patrón, no un bug. Si la jerarquía en realidad no es estable, cambia a pattern matching (sealed classes +
switchexhaustivo en Java 17+, discriminated unions en TypeScript) en vez de pelear contra el patrón. - Vuelven a aparecer
instanceof: el doble despacho se está saltando — falta elaccept()en un elemento o elvisitXpara ese tipo en el visitor. - El visitor no alcanza el estado del elemento que necesita: expone solo el mínimo que la operación requiere, y trata el acoplamiento visitor↔elemento como una decisión de diseño deliberada, no un accidente.
- Resultados viejos de un visitor reutilizado: el estado acumulado (como
total) persiste entre recorridos. Reinícialo al empezar cada pasada o crea una instancia nueva. - Decenas de clases visitor diminutas: fusiona las operaciones relacionadas en un visitor cohesionado — un visitor por responsabilidad (exportar, dibujar, validar), no por método.
Puntos clave
- Visitor saca las operaciones de las clases de elemento y las mete en objetos visitor, resueltas por doble despacho:
accept()elige el elemento,visitX()elige la operación. - Rinde cuando la jerarquía es estable y las operaciones cambian a menudo — el caso clásico son compiladores y exportadores.
- Añadir tipos de elemento es la dirección cara: todos los visitors existentes deben actualizarse.
- La alternativa moderna son los sealed types con pattern matching exhaustivo — la misma garantía de cobertura con menos boilerplate.
Errores comunes en producción
- Aplicar Visitor a una jerarquía que gana un tipo de nodo cada sprint — el costo de actualización cae sobre todos los visitors simultáneamente.
- Dejar que los visitors accedan a internals profundos de los elementos, así cada refactor del elemento rompe todos los visitors.
- Reutilizar un visitor con estado entre recorridos sin reiniciar los resultados acumulados.
- Visitors que lanzan excepciones a mitad del recorrido y dejan resultados parciales — define la semántica de errores desde el inicio.
- Esconder la recursión dentro de
accept()en árboles profundos y chocar con límites de stack en estructuras grandes. - Usar Visitor donde un simple
switch/matchsobre un sealed type sería más corto y legible.
Preguntas frecuentes
¿Por qué no agregar métodos a las clases de elementos directamente?
Si la operación es específica de un caso de uso del cliente — exportación a PDF, por ejemplo — y no es intrínseca al elemento, agregarla directamente viola el Principio de Responsabilidad Única. Visitor mantiene las clases de elementos enfocadas y te deja acumular operaciones.
¿Qué es el doble despacho?
El despacho simple elige un método según el tipo de un objeto — polimorfismo normal. El doble despacho elige según dos tipos: el elemento vía accept() y luego el visitor vía visitX(this). Ese emparejamiento es lo que permite al visitor ejecutar código específico por tipo sin instanceof.
¿Hay una alternativa moderna a Visitor?
En lenguajes con sealed classes y pattern matching — Java 17+, TypeScript con discriminated unions — un switch/match con type checking exhaustivo te da la misma seguridad sin el boilerplate de accept(). Visitor sigue ganando cuando necesitas la lógica de la operación en un objeto separado y reutilizable.
¿Cuándo debería evitar el Patrón Visitor?
Cuando los tipos de elemento se agregan con frecuencia, cuando solo hay una o dos operaciones que podrían vivir en los propios elementos, o cuando tu lenguaje ya tiene buen pattern matching. Visitor solo justifica su complejidad en el cuadrante jerarquía-estable + muchas-operaciones.
¿Cómo se relaciona Visitor con Iterator y Composite?
¿Qué pasa cuando agrego un nuevo tipo de elemento?
Cada visitor concreto debe implementar un nuevo método visitX. En lenguajes tipados el compilador lo exige — lo cual es a la vez la ventaja (nada queda sin manejar en silencio) y el costo (un nodo nuevo toca todos los visitors de golpe).
Recursos Relacionados
Patrón Composite
Compone objetos en estructuras de árbol para representar jerarquías parte-todo. Un patrón estructural para tratar objetos individuales y composiciones uniformemente.
PatternPatrón Iterator
Proporciona una forma de acceder a los elementos de una colección secuencialmente sin exponer su representación subyacente. Un patrón de comportamiento para recorrido.
PatternPatrón Strategy
Define una familia de algoritmos, encapsula cada uno y los hace intercambiables. Patrón de diseño conductual para selección flexible de comportamiento.
PatternPatrón Visitor: Operaciones Extensibles en TypeScript
Separa algoritmos de los objetos sobre los que operan, permitiendo añadir nuevas operaciones sin modificar las clases de elementos existentes
GuideGuía Práctica de Design Patterns
Elige el design pattern correcto para cada problema: Singleton, Factory, Observer, Strategy y más, con código ejecutable y trade-offs honestos.