REBAJA de primavera
Visitor

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

Visitor en otros lenguajes

Visitor en C# Visitor en C++ Visitor en Java Visitor en PHP Visitor en Python Visitor en Ruby Visitor en Rust Visitor en Swift Visitor en TypeScript