Visitor Design Pattern in Ruby

1. Definition

The Visitor Design Pattern involves adding further operations to objects without having to modify them. It's about adding functionality externally to classes. Visitors represent a new operation to be performed on the elements of an object structure.

2. Problem Statement

Imagine you have a collection of objects and you need to add new operations or functions to these objects. Modifying each object's class is intrusive, breaks the open/closed principle, and is not always feasible especially if they come from a library or you don't have access to the source code.

3. Solution

Instead of adding the required operations to the objects themselves, a separate visitor class is created that performs the operation. The original object collection must be equipped to accept a visitor and let it operate on them.

4. Real-World Use Cases

1. Rendering graphical objects in multiple formats.

2. Syntax tree representation in compilers: Each node can be "visited" to generate assembly or machine code.

3. Applying promotions or discounts to a group of products in a shopping cart.

5. Implementation Steps

1. Define an interface (Visitor) that declares a visit operation for each type of concrete element in the object structure.

2. Concrete classes that implement this Visitor interface define the operations.

3. The Element interface has a method accepting a visitor.

4. Concrete elements implement this method.

5. The client creates Visitor objects and passes them into elements via the 'accept' method.

6. Implementation in Ruby

# Step 1, 3 & 4: Element Interface and Concrete Elements
class Element
  def accept(visitor)
    raise NotImplementedError
  end
end
class ConcreteElementA < Element
  def accept(visitor)
    visitor.visit_element_a(self)
  end
  def operation_a
    "Element A operation."
  end
end
class ConcreteElementB < Element
  def accept(visitor)
    visitor.visit_element_b(self)
  end
  def operation_b
    "Element B operation."
  end
end
# Step 2: Visitor Interface and Concrete Visitor
class Visitor
  def visit_element_a(element)
    raise NotImplementedError
  end
  def visit_element_b(element)
    raise NotImplementedError
  end
end
class ConcreteVisitor < Visitor
  def visit_element_a(element)
    puts "Visitor on " + element.operation_a
  end
  def visit_element_b(element)
    puts "Visitor on " + element.operation_b
  end
end
# Client Code
element_a = ConcreteElementA.new
element_b = ConcreteElementB.new
visitor = ConcreteVisitor.new
element_a.accept(visitor)
element_b.accept(visitor)

Output:

Visitor on Element A operation.
Visitor on Element B operation.

Explanation:

1. The Element class is equipped with an 'accept' method which allows a visitor to "visit" it.

2. Concrete elements, ConcreteElementA and ConcreteElementB, implement their own unique operations and the 'accept' method.

3. The Visitor interface has methods for visiting each type of element.

4. The ConcreteVisitor class implements the operations to be performed on each element.

5. In the client code, elements accept the visitor, which then performs its operations on them.

7. When to use?

Use the Visitor Pattern when:

1. You have distinct and unrelated operations to perform across a set of objects, and you want to avoid contaminating their classes with these operations.

2. The object structure rarely changes but you often need to define new operations.

3. The features and classes of objects are fixed, but you need to perform operations that depend on the concrete classes of these objects.


Comments