personVisitor

Description

Visitor is a behavioral design pattern that allows you to add new behaviors without changing existing code.

Problem

We will create a book ordering system that will also allow us to add special discounts.
First, we will create our Book, which will have a title and a price.

class Book
  attr_accessor :title, :price

  def initialize(title:, price:)
    @title = title
    @price = price
  end
end
The next step will be to create an order, which will allow us to add books to the order and display the order total.

class Order
  def initialize
    @books = []
  end

  def add_book(book)
    @books << book
  end

  def total
    @books.sum(&:price)
  end
end
The first thing we need to do to implement the Visitor pattern is to create a Visitable module.

module Visitable
  def accept(visitor)
    visitor.visit(self)
  end
end
Then we need to attach it to Book class.

class Book
  include Visitable

  attr_accessor :title, :price

  def initialize(title:, price:)
    @title = title
    @price = price
  end
end
In the Order class we also create an accept method that works on our list of books.

class Order
  def initialize
    @books = []
  end
  def add_book(book)
    @books<< book
  end

  def total
    @books.sum(&:price)
  end

  def accept(visitor)
    @books.each do |book|
      book.accept(visitor)
    end
  end
end
At this point we can create our base Visitor class that prepare a common interface for new visitors.

class Visitor
  def visit(object)
    raise NoMethodError
  end
end
And now we are able to create our implementation of a specific that will be responsible for reducing the prices by half.

class DiscountVisitor < Visitor
  def visit(object)
    object.price /= 2
  end
end
We can now use Visitor pattern in real code

# Create two books
hobbit = Book.new(title: 'The Hobbit', price: 32)
harry_potter = Book.new(title: "Harry Potter and the Philosopher's Stone", price: 28)

# Create an order
order = Order.new

# Add books to the order
order.add_book(hobbit)
order.add_book(harry_potter)

# Order returns a sum of books prices 
order.total # => 60

# Attach discount to the book
order.accept(DiscountVisitor.new)

# Adding a discount cuts prices in half
order.total # => 30
The pattern prepared in this way allows us to add new code without interfering with the base code.
However in Ruby code, this pattern is rarely seen because many of the functions of this pattern can be replaced with a simple block.