В этой статье я объясню концепцию шаблона проектирования «Посетитель», цели, плюсы и минусы, сценарии и способы реализации, а также предоставлю два экземпляра и модульные тесты.

Концепция

Шаблон посетителя — это шаблон проектирования поведения, который позволяет отделить алгоритмы от объектов, с которыми они работают. Это позволяет добавлять новые операции без изменения существующей структуры объекта.

Шаблон посетителя включает четыре основных компонента:

  1. Интерфейс посетителя: интерфейс, объявляющий операцию посещения для каждого типа конкретного элемента в структуре объекта.
  2. Интерфейс элемента: интерфейс, представляющий элементы, принимающие посетителей. Он определяет метод accept, который принимает посетителя в качестве параметра.
  3. Конкретная структура элемента. Структура реализует интерфейс элемента, реализуя метод accept.
  4. Конкретная структура посетителя. Структура реализует интерфейс посетителя, содержащий конкретную реализацию каждой операции посещения.

Цели

Цели шаблона посетителя:

  1. Отделяйте алгоритмы от объектов, с которыми они работают.
  2. Добавляйте новые операции в существующие структуры, не изменяя их.
  3. Поощряйте повторное использование кода и ремонтопригодность.

За и против

Преимущества использования шаблона посетителя:

  1. Поощряет принцип единой ответственности.
  2. Упрощает обслуживание кода, сохраняя связанное поведение вместе.
  3. Позволяет добавлять новые поведения без изменения существующей структуры.
  4. Позволяет расширить функциональность struct.

Недостатки использования шаблона интерпретатора:

  1. Может потребоваться изменить интерфейс посетителя и все реализации, если изменится иерархия элементов.
  2. Приводить к повышенной сложности, если количество элементов или посетителей увеличивается.
  3. Нарушает «принцип открытости/закрытости», поскольку новая функциональность требует изменения интерфейса посетителя.

Сценарии

Шаблон Посетитель особенно полезен, когда у вас есть сложная иерархия объектов и вы хотите выполнять различные операции с объектами, не изменяя их структуры. Вот несколько реальных сценариев, в которых паттерн «Посетитель» можно применить в Golang:

1. Обработка документов

При работе со сложным форматом документа (например, HTML или XML) вы можете использовать шаблон Посетитель для обхода дерева документа и выполнения операций рендеринга, проверки или преобразования.

2. Иерархия игровых объектов

В игровом движке у вас может быть иерархия игровых объектов, таких как предметы, персонажи и декорации. Шаблон Visitor можно использовать для реализации таких операций, как рендеринг, симуляция физики, поведение ИИ или обнаружение столкновений, без изменения структур игровых объектов.

3. Иерархия виджетов пользовательского интерфейса

В библиотеке пользовательского интерфейса у вас может быть иерархия виджетов пользовательского интерфейса (например, кнопки, текстовые поля и контейнеры). Шаблон Посетитель можно использовать для реализации таких операций, как рендеринг, обработка событий или расчет макета, без изменения структур виджета.

4. Алгоритмы обхода графа

Если у вас есть структура данных графа, шаблон Посетитель можно использовать для реализации различных алгоритмов обхода графа, таких как поиск в глубину, поиск в ширину или алгоритм поиска кратчайшего пути Дейкстры, без изменения структур узлов графа.

5. Сериализация/десериализация данных

При работе с данными в разных форматах (например, JSON, XML или двоичном) вы можете использовать шаблон посетителя для реализации логики сериализации и десериализации для каждого формата данных без изменения самих структур данных.

Помните, что шаблон посетителя можно использовать для решения этих проблем, но это не всегда лучшее решение. Возможно, более эффективно использовать специальные библиотеки или инструменты синтаксического анализа или другие шаблоны проектирования для сложных языков.

Как реализовать

Ниже приведены шаги по реализации шаблона посетителя в Go.

  1. Определите интерфейс посетителя.
  2. Определите интерфейс Element, который содержит метод Accept().
  3. Создайте конкретные элементы структуры, реализующие интерфейс Element.
  4. Создайте конкретные структуры посетителя для реализации интерфейса посетителя.
  5. Вызовите шаблон посетителя в своем коде.

Первый случай

Я приведу простой пример пошаговой реализации шаблона посетителя.

Следующий код является содержимым файла с именем visitor.go.

package sample

// step 1
type Visitor interface {
   VisitConcreteElementA(*ConcreteElementA) string
   VisitConcreteElementB(*ConcreteElementB) string
}

// step 2
type Element interface {
   Accept(Visitor) string
}

// step 3
type ConcreteElementA struct {
   Value string
}

func (e *ConcreteElementA) Accept(v Visitor) string {
   return v.VisitConcreteElementA(e)
}

type ConcreteElementB struct {
   Value string
}

func (e *ConcreteElementB) Accept(v Visitor) string {
   return v.VisitConcreteElementB(e)
}

// step 4
type ConcreteVisitor1 struct{}

func (v *ConcreteVisitor1) VisitConcreteElementA(e *ConcreteElementA) string {
   return e.Value
}

func (v *ConcreteVisitor1) VisitConcreteElementB(e *ConcreteElementB) string {
   return e.Value
}

type ConcreteVisitor2 struct{}

func (v *ConcreteVisitor2) VisitConcreteElementA(e *ConcreteElementA) string {
   return e.Value
}

func (v *ConcreteVisitor2) VisitConcreteElementB(e *ConcreteElementB) string {
   return e.Value
}

В приведенном выше коде определите интерфейс посетителя с методами для каждой конкретной структуры элемента. Определите интерфейс Element, который содержит метод Accept(). Метод Accept() используется для приема большого количества посетителей и не требует изменения исходного интерфейса. ConcreteElementA и ConcreteElementB — это структуры, используемые для реализации интерфейса Element. ConcreteVisitor1 и ConcreteVisitor2 — это структуры, используемые для реализации интерфейса посетителя.

Приведенный ниже код является содержимым файла с именем visitor_test.go.

package sample

import (
   "testing"

   "github.com/go-playground/assert/v2"
)

func TestVisitor(t *testing.T) {
   var value1 = "Test1"
   var value2 = "Test2"

   elementA := &ConcreteElementA{Value: value1}
   elementB := &ConcreteElementB{Value: value2}

   visitor1 := &ConcreteVisitor1{}
   visitor2 := &ConcreteVisitor2{}

   expectedValue1 := "Test1"
   expectedValue2 := "Test2"

   t.Run("Test VisitConcreteElementA with ConcreteVisitor1", func(t *testing.T) {
      assert.Equal(t, elementA.Accept(visitor1), expectedValue1)
   })

   t.Run("Test VisitConcreteElementB with ConcreteVisitor1", func(t *testing.T) {
      assert.Equal(t, elementB.Accept(visitor1), expectedValue2)
   })

   t.Run("Test VisitConcreteElementA with ConcreteVisitor2", func(t *testing.T) {
      assert.Equal(t, elementA.Accept(visitor2), expectedValue1)
   })

   t.Run("Test VisitConcreteElementB with ConcreteVisitor2", func(t *testing.T) {
      assert.Equal(t, elementB.Accept(visitor2), expectedValue2)
   })
}

Приведенный выше код содержит тесты VisitConcreteElementA и VisitConcreteElementB методов ConcreteVisitor1 и ConcreteVisitor2. Скриншот результатов теста ниже.

Второй экземпляр

Я создам простую систему покупок с двумя типами предметов: книги и электроника. Используя шаблон «Посетитель», мы будем применять разные скидки в зависимости от типа товара.

Приведенный ниже код является содержимым файла с именем visitor.go.

package shop

type Item interface {
   Accept(visitor DiscountVisitor) float64
}

type DiscountVisitor interface {
   VisitBook(book *Book) float64
   VisitElectronic(electronic *Electronic) float64
}

type Book struct {
   price float64
}

func (b *Book) Accept(visitor DiscountVisitor) float64 {
   return visitor.VisitBook(b)
}

type Electronic struct {
   price float64
}

func (e *Electronic) Accept(visitor DiscountVisitor) float64 {
   return visitor.VisitElectronic(e)
}

type SeasonalDiscountVisitor struct {
   bookDiscount       float64
   electronicDiscount float64
}

func (v *SeasonalDiscountVisitor) VisitBook(book *Book) float64 {
   return book.price * (1 - v.bookDiscount)
}

func (v *SeasonalDiscountVisitor) VisitElectronic(electronic *Electronic) float64 {
   return electronic.price * (1 - v.electronicDiscount)
}

В приведенном выше коде Item является интерфейсом Element. DiscountVisitor — это интерфейс посетителя. Метод Accept используется для получения DiscountVisitor. Book и Electronic — это конкретные структуры Item. SeasonalDiscountVisitor — это конкретная структура интерфейса DiscountVisitor.

Приведенный ниже код является содержимым файла с именем visitor_test.go.

package shop

import (
   "testing"
)

func TestDiscounts(t *testing.T) {
   book := &Book{price: 100}
   electronic := &Electronic{price: 200}

   seasonalDiscount := &SeasonalDiscountVisitor{
      bookDiscount:       0.1,
      electronicDiscount: 0.2,
   }

   t.Run("Test Book price after discount", func(t *testing.T) {
      discountedBookPrice := book.Accept(seasonalDiscount)
      expectedBookPrice := 100 * (1 - 0.1)
      if discountedBookPrice != expectedBookPrice {
         t.Errorf("Book price after discount is incorrect, expected: %.2f, got: %.2f", expectedBookPrice, discountedBookPrice)
      }
   })

   t.Run("Test Electronic price after discount is incorrect", func(t *testing.T) {
      discountedElectronicPrice := electronic.Accept(seasonalDiscount)
      expectedElectronicPrice := 200 * (1 - 0.2)
      if discountedElectronicPrice != expectedElectronicPrice {
         t.Errorf("Electronic price after discount is incorrect, expected: %.2f, got: %.2f", expectedElectronicPrice, discountedElectronicPrice)
      }
   })
}

В приведенном выше коде я использую способ подтеста для проверки скидки на цену книги и скидки на электронную цену. Модульные тесты гарантируют правильность применения скидок. Скриншот результатов теста выглядит следующим образом.

Заключение

Шаблон «Посетитель» — это мощный и гибкий шаблон проектирования поведения, который помогает отделить алгоритмы от объектов, с которыми они работают, позволяя добавлять новые варианты поведения без изменения существующих иерархий структур. Определив интерфейс посетителя с методами для каждого типа элемента и заставив элементы принимать посетителя, вы можете выполнять различные операции с элементами без изменения их структур.

Шаблон посетителя лучше всего подходит для ситуаций, когда вам нужно выполнить несколько несвязанных операций со структурой объекта или когда вы хотите избежать изменения существующих структур при добавлении новых операций.

Вернитесь к шаблонам поведенческого проектирования и нажмите здесь.

Чтобы просмотреть шаблоны креативного дизайна в Golang, нажмите здесь.

Чтобы просмотреть шаблоны структурного проектирования в Golang, нажмите здесь.

Спасибо, что читаете. Если вам понравилась моя статья, хлопайте в ладоши и подписывайтесь на меня. Я с удовольствием отвечу на все ваши вопросы, если вы спросите меня в комментарии. Нажмите на следующую ссылку, чтобы стать средним участником.

Нажмите, чтобы стать средним участником и читать неограниченное количество историй!

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 💰 Бесплатный курс собеседования по программированию ⇒ Просмотреть курс
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу