Visitor design pattern in Ruby

According to GoF and their great book “Design Patterns: Elements of Reusable Object-Oriented Software”, visitor pattern:

Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.

Let’s explain that on some examples. So, we have a Product and an Order :

class Product
  attr_reader :name, :price
def initialize(name:, price:)
@name = name
@price = price
end
end

Product has two attributes that represent its name and price.

class Order
  def initialize
@products = []
end
  def add_product(product)
@products << product
end
end

Order contains an array of product and exposes simple method to add new product to the list.

The problem: because products, that are a part of the order, are stored in internal and not available outside representation, if we want to define any operation on products we need to know how they are stored in the order. We can either expose products outside the Order class, which is not good, or define each operation in Order class which will quickly pollut that class.

To avoid this, we can use Visitor pattern that will allow us to define new operations without knowing anything about how products are stored inside Order class and without changing Product class.

Step I (Product and Ordercan accept visitor. )

You can think about visitor as an operation that is defined outside. First step will be to enable Product class to accept visitor. We gonna create simple module/interface for it.

module Visitable
def accept(visitor)
visitor.visit(self)
end
end

Now, let’s add it to Order and Product :

class Product
include Visitable
  attr_reader :name, :price
def initialize(name:, price:)
@name = name
@price = price
end
end
class Order
include Visitable
  def initialize
@products = []
end
  def add_product(product)
@products << product
end
  def accept(visitor)
@products.each do |product|
product.accept(visitor)
end
end
end

As you can see, we overwritten accept(visior) in Order so it iterate over products and run that method on each product.

Step II (Visitor interface)

First, let’s build root class for visitors. It will be very simple class with just one method visit(subject) :

class Visitor
def visit(subject)
raise NotImpelementedError.new
end
end

Step III (Concrete visitors)

In that step we will build concrete visitors/operations that can do something. So let’s start with printer that presents our order:

class ProductsPrinterVisitor < Visitor
def visit(subject)
puts "Product: #{subject.name} - $#{subject.price}"
end
end

or an operation that shows how the order will look like after applying 50% discount:

class HalfPriceSimulatorVisitor < Visitor
def visit(subject)
puts "Product: #{subject.name} - after 50% discount: $#{subject.price / 2.0}"
end
end

Now, we have all classes to play with that solution.
Let’s create few products and an order:

p1 = Product.new(name: 'Laptop', price: 1000)
p2 = Product.new(name: 'Beer', price: 5)
order = Order.new
order.add_product(p1)
order.add_product(p2)

Let’s apply some operations to the order (let’s visit each product of the order with a logic implemented in the operation):

order.accept(ProductsPrinterVisitor.new)
order.accept(HalfPriceSimulatorVisitor.new)

We gonna see:

Product: Laptop - $1000
Product: Beer - $5
Product: Laptop - after 50% discount: $500.0
Product: Beer - after 50% discount: $2.5

We can define operations in a separate classes and we can visit the order with that operation. Order will apply that operation to every product from the list. That way we don’t have to expose internal representation of products and we don’t have to extend Order or Product if we wan’t to add new operation.


Probably that is not well known and used design pattern, but you can spot it from time to time. For example in Rails/Arel — https://github.com/rails/arel/blob/master/lib/arel/visitors/visitor.rb


You can find working code used in that blog post here — https://gist.github.com/kkempin/45764d94f587f9dea4ffefd53b463947


I’ve recorder a video of live coding implementing that pattern —