Нужна клёвая книга о паттернах на русском? Вот она »
Посетитель

Посетитель на 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

Посетитель на других языках программирования

Посетитель на Java Посетитель на C# Посетитель на C++ Посетитель на PHP Посетитель на Python Посетитель на Ruby Посетитель на Swift Посетитель на TypeScript