Wiosenna WYPRZEDAŻ
Odwiedzający

Odwiedzający w języku Go

Odwiedzający to behawioralny wzorzec projektowy pozwalający dodawać nowe zachowanie istniejącej hierarchii klas bez zmiany kodu jej klas.

O tym, dlaczego Odwiedzającego nie można po prostu zastąpić przeciążaniem metod, przeczytasz w naszym artykule Odwiedzający i podwójna dyspozycja.

Przykład koncepcyjny

Wzorzec Odwiedzający pozwala dodawać nowe zachowania do struktury bez konieczności modyfikowania jej. Powiedzmy, że naszym zadaniem jest utrzymanie biblioteki zawierającej różne struktury figur geometrycznych:

  • Kwadrat
  • Okrąg
  • Trójkąt

Każda z powyższych struktur figur implementuje wspólny interfejs figury geometrycznej.

Jak tylko twoi współpracownicy zaczęli korzystać z biblioteki, napłynęła masa próśb o dodatkową funkcjonalność. Skupmy się na najprostszych: powiedzmy, że zespół poprosił o dodanie funkcjonalności getArea (pobierz pole) do struktur.

Jest wiele sposobów na rozwiązanie tego problemu.

Pierwszą możliwością, jaka przychodzi do głowy, jest dodanie metody getArea bezpośrednio do interfejsu figury geometrycznej, a następnie zaimplementowanie jej dla każdej z figur. Jest to najpopularniejsze podejście w przypadku tego typu sytuacji, ale wiąże się z pewnym kosztem. Jako osoba odpowiedzialna za utrzymanie biblioteki nie chcesz ryzykować zepsucia kodu za każdym razem gdy ktoś poprosi o jakieś nowe funkcje. Z drugiej strony, chcesz pozwolić innym zespołom rozszerzać twoją bibliotekę.

Drugą opcją jest umożliwienie zespołom samodzielnej implementacji potrzebnego zachowania. Nie jest to jednak zawsze możliwe, gdyż potrzebne zachowanie może być zależne od kodu prywatnego.

Trzecie rozwiązanie sugeruje zastosowanie wzorca Odwiedzający w połączeniu z powyższym rozwiązaniem. Zaczniemy od zdefiniowania interfejsu odwiedzającego:

type visitor interface {
   visitForSquare(square)
   visitForCircle(circle)
   visitForTriangle(triangle)
}

Funkcje visitForSquare(square), visitForCircle(circle), visitForTriangle(triangle) pozwolą dodać funkcjonalność do — odpowiednio — kwadratów, okręgów i trójkątów.

Pewnie zastanawiasz się, dlaczego nie możemy mieć tylko jednej metody visit(shape) dla wszystkich figur. Powodem jest brak możliwości przeciążania metod w języku Go, więc nie możemy mieć metod o takiej samej nazwie, różniących się tylko parametrem.

Kolejnym istotnym krokiem jest dodanie metody accept (przyjmij) do interfejsu figury.

func accept(v visitor)

Wszystkie struktury figur geometrycznych muszą definiować tę metodę w następujący sposób:

func (obj *square) accept(v visitor){
    v.visitForSquare(obj)
}

Ale zaraz, przecież nie chcieliśmy zmieniać istniejących struktur figur! Niestety — zastosowanie wzorca Odwiedzający nakłada na nas taki obowiązek. Na szczęście modyfikację trzeba wykonać tylko jednorazowo.

W przypadku dodawania innych funkcjonalności, jak getNumSides lub getMiddleCoordinates będziemy mogli stosować tę samą funkcję przyjmowania accept(v visitor) bez konieczności ponownego zmieniania struktur reprezentujących figury.

Podsumowując, struktury odpowiadające poszczególnym figurom trzeba zmodyfikować tylko raz, a wszystkie nowe funkcjonalności będzie można obsłużyć tą samą funkcją przyjmującą. Jeśli zespół poprosi o funkcjonalność getArea, możemy po prostu zdefiniować konkretną implementację interfejsu odwiedzającego i zaprogramować logikę obliczania w tej konkretnej implementacji.

shape.go: Element

package main

type Shape interface {
	getType() string
	accept(Visitor)
}

square.go: Konkretny element

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: Konkretny element

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: Konkretny element

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: Odwiedzający

package main

type Visitor interface {
	visitForSquare(*Square)
	visitForCircle(*Circle)
	visitForrectangle(*Rectangle)
}

areaCalculator.go: Konkretny odwiedzający

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: Konkretny odwiedzający

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: Kod klienta

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: Wynik działania

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
Na podstawie: Golang By Example

Odwiedzający w innych językach

Odwiedzający w języku C# Odwiedzający w języku C++ Odwiedzający w języku Java Odwiedzający w języku PHP Odwiedzający w języku Python Odwiedzający w języku Ruby Odwiedzający w języku Rust Odwiedzający w języku Swift Odwiedzający w języku TypeScript