Diseño de software

Patrón Visitor en Swift: cuándo se necesita el doble despacho

En el desarrollo para iOS, tarde o temprano hay que seguir añadiendo operaciones a objetos de varios tipos, como figuras o nodos.

4 min de lectura
Imagen de portada de Patrón Visitor en Swift: cuándo se necesita el doble despacho

En el desarrollo para iOS, tarde o temprano hay que seguir añadiendo operaciones a objetos de varios tipos, como figuras o nodos.

Es habitual inspeccionar los tipos uno a uno con if let y as?. Cada nuevo tipo obliga a volver a modificar las ramas.

Este artículo explica, con ejemplos, el patrón Visitor de Swift y cuándo se necesita exactamente su núcleo: el doble despacho.

Al elegir un método, Swift solo considera el tipo dinámico del receptor, el objeto que recibe el mensaje.

Cuando cambian a la vez «qué figura» y «qué operación», el despacho único no basta. El doble despacho determina ambos tipos en tiempo de ejecución mediante dos llamadas.


Despacho de métodos en Swift: ¿por qué uno solo no basta?

Al llamar a shape.draw() en Swift, se ejecuta shape que corresponde al tipo real de draw().

Esto es despacho único: el método se elige usando solo el tipo del receptor, ****.

El problema aparece cuando hay varias operaciones.

Las figuras son círculos, rectángulos y triángulos; las operaciones, dibujar, calcular el área y exportar JSON. Al combinar ambos ejes, las combinaciones se multiplican.

Por eso es habitual escribir código como este.

// Crece cada vez que se añaden operaciones para una figura if-else
func export(_ shape: Shape) -> String {
    if let c = shape as? Circle { return drawCircle(c) }
    if let r = shape as? Rect   { return drawRect(r) }
    return "" // Hay que modificar esta parte cada vez que aparece una figura nueva
}

¿Añades una figura nueva? Tienes que abrir esta función e insertar otra rama.

Con cinco operaciones, es como tener cinco funciones de este tipo.

Cuando los if-else se acumulan así, lo considero una señal.
Cuando los if-else se acumulan así, lo considero una señal.

¿Cuándo se necesita el doble despacho?

Mi criterio es sencillo.

Cuando el comportamiento de un método depende simultáneamente de dos tipos, ahí se necesita el doble despacho.

El ejemplo anterior es exactamente eso: el código depende tanto del tipo de figura como del tipo de operación.

El despacho predeterminado de Swift solo mira el tipo del receptor, así que el otro eje se gestiona manualmente con comprobaciones como as?.

Esa ramificación manual es un code smell.

El doble despacho deja incluso la decisión del segundo tipo en manos de la sobrecarga del lenguaje, sin inspeccionarlo directamente.

En cambio, si solo hay una operación o los tipos no van a crecer, no hace falta este patrón.


¿Cómo se implementa el patrón Visitor en Swift?

La clave es dividir la llamada en dos pasos. Por eso se llama doble despacho.

protocol Shape { func accept(_ v: Visitor) -> String }
struct Circle: Shape { func accept(_ v: Visitor) -> String { v.visit(self) } }
struct Rect: Shape   { func accept(_ v: Visitor) -> String { v.visit(self) } }

protocol Visitor {
    func visit(_ c: Circle) -> String   // Aquí se determina el tipo de figura
    func visit(_ r: Rect) -> String      // La operación se determina mediante sobrecarga
}

Sigamos el flujo.

En la primera llamada, shape.accept(v), se determina el tipo real de la figura. En tiempo de ejecución se elige si llamar a Circle o a accept de Rect.

En la segunda llamada, v.visit(self), el tipo estático de self ya está fijado como Circle o Rect. Por eso se selecciona correctamente visit sobrecargado.

Las dos llamadas se superponen y determinan con precisión ambos tipos.

El punto donde se superponen dos llamadas: doble despacho
El punto donde se superponen dos llamadas: doble despacho

Para añadir una operación, basta con crear una estructura nueva que adopte Visitor. El código existente de las figuras no se toca.


Patrón Visitor: ¿usarlo o no? Comparación de ventajas y desventajas

No existe un patrón perfecto. Veamos sus compensaciones.

Categoría Ramificación por tipo (as? / switch) Patrón Visitor
Añadir una operación Modificar ramas en varios lugares Añadir un Visitor nuevo
Añadir un tipo Añadir una rama Modificar todos los Visitor
Legibilidad del código Se complica al crecer las ramas Separación clara por operación
Coste inicial de implementación Bajo Relativamente alto

Como muestra la tabla, el patrón Visitor destaca cuando las operaciones crecen con frecuencia y los tipos son estables.

Si los tipos (figuras) siguen aumentando, da más trabajo, porque hay que modificar todos los Visitor.

accept y visit: solo dos pasos
accept y visit: solo dos pasos

Preguntas frecuentes (Q&A)

P. Swift tiene genéricos. ¿Sigue siendo necesario Visitor?

Los genéricos son potentes cuando el tipo se conoce en compilación. Pero si en tiempo de ejecución hay tipos reales mezclados, como en un array [Shape], el doble despacho sigue siendo útil.

P. ¿No se puede hacer también con enum y switch?

Sí. Si los tipos son fijos, enum + switch suele ser más sencillo. Visitor encaja mejor cuando las operaciones siguen creciendo.


Cuando un comportamiento depende simultáneamente de dos tipos, es el momento de pensar en el doble despacho y el patrón Visitor de Swift.

Si tienes código donde as?las ramas no dejan de crecer, prueba a refactorizarlo con el ejemplo de hoy. Será mucho más fácil de mantener 🙂

Artículos relacionados