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(Visitor).
  • 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 Java

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

package tutorial.visitor;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

public interface VisitorPromocion {
    void visit(PromocionProducto promocion);
    void visit(PromocionCategoria promocion);
    void visit(PromocionVendedor promocion);
}

public interface Promocion {
    void accept(VisitorPromocion visitor);
}

public final class PromocionProducto implements Promocion {
    private final String sku;
    private final BigDecimal porcentaje;
    private final LocalDate vigencia;

    public PromocionProducto(String sku, BigDecimal porcentaje, LocalDate vigencia) {
        this.sku = Objects.requireNonNull(sku);
        this.porcentaje = Objects.requireNonNull(porcentaje);
        this.vigencia = Objects.requireNonNull(vigencia);
    }

    public String getSku() {
        return sku;
    }

    public BigDecimal getPorcentaje() {
        return porcentaje;
    }

    public LocalDate getVigencia() {
        return vigencia;
    }

    @Override
    public void accept(VisitorPromocion visitor) {
        visitor.visit(this);
    }
}

public final class PromocionCategoria implements Promocion {
    private final String categoria;
    private final BigDecimal montoFijo;

    public PromocionCategoria(String categoria, BigDecimal montoFijo) {
        this.categoria = Objects.requireNonNull(categoria);
        this.montoFijo = Objects.requireNonNull(montoFijo);
    }

    public String getCategoria() {
        return categoria;
    }

    public BigDecimal getMontoFijo() {
        return montoFijo;
    }

    @Override
    public void accept(VisitorPromocion visitor) {
        visitor.visit(this);
    }
}

public final class PromocionVendedor implements Promocion {
    private final String vendedorId;
    private final BigDecimal tope;

    public PromocionVendedor(String vendedorId, BigDecimal tope) {
        this.vendedorId = Objects.requireNonNull(vendedorId);
        this.tope = Objects.requireNonNull(tope);
    }

    public String getVendedorId() {
        return vendedorId;
    }

    public BigDecimal getTope() {
        return tope;
    }

    @Override
    public void accept(VisitorPromocion visitor) {
        visitor.visit(this);
    }
}

class VisitorBonificacion implements VisitorPromocion {
    private BigDecimal totalBonificaciones = BigDecimal.ZERO;

    @Override
    public void visit(PromocionProducto promocion) {
        totalBonificaciones = totalBonificaciones.add(promocion.getPorcentaje());
    }

    @Override
    public void visit(PromocionCategoria promocion) {
        totalBonificaciones = totalBonificaciones.add(promocion.getMontoFijo());
    }

    @Override
    public void visit(PromocionVendedor promocion) {
        totalBonificaciones = totalBonificaciones.add(promocion.getTope().multiply(new BigDecimal("0.05")));
    }

    public BigDecimal getTotalBonificaciones() {
        return totalBonificaciones;
    }
}

class VisitorReporte implements VisitorPromocion {
    private final StringBuilder builder = new StringBuilder();

    @Override
    public void visit(PromocionProducto promocion) {
        builder.append("Producto ")
                .append(promocion.getSku())
                .append(": descuento ")
                .append(promocion.getPorcentaje())
                .append("% hasta ")
                .append(promocion.getVigencia())
                .append("\n");
    }

    @Override
    public void visit(PromocionCategoria promocion) {
        builder.append("Categoría ")
                .append(promocion.getCategoria())
                .append(": monto fijo ")
                .append(promocion.getMontoFijo())
                .append("\n");
    }

    @Override
    public void visit(PromocionVendedor promocion) {
        builder.append("Vendedor ")
                .append(promocion.getVendedorId())
                .append(": tope ")
                .append(promocion.getTope())
                .append("\n");
    }

    public String generar() {
        return builder.toString();
    }
}

class EstructuraPromociones {
    private final List<Promocion> promociones = new ArrayList<>();

    public void agregar(Promocion promocion) {
        promociones.add(promocion);
    }

    public void aplicar(VisitorPromocion visitor) {
        promociones.forEach(promocion -> promocion.accept(visitor));
    }
}

class AplicacionVisitor {
    public static void main(String[] args) {
        EstructuraPromociones estructura = new EstructuraPromociones();
        estructura.agregar(new PromocionProducto("SKU-100", new BigDecimal("10"), LocalDate.now().plusDays(30)));
        estructura.agregar(new PromocionCategoria("Electro", new BigDecimal("1500")));
        estructura.agregar(new PromocionVendedor("VEN-09", new BigDecimal("50000")));

        VisitorBonificacion visitorBonificacion = new VisitorBonificacion();
        estructura.aplicar(visitorBonificacion);
        System.out.println("Total bonificaciones estimadas: " + visitorBonificacion.getTotalBonificaciones());

        VisitorReporte visitorReporte = new VisitorReporte();
        estructura.aplicar(visitorReporte);
        System.out.println("Reporte de promociones:\n" + 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 Java

La documentación de ElementVisitor muestra cómo la API de procesamiento de anotaciones usa Visitor para recorrer elementos del código fuente. Librerías como Jackson, Hibernate o procesadores de expresiones utilizan variantes para aplicar transformaciones y validaciones sobre estructuras complejas.

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.