![Visitor](/images/patterns/cards/visitor-mini.png?id=854a35a62963bec1d75eab996918989b)
Visitor en Go
Visitor es un patrón de diseño de comportamiento que permite añadir nuevos comportamientos a una jerarquía de clases existente sin alterar el código.
Lee por qué el patrón Visitor no puede simplemente sustituirse por la sobrecarga de métodos, en nuestro artículo Visitor y envío doble.
Ejemplo conceptual
El patrón Visitor permite añadir comportamientos a una estructura sin modificar dicha estructura. Digamos que tienes encargado mantener una biblioteca que tiene distintas estructuras de forma, como:
- Cuadrado
- Círculo
- Triángulo
Cada una de estas estructuras de forma implementa la interfaz común de forma.
Una vez que la gente de tu empresa empieza a utilizar tu estupenda biblioteca, te llueven las solicitudes de funciones. Vamos a revisar algunas de las más simples: un equipo te pide que añadas el comportamiento getArea
a las estructuras de forma.
Existen muchas opciones para resolver este problema.
La primera opción que viene a la mente es añadir el método getArea
directamente a la interfaz de forma e implementarla después en cada estructura de forma. Ésta parece una solución a la que recurrir, pero tiene un precio. Como encargado de mantener la biblioteca, no puedes arriesgarte a descomponer tu precioso código cada vez que alguien pide un nuevo comportamiento. Sin embargo, quieres que otros equipos amplíen también tu biblioteca.
La segunda opción es que el equipo que solicita la función pueda implementar el comportamiento por sí mismo. No obstante, esto no siempre es posible, ya que el comportamiento dependerá del código privado.
La tercera opción es resolver el problema anterior utilizando el patrón Visitor. Comenzamos definiendo una interfaz visitante, así:
type visitor interface { visitForSquare(square) visitForCircle(circle) visitForTriangle(triangle) }
Las funciones visitForSquare(square)
, visitForCircle(circle)
, visitForTriangle(triangle)
nos permitirán añadir funcionalidades a cuadrados, círculos y triángulos respectivamente.
¿Te preguntas por qué no podemos tener un único método visit(shape)
en la interfaz visitante? La razón es que el lenguaje Go no soporta la sobrecarga de métodos, por lo que no puedes tener métodos con el mismo nombre pero distintos parámetros.
Ahora, la segunda parte importante es añadir el método accept
a la interfaz de forma.
func accept(v visitor)
Toda la estructura de forma tiene que definir este método, de forma parecida a ésta:
func (obj *square) accept(v visitor){ v.visitForSquare(obj) }
Espera un momento, ¿no acabo de decir que no queremos modificar nuestras estructuras de forma existentes? Lamentablemente, sí, cuando utilizamos el patrón Visitor, tenemos que alterar nuestras estructuras de forma. Pero esta modificación se realizará solo una vez.
En caso de añadir otros comportamientos, como getNumSides
, getMiddleCoordinates
, utilizaremos la misma función accept(v visitor)
sin realizar más cambios en las estructuras de forma.
Al final, las estructuras de forma solo tienen que modificarse una vez, y todas las futuras solicitudes de comportamientos podrán gestionarse utilizando la misma función accept. Si el equipo solicita el comportamiento getArea
, podemos limitarnos a definir la implementación concreta de la interfaz visitante y escribir la lógica de cálculo del área en esa implementación concreta.
shape.go: Elemento
package main
type Shape interface {
getType() string
accept(Visitor)
}
square.go: Elemento concreta
package main
type Square struct {
side int
}
func (s *Square) accept(v Visitor) {
v.visitForSquare(s)
}
func (s *Square) getType() string {
return "Square"
}
circle.go: Elemento concreta
package main
type Circle struct {
radius int
}
func (c *Circle) accept(v Visitor) {
v.visitForCircle(c)
}
func (c *Circle) getType() string {
return "Circle"
}
rectangle.go: Elemento concreta
package main
type Rectangle struct {
l int
b int
}
func (t *Rectangle) accept(v Visitor) {
v.visitForrectangle(t)
}
func (t *Rectangle) getType() string {
return "rectangle"
}
visitor.go: Visitante
package main
type Visitor interface {
visitForSquare(*Square)
visitForCircle(*Circle)
visitForrectangle(*Rectangle)
}
areaCalculator.go: Visitante concreta
package main
import (
"fmt"
)
type AreaCalculator struct {
area int
}
func (a *AreaCalculator) visitForSquare(s *Square) {
// Calculate area for square.
// Then assign in to the area instance variable.
fmt.Println("Calculating area for square")
}
func (a *AreaCalculator) visitForCircle(s *Circle) {
fmt.Println("Calculating area for circle")
}
func (a *AreaCalculator) visitForrectangle(s *Rectangle) {
fmt.Println("Calculating area for rectangle")
}
middleCoordinates.go: Visitante concreta
package main
import "fmt"
type MiddleCoordinates struct {
x int
y int
}
func (a *MiddleCoordinates) visitForSquare(s *Square) {
// Calculate middle point coordinates for square.
// Then assign in to the x and y instance variable.
fmt.Println("Calculating middle point coordinates for square")
}
func (a *MiddleCoordinates) visitForCircle(c *Circle) {
fmt.Println("Calculating middle point coordinates for circle")
}
func (a *MiddleCoordinates) visitForrectangle(t *Rectangle) {
fmt.Println("Calculating middle point coordinates for rectangle")
}
main.go: Código cliente
package main
import "fmt"
func main() {
square := &Square{side: 2}
circle := &Circle{radius: 3}
rectangle := &Rectangle{l: 2, b: 3}
areaCalculator := &AreaCalculator{}
square.accept(areaCalculator)
circle.accept(areaCalculator)
rectangle.accept(areaCalculator)
fmt.Println()
middleCoordinates := &MiddleCoordinates{}
square.accept(middleCoordinates)
circle.accept(middleCoordinates)
rectangle.accept(middleCoordinates)
}
output.txt: Resultado de la ejecución
Calculating area for square
Calculating area for circle
Calculating area for rectangle
Calculating middle point coordinates for square
Calculating middle point coordinates for circle
Calculating middle point coordinates for rectangle