Visitor y Double Dispatch
Veamos la siguiente jerarquía de clases de formas geométricas (ing. “shape”), atención al pseudocódigo:
El código funciona bien y la aplicación está produciendo. Pero un día decides crear una función de exportación. Resultaría extraño colocar el código de exportación en estas clases. De modo que, en lugar de añadir la exportación a todas las clases de esta jerarquía, decides crear una nueva clase, externa a la jerarquía, y colocar toda la lógica de exportación dentro de ella. La clase obtendrá métodos para exportar el estado público de cada objeto a cadenas XML:
El código tiene buen aspecto, pero vamos a probarlo:
¡Espera! ¿Por qué?
Piensa como un compilador
Nota: la siguiente información es válida para la mayoría de lenguajes modernos de programación orientada a objetos (Java, C#, PHP y otros).
Vinculación tardía/dinámica
Imagina que eres un compilador. Tienes que decidir cómo compilar el siguiente código:
Veamos... el método draw
definido en la clase Shape
. Espera un minuto, pero también hay cuatro subclases que sobrescriben este método. ¿Podemos estar seguros de las implementaciones que debemos invocar aquí? No lo parece. La única forma de saberlo con seguridad es ejecutando el programa y comprobando la clase de un objeto pasado al método. Lo único que sabemos con certeza es que el objeto tendrá la implementación del método draw
.
Por lo tanto, el código máquina resultante comprobará la clase del parámetro s
y tomará la implementación draw
de la clase adecuada.
Tal comprobación de un tipo dinámico se denomina vinculación tardía (o dinámica):
- Tardía porque vinculamos el objeto y su implementación después de la compilación, durante el tiempo de ejecución.
- Dinámica, porque puede que haya que vincular cada nuevo objeto a una implementación diferente.
Vinculación temprana/estática
Ahora vamos a “compilar” el siguiente código:
Todo queda claro con la segunda línea: la clase Exporter
no tiene un constructor, por lo que nos limitamos a instanciar un objeto. ¿Qué pasa con la invocación a export
? La clase Exporter
tiene cinco métodos con el mismo nombre, que se diferencian en los tipos de parámetros. ¿Cuál invocar? Parece que aquí también vamos a necesitar una vinculación dinámica.
Pero hay otro problema. ¿Qué sucede si hay una clase de forma que no tiene el método export
adecuado en la clase Exporter
? Por ejemplo, un objeto Elipse
. El compilador no puede garantizar que exista el método sobrecargado adecuado, en contraste con métodos sobrescritos. Surge una situación ambigua que un compilador no puede permitir.
Por lo tanto, los desarrolladores de compiladores utilizan una ruta segura y utilizan la vinculación temprana (o estática) para métodos sobrecargados:
- Temprana porque sucede durante el tiempo de compilación, antes de que se lance el programa.
- Estática porque no se puede alterar durante el tiempo de ejecución.
Regresemos a nuestro ejemplo. Sabemos con seguridad que el argumento entrante será de la jerarquía Shape
, ya sea de la clase Shape
o bien una de sus subclases. También sabemos que la clase Exporter
tiene una implementación básica de la exportación que soporta la clase Shape
: export(s: Shape)
.
Esa es la única implementación que puede vincularse de forma segura con un código dado sin provocar ambigüedad. Ese es el motivo por el que, si pasamos un objeto Rectángulo
a exportShape
, el exportador aún invocará un método export(s: Shape)
.
Double dispatch (envío doble)
El Double dispatch (envío doble) es un truco que permite el uso de la vinculación dinámica junto a métodos sobrecargados. Se hace así:
Epílogo
Aunque el patrón Visitor se basa en el principio del double dispatch, éste no es su principal propósito. Visitor te permite añadir operaciones “externas” a toda una jerarquía de clase sin cambiar el código existente de esas clases.