🎉 Ура! После трёх лет работы, я наконец выпустил английскую версию книги о паттернах! Вот она »
Посетитель

Посетитель на Ruby

Посетитель — это поведенческий паттерн, который позволяет добавить новую операцию для целой иерархии классов, не изменяя код этих классов.

Подробней о том, почему Посетитель нельзя заменить простой перегрузкой методов читайте в статье Посетитель и Double Dispatch.

Подробней о Посетителе

Особенности паттерна на Ruby

Сложность:

Популярность:

Применимость: Посетитель нечасто встречается в Ruby-коде из-за своей сложности и нюансов реализазации.

Концептуальный пример

Этот пример показывает структуру паттерна Посетитель, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.

main.rb: Пример структуры паттерна

# Интерфейс Компонента объявляет метод accept, который в качестве аргумента
# может получать любой объект, реализующий интерфейс посетителя.
class Component
  # @abstract
  #
  # @param [Visitor] visitor
  def accept(_visitor)
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end
end

# Каждый Конкретный Компонент должен реализовать метод accept таким образом,
# чтобы он вызывал метод посетителя, соответствующий классу компонента.
class ConcreteComponentA < Component
  # Обратите внимание, мы вызываем visitConcreteComponentA, что соответствует
  # названию текущего класса. Таким образом мы позволяем посетителю узнать, с
  # каким классом компонента он работает.
  #
  # @param [Visitor] visitor
  def accept(visitor)
    visitor.visit_concrete_component_a(self)
  end

  # Конкретные Компоненты могут иметь особые методы, не объявленные в их базовом
  # классе или интерфейсе. Посетитель всё же может использовать эти методы,
  # поскольку он знает о конкретном классе компонента.
  def exclusive_method_of_concrete_component_a
    'A'
  end
end

# То же самое здесь: visit_concrete_component_b => ConcreteComponentB
class ConcreteComponentB < Component
  # @param [Visitor] visitor
  def accept(visitor)
    visitor.visit_concrete_component_b(self)
  end

  def special_method_of_concrete_component_b
    'B'
  end
end

# Интерфейс Посетителя объявляет набор методов посещения, соответствующих
# классам компонентов. Сигнатура метода посещения позволяет посетителю
# определить конкретный класс компонента, с которым он имеет дело.
#
# @abstract
class Visitor
  # @abstract
  #
  # @param [ConcreteComponentA] element
  def visit_concrete_component_a(_element)
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end

  # @abstract
  #
  # @param [ConcreteComponentB] element
  def visit_concrete_component_b(_element)
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end
end

# Конкретные Посетители реализуют несколько версий одного и того же алгоритма,
# которые могут работать со всеми классами конкретных компонентов.
#
# Максимальную выгоду от паттерна Посетитель вы почувствуете, используя его со
# сложной структурой объектов, такой как дерево Компоновщика. В этом случае было
# бы полезно хранить некоторое промежуточное состояние алгоритма при выполнении
# методов посетителя над различными объектами структуры.
class ConcreteVisitor1 < Visitor
  def visit_concrete_component_a(element)
    puts "#{element.exclusive_method_of_concrete_component_a} + #{self.class}"
  end

  def visit_concrete_component_b(element)
    puts "#{element.special_method_of_concrete_component_b} + #{self.class}"
  end
end

class ConcreteVisitor2 < Visitor
  def visit_concrete_component_a(element)
    puts "#{element.exclusive_method_of_concrete_component_a} + #{self.class}"
  end

  def visit_concrete_component_b(element)
    puts "#{element.special_method_of_concrete_component_b} + #{self.class}"
  end
end

# Клиентский код может выполнять операции посетителя над любым набором
# элементов, не выясняя их конкретных классов. Операция принятия направляет
# вызов к соответствующей операции в объекте посетителя.
#
# @param [Array<Component>] components
# @param [Visitor] visitor
def client_code(components, visitor)
  # ...
  components.each do |component|
    component.accept(visitor)
  end
  # ...
end

components = [ConcreteComponentA.new, ConcreteComponentB.new]

puts 'The client code works with all visitors via the base Visitor interface:'
visitor1 = ConcreteVisitor1.new
client_code(components, visitor1)

puts 'It allows the same client code to work with different types of visitors:'
visitor2 = ConcreteVisitor2.new
client_code(components, visitor2)

output.txt: Результат выполнения

The client code works with all visitors via the base Visitor interface:
A + ConcreteVisitor1
B + ConcreteVisitor1
It allows the same client code to work with different types of visitors:
A + ConcreteVisitor2
B + ConcreteVisitor2

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

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