Весняний РОЗПРОДАЖ
Відвідувач

Відвідувач на Go

Відвідувач — це поведінковий патерн, який дозволяє додати нову операцію для цілої ієрархії класів, не змінюючи код цих класів.

Детальніше про те, чому Відвідувач не можна замінити звичайним перевантаженням методів читайте в статті Відвідувач і Double Dispatch.

Концептуальний приклад

Патерн Відвідувач дає змогу вам додавати поведінку в структуру без її зміни. Уявімо, що ви розробник бібліотеки, яка містить структури різних фігур:

  • Квадрат
  • Коло
  • Трикутник

Структури кожної з вищеназваних фігур реалізують спільний інтерфейс фігури.

Як тільки співробітники у вашій компанії почали використовувати вашу чудову бібліотеку, вас засипали проханнями додати той чи інший функціонал. Розглянемо один з найпростіших варіантів: команда попросила вас додати в структуру функцію getArea, що повертає площу фігури.

Є багато рішень цієї проблеми.

Перше, що спадає на думку — додати метод getArea безпосередньо в інтерфейс фігури, і потім реалізувати його у кожній структурі. Це здається найбільш правильним рішенням, але у нього є свої мінуси. Як розробник бібліотеки, ви ризикуєте зламати ваш дорогоцінний код щоразу, коли додаєте нову поведінку. Незважаючи на це, ви хочете дати іншим командам можливість якимось чином розширювати вашу бібліотеку.

Другий варіант — команда, яка запитує додавання нової функції, може сама реалізувати її в своєму проекті. Однак, це не завжди є можливим, оскільки новий функціонал може покладатися на прихований код.

Третій можливий варіант — вирішити вищезгадану проблему завдяки використанню патерна Відвідувач. Спершу ми визначаємо інтерфейс користувача в такий спосіб:

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

Функції visitForSquare(square), visitForCircle(circle), visitForTriangle(triangle) дозволять нам додавати функціонал для квадратів, кіл і трикутників відповідно.

Не розумієте, чому ми не можемо залишити лише один метод visit(shape) в інтерфейсі користувача? Це неможливо через те, що мова Go не підтримує перевантаження методів, тому ви не можете мати методи з однаковими іменами, але різними параметрами.

Другий, не менш важливий, етап — додавання методу accept в інтерфейс фігури.

func accept(v visitor)

Всі структури фігур мусять визначати цей метод схожим способом:

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

Стривайте, хіба я трохи раніше не заявляв, що не хочу змінювати наявні структури фігур? На жаль, під час використання патерну Відвідувача нам доведеться вносити зміни в структури, але лише один раз.

У разі додавання іншого функціоналу, наприклад getNumSides або getMiddleCoordinates, ми будемо використовувати все той же метод accept(v visitor) без нових змін у структурах фігур.

В кінцевому підсумку, структури потрібно змінити лише один раз, і всі майбутні запити нового функціоналу можна буде реалізувати завдяки функції accept(v visitor). Якщо команда дасть запит на поведінку getArea, ми можемо просто визначити явну реалізацію інтерфейсу користувача і прописати логіку обчислення площі в цій конкретній імплементації.

shape.go: Елемент

package main

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

square.go: Конкретний елемент

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: Конкретний елемент

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: Конкретний елемент

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: Відвідувач

package main

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

areaCalculator.go: Конкретний відвідувач

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: Конкретний відвідувач

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: Клієнтський код

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: Результат виконання

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
На основі: Golang By Example

Відвідувач іншими мовами програмування

Відвідувач на C# Відвідувач на C++ Відвідувач на Java Відвідувач на PHP Відвідувач на Python Відвідувач на Ruby Відвідувач на Rust Відвідувач на Swift Відвідувач на TypeScript