Visitor

Intent

Visitor is a behavioral design pattern that let you define a new operation without changing the classes of the objects on which it operates.

Problem

Your team develops an app that works with geographic information structured as a graph. Nodes of the graph represent not only cities and towns but also many other types of locations, such as industries, sightseeings, etc. Nodes connected with each other if there is a road between them. Under the hood, each node is an object, and each node type represented by its own class.

You've got a task to make it possible to export the graph into XML. The job looked easy at first. You would need to add an export method for each node type and then go over the graph, executing this method for each node. The solution was not only simple but also elegant, since you leveraged the polymorphism to avoid coupling to concrete node classes.

But unfortunately, the system architect refused to allow you to alter existing node classes. The code was already in production and nobody wanted to risk breaking it.

In addition, he questioned whether the XML export makes sense in node classes in first place. The primary job of these classes is to work with geodata. The export behavior would look alien there.

There was another reason for the refusal. In close future, someone from the marketing department may ask you to change the export to a different format or add some other weird feature. That would force you to change that precious code again.

Solution

The Visitor pattern suggests placing a new behavior into a separate class, instead of integrating it into existing classes. Objects related to the behavior, won't be calling it by themselves. They will be passed as arguments to methods of the visitor object instead.

The behavior's code may be a bit different for different types of objects. Hence, the visitor class must have a set of behavioral methods with different parameter types:

class ExportVisitor implements Visitor is
    method doForCity(City c) { ... }
    method doForIndustry(Industry f) { ... }
    method doForSightSeeing(SightSeeing ss) { ... }
    // ...

But how exactly would we call these methods when dealing with a whole graph? The methods have different signatures, which doesn't allow us to use polymorphism. To pick a proper visitor's method able to process a given object, we would need to check its class. Doesn't that sound as a nightmare?

foreach (Node node : graph)
    if (node instanceof City)
        exportVisitor.doForCity((City) node);
    if (node instanceof Industry)
        exportVisitor.doForIndustry((Industry) node);
    // ...
}

The method overloading won't help either, given that our programming language supports it at all (like Java or C#). That is when you give all methods the same name, even if they support a different set of parameters. But since the exact class of a given node is not known in advance, the overloading won't be able to determine the correct method to execute. It will default to a visitor's method that expects an object of a base Node class as an argument.

But the Visitor pattern has a solution for that problem as well. It uses Double Displatch techniques to keep the polymorphism around. Instead of trying to pick a proper visitor's method ourselves, how about we delegate this to the objects we're passing to the visitor? Those objects are aware of their own class, which will let them pick a proper method less awkwardly.

// Client code
foreach (Node node : graph)
    node.accept(exportVisitor);

// City
class City is
    method accept(Visitor v) is
        v.doForCity(this);
    // ...

// Industry
class Industry is
    method accept(Visitor v) is
        v.doForIndustry(this);
    // ...

I confess. We had to change the node classes after all. But at least, the change is trivial and let us add further behaviors without altering any more existing code.

To make it truly happen, we must go further and extract a common interface for all future visitors. Now, if you need to introduce a new behavior to the program, all you have to do is to implement a new visitor class. And all existing node classes will be able to work with it just fine.

Real-World Analogy

Insurance agent

Imagine a novice insurance agent, eager to get new customers. He randomly visits all the houses in the neighborhood, offering his services. But for each of the "types" of houses he visits, he has a special offer.

  • To a residence, he sells medical insurance.
  • To a bank, he sells theft insurance.
  • To a business, he sells fire and floods insurance.

Structure

Visitor pattern structure
  1. Visitor defines a common interface for all types of visitors. It declares a set of visiting methods that accept various Context Components as parameters. The methods may have the same names in languages that support overloading, but their parameter types must be different.

  2. Concrete Visitors implements all the operations described in the common Visitor interface. Each concrete visitor represents a single behavior.

  3. Component defines a method for acceptance Visitors. The method should accept visitors as arguments using the common Visitor interface.

  4. Concrete Component implements the acceptance method. The purpose of this method is to redirect the call to a proper visitor's method corresponding to the current component class.

  5. Client represents a collection or some other complex object (for example, a Composite tree). Clients are usually not aware of the concrete classes of their components.

Pseudocode

In this example, the Visitor pattern adds XML export support to a hierarchy of geometric shapes.

// Сложная иерархия компонентов.
interface Graphic is
    method move(x, y)
    method draw()
    method accept(v: Visitor)

class Shape implements Graphic is
    field id

    // Метод принятия посетителя должен быть реализован в каждом компоненте, а
    // не только в базовом классе. Это поможет программе определить какой метод
    // посетителя нужно вызвать, в случае если вы не знаете тип компонента.
    method accept(v: Visitor) is
        v.visitDot(this);

class Dot extends Shape is
    field x, y
    // ...
    method accept(v: Visitor) is
        v.visitDot(this);

class Circle extends Dot is
    field radius
    // ...
    method accept(v: Visitor) is
        v.visitCircle(this);

class Rectangle extends Shape is
    field width, height
    // ...
    method accept(v: Visitor) is
        v.visitRectangle(this);

class CompoundGraphic implements Graphic is
    field children: array of Graphic
    // ...
    method accept(v: Visitor) is
        v.visitCompoundGraphic(this);


// Интерфейс посетителей должен содержать методы посещения каждого компонента.
// Важно, чтобы иерархия компонентов менялась редко, так как при добавлении
// нового компонента придётся менять всех существующих посетителей.
interface Visitor is
    method visitDot(d: Dot)
    method visitCircle(c: Circle)
    method visitRectangle(r: Rectangle)
    method visitCompoundGraphic(cs: CompoundGraphic)

// Конкретный посетитель реализует одну операцию для всей иерархии компонентов.
// Новая операция = новый посетитель. Посетитель выгодно применять, когда новые
// компоненты добавляются очень редко, а команды добавляются очень часто.
class XMLExportVisitor is
    method visitDot(d: Dot) is
        Export dot's id and center coordinates.

    method visitCircle(c: Circle) is
        Export circle's id, center coordinates and radius.

    method visitRectangle(r: Rectangle) is
        Export rectangle's id, left-top coordinates, width and height.

    method visitCompoundGraphic(cg: CompoundGraphic) is
        Export shape's id and the list of children ids.


// Приложение может применять посетителя к любому набору объектов компонентов,
// даже не уточняя их типы. Нужный метод посетителя будет выбран благодаря
// проходу через метод accept.
class Application is
    field allGraphics: array of Graphic

    method export() is
        exportVisitor = new XMLExportVisitor()

        foreach (allGraphics as graphic)
            graphics.accept(exportVisitor)

If you wonder why do we need the accept method in this example, then read our article Visitor and Double Dispatch, which addresses that question in detail.

Applicability

When you need to perform an operation on all elements of a complex object structure (for example, a tree), and all the elements are heterogeneous.

The Visitor pattern allows you to execute an operation over a set of objects with different classes.

When you need to be able to run several unrelated behaviors over a complex object structure, but you don't want to "clog" the structure's classes with the code of these behaviors.

The Visitor pattern allows you to extract and unify related behaviors from a bunch of classes that make an object structure, into a single visitor class. Such transformation will allow reusing these classes in various apps without carrying over nonrelevant behaviors

When a new behavior makes sense only for some classes from the existing hierarchy.

The Visitor pattern allows you to craft a special visitor class implements behavior for some objects, but not for others.

How to Implement

  1. Create the Visitor interface and declare a "visiting" method for each concrete component class that exists in the program.

  2. Add an abstract acceptance method to the base class of the component hierarchy.

  3. Implement the acceptance methods in all concrete components. They must redirect calls to a particular visitor's method that has a parameter of the same class as the current component.

  4. The component hierarchy should only be aware of the Visitor interface. On the other hand, visitors will be coupled to all concrete components.

  5. For each new behavior, create a new Concrete Visitor class and implement all of the visiting methods.

  6. The client will create visitor objects, and pass them into components via acceptance methods.

Pros and Cons

  • Simplifies adding new operations over a complex object structure.
  • Moves related behaviors into a single class.
  • A visitor can accumulate state over the course of working with an object structure.
  • The pattern is not justified if a hierarchy of components often changes.
  • Violates encapsulation of components.

Relations with Other Patterns

  • The Visitor pattern is like a more powerful version of the Command pattern that may execute an operation over an object of any type.

  • Visitor can apply an operation over entire Composite tree.

  • Visitor can be used along with the Iterator pattern to traverse a complex data structure and execute some operation over all its elements, even if they have different types.

Implementations in Different Programming Languages

Java

Extra Content